SharePoint Drag Drop Tile Selector - Part 2

In Part 1 we created the content types, libraries, folder structure and layout page to make this work. Now for the fun bit...

Javascript - aka the fun part

Open custom-request.js. We are going to make a REST module that can be plugged into any Sharepoint Site.

Create an object (use the same object for all of your custom modules).

window.KEH = window.KEH || {};  

This uses the current object or creates a new one if it does not exist.

Here's the basic frame for the module:

window.KEH.RequestREST = (function() {  
    var makeRequest = function() {
    };
    return {
        makeRequest : makeRequest
    };
})();

Groovy! Now let's make it do something. I am using the RequestExecutor from SharePoint. This is not actually necessary since we are not making cross-domain requests, but it makes the module useable with the App model as well as a plugin for a custom layout. It could be refactored a bit to use jQuery ajax for all calls (this would be a touch faster than request executor, since it doesn't request context first).

It will accept a number of parameters and will return a deferred promise if that is requested.

Anyway, here's what you want from this file:

window.KEH = window.KEH || {};

/**********************************************
 * Request module returns makeRequest function
 *********************************************/

window.KEH.RequestREST = (function() {

    var makeRequest = function (deferred, url, query, method, body, headers, success, error, defObj) {
            // must be called after init, must have request executor and sp.js loaded
            // default values
            deferred    =    deferred    ||    false,
            defObj      =    defObj      ||    null,
            url         =    url         ||    _spPageContextInfo.webAbsoluteUrl,
            query       =    query       ||    null,
            method      =    method      ||    'POST',
            body        =    body        ||    '',
            headers     =    headers     ||    { 
                                                'accept': 'application/json; odata=verbose',
                                                'content-type': 'application/json; odata=verbose'
                                               },
            success     =    success     ||    function(data) { console.log("Successfully posted request!") },
            error       =    error       ||    function(data, errorCode, errorMessage) { 
                                                console.log("Error: " + errorMessage) };

            /**********************************
             * function vars
             * set up request executor
             * build query string
             *********************************/

            var re      =    new SP.RequestExecutor( _spPageContextInfo.webAbsoluteUrl ),
                uri     =    query !== null ? url + '?' + query : url;

            // execute request
            re.executeAsync({
                url: uri,
                method: method,    
                body: body, 
                headers: headers,
                success: success,
                error: error
            });

            // if deferred return data object
            if (deferred)
                return defObj.promise();
            else
                return null;
    };

    return {
        makeRequest : makeRequest
    };

})();

The next file is going to make the requests and build the items and append them to the DOM. There could be further modularization done here and I will update if I do that. Notice that I locally scope jQuery and the other modules when they are called for within a module.

We need a few modules here one for the tiles, one for drag-drop functionality and init:

window.KEH = window.KEH || {};

/********************************************************
 * Nav Tile Items Module - Requires Request Module
 * This module refers to an icon library with these fields
 ***** icon : image
 ***** Title : display name
 ***** Background Color : rgb value
 ***** Link for this icon : default link to direct to
 * Returns these public methods:
 ***** getAvailableItems
 ***** getSelectedItems
 ***** setSelectedItems
 ***** sortItems
 ***** makeEditPanels
 ***** makeDisplayTiles
 *******************************************************/

window.KEH.NavTileItems = (function(req, $) {

    // private edit panel maker
    // this will appear when the page is in edit mode
    // edit mode must have zones with id 'available-tiles' and 'selected-tiles'
    var editPanel = function() {

    };

    // private display panel maker
    // this appears when not in edit mode
    // requires div with class 'nav-tiles-display'
    var displayPanel = function() {

    }

    // public function to get all available icon items in JSON - parse to return object
    var getAvailableItems = function() {            

    };

    // public function to get all selected icon items in JSON - parse to return object
    var getSelectedItems = function() {

    };

    // private function to create selected items object by comparing string names 
    // using the Title in available.d.results and text node from selected tile in edit mode
    // sets url in new object based on the field in the edit screen
    var parseSelectedItems = function() {

    };    

    // public function to set selected icon items in JSON object
    // this sets the value in the field on the edit screen and will update on page save.
    var setSelectedItems = function() {

    };

    // public function to compare selected and available and to select which area to place them in in edit
    // removes items in selected from display in available
    var sortItems = function() {

    };

    // public function to make Edit panels
    var makeEditPanels = function() {

    };
    // public function to make Display tiles
    var makeDisplayTiles = function() {

    };

    return {
        getAvailableItems   : getAvailableItems,
        getSelectedItems    : getSelectedItems,
        setSelectedItems    : setSelectedItems,
        sortItems           : sortItems,
        makeEditPanels      : makeEditPanels,
        makeDisplayTiles    : makeDisplayTiles
    }

})(window.KEH.RequestREST, window.jQuery);

/**********************************************
 * Enable drag and drop sorting in edit mode
 * TODO - Pull into it's own module
 *********************************************/

window.KEH.Sorting = (function($) {

    var enableSorting = function() {

    };

    return {
        enableSorting : enableSorting
    }

})(window.jQuery);

/**************************************
 * init
 *************************************/

window.KEH.NavButtons = (function ( req, items, sort, $ ) {  
    var _webUrl,
        _siteUrl,
        _listTitle,
        _userId;

    // public init function
    var init = function() {

    };

    return {
        init : init
    }

})(window.KEH.RequestREST, window.KEH.NavTileItems, window.KEH.Sorting, window.jQuery);

// initiate the above
(function() {
    ExecuteOrDelayUntilBodyLoaded(function () {
        SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
            SP.SOD.registerSod('sp.requestexecutor.js', '/_layouts/15/sp.requestexecutor.js');
            SP.SOD.executeFunc('sp.requestexecutor.js', 'SP.RequestExecutor', function () {
                window.KEH.NavButtons.listTitle = 'Site Icon Nav Buttons',    
                window.KEH.NavButtons.init();
            });
        });
    });
})();

TODO On this post, explain each module in more depth. For now...

Here's the finished script.js file:

window.KEH = window.KEH || {};

/********************************************************
 * Nav Tile Items Module - Requires Request Module
 * This module refers to an icon library with these fields
 ***** icon : image
 ***** Title : display name
 ***** Background Color : rgb value
 ***** Link for this icon : default link to direct to
 * Returns these public methods:
 ***** getAvailableItems
 ***** getSelectedItems
 ***** setSelectedItems
 ***** sortItems
 ***** makeEditPanels
 ***** makeDisplayTiles
 *******************************************************/

window.KEH.NavTileItems = (function(req, $) {

    // private edit panel maker
    // this will appear when the page is in edit mode
    // edit mode must have zones with id 'available-tiles' and 'selected-tiles'
    var editPanel = function(items, parent) {
        // available items object is items.d.results selected is items.results or null
        if(items != null && typeof items.results === 'undefined') {
            var itemsResults    = items.d.results,
                buttonClasses   = 'button-items col-sm-6',
                inputClasses    = 'col-sm-12 ms-hidden';
        }
        else {
            if (items === null || items.results.length === 0)
                parent.append('<div class="col-sm-12">Drag Items Here</div>');
            else {
                var itemsResults  = items.results,
                    buttonClasses = 'button-items col-sm-12',
                    inputClasses  = 'col-sm-12';
            }
        }        

        if(items != null && typeof itemsResults != 'undefined') {
            for(var i = 0; i < itemsResults.length; i++) {
                // create available items elements
                var item         =    itemsResults[i],
                    itemName     =    item.Title,
                    itemColor    =    item.Background_x0020_Color,
                    itemEl       =    $("<div class='"+ buttonClasses +"'>"+ itemName+"</div>");
                    itemUrlEdit  =    $("<input class='"+ inputClasses +"' type='text' name='edit-url' "
                                        +    "id='"+ itemName +"-url' "
                                        +    "placeholder='"+ item.Link_x0020_for_x0020_this_x0020_icon +"' "
                                        +    "value='"+ item.Link_x0020_for_x0020_this_x0020_icon +"' />");
                // add url input as hidden element that appears when dragged
                itemEl.append(itemUrlEdit);
                itemEl.css({
                    'background-color': 'rgb('+itemColor+')',
                    'color': '#fff'
                });
                // add to DOM        
                parent.append(itemEl);
            }
        }
    };

    // private display panel maker
    // this appears when not in edit mode
    // requires div with class 'nav-tiles-display'
    var displayPanel = function(items, parent) {
        var selectedItems = items || {'results':[]};
        if(selectedItems.results.length === 0)
            parent.append('<h1>Edit This Page To Select Tiles</h1>');
        else {
            for(var i = 0; i < items.results.length; i++) {
                // create elements
                var item            =    items.results[i],
                    itemName        =    item.Title,
                    itemColor       =    'rgb('+ item.Background_x0020_Color +')',
                    itemLink        =    item.Link_x0020_for_x0020_this_x0020_icon,
                    itemIcon        =    item.FieldValuesAsText.FileRef,
                    itemContainer   =    $('<a class="keh-nav-tile col-md-4 col-xs-6" href="'
                                          + itemLink +'" />'),
                    itemNavBlock    =    $('<div class="keh-nav-block" />'),
                    itemNavContent  =    $('<div class="keh-nav-content" />'),
                    itemIcon        =    $('<div class="keh-nav-icon">'
                                          +    '<img src="'+ itemIcon +'" alt="" />'
                                          +'</div>'),
                    itemTitle       =    $('<div class="keh-nav-title"><h2>'+ itemName +'</h2></div>');

                itemNavContent.append([itemIcon, itemTitle]);
                itemNavBlock.css('background-color', itemColor).append(itemNavContent); 
                itemContainer.append(itemNavBlock);                
                parent.append(itemContainer);                                         
            }
        }
    }

    // public function to get all available icon items in JSON - parse to return object
    var getAvailableItems = function(webUrl, listTitle) {            
        var targetUrl         =    webUrl + "/_api/web/lists/getbytitle('"+listTitle+"')/getitems",
            queryString       =    "$select=Title,Background_x0020_Color,Link_x0020_for_x0020_this_x0020_icon,"
                                    + "FieldValuesAsText/FileRef&$expand=FieldValuesAsText",
            method            =    'POST',
            body              =    "{ 'query' : {'__metadata': { 'type': 'SP.CamlQuery' }, \"ViewXml\": \"<View></View>\" } }",
            defObj            =    $.Deferred(),
            headers           =    {
                                    "accept": "application/json; odata=verbose",
                                    "content-type": "application/json; odata=verbose"
                                   },
            success           =    function(data) {
                                     window.KEH.NavTileItems.availableItemsObj = JSON.parse(data.body);
                                     defObj.resolve(window.KEH.NavTileItems.availableItemsObj);
                                   },
            error             =    function(data, errorCode, errorMessage) { console.log("Error: " + errorMessage) },
            availableItems    =    req.makeRequest(true, targetUrl, queryString, method, body, headers, success, error, defObj);

        return availableItems;
    };
    // public function to get all selected icon items in JSON - parse to return object
    var getSelectedItems = function(webUrl, listTitle) {
        var targetUrl        =    webUrl + "/_api/web/lists/getbytitle('"+listTitle+"')/items(" 
                                  + _spPageContextInfo.pageItemId + ")",
            queryString,
            method           =    'GET',
            body,
            defObj           =     $.Deferred(),
            headers          =    {
                                    "accept": "application/json; odata=verbose",
                                    "content-type": "application/json; odata=verbose"
                                  },
            success          =    function(data) {
                                    var selectedItems = JSON.parse(data.body);
                                    // returning 1 level deeper than available to make sorting easier
                                    defObj.resolve(selectedItems.d);
                                  },
            error            =    function(data, errorCode, errorMessage) { console.log("Error: " + errorMessage) },
            selectedItems    =    req.makeRequest(true, targetUrl, queryString, method, body, headers, success, error, defObj);

        return selectedItems;

    };

    // private function to create selected items object by comparing string names 
    // using the Title in available.d.results and text node from selected tile in edit mode
    // sets url in new object based on the field in the edit screen
    var parseSelectedItems = function(availableItemObject, selectedItems) {

        var sItemsObjOrdered = {'results':[]};

        for(var i = 0; i < selectedItems.length; i++) {
            var itemName     =    selectedItems[i].name,
                itemUrl      =    selectedItems[i].url;

            for(var j = 0; j < availableItemObject.d.results.length; j++) {
                var aItemName = availableItemObject.d.results[j].Title;
                if (itemName === aItemName){
                    availableItemObject.d.results[j].Link_x0020_for_x0020_this_x0020_icon = itemUrl;
                    sItemsObjOrdered.results.push(availableItemObject.d.results[j]);
                }
            }
        }        
        var sObjJSON = JSON.stringify(sItemsObjOrdered);
        return sObjJSON.toString();
    };    

    // public function to set selected icon items in JSON object
    // this sets the value in the field on the edit screen and will update on page save.
    var setSelectedItems = function() {

        var selectedItems = [];

        $('#selected-tiles .button-items').each(function(){ 
            selectedItems.push({
                name    : $(this).text(),
                url     : $(this).children('input')[0].value
            }); 
        });

        var aObj       = $.extend(true, {}, window.KEH.NavTileItems.availableItemsObj),
            sItemsJSON = parseSelectedItems(aObj, selectedItems);

        $('#keh-icon-json textarea').text(sItemsJSON);

        return false;            
    };

    // public function to compare selected and available and to select which area to place them in in edit
    // removes items in selected from display in available
    var sortItems = function(available, selected) { 

        var selectedItems = selected || {'results':[]};

        if(selectedItems.results.length === 0)
            return available;
        else {
            for( var i = 0; i < selectedItems.results.length; i++ ) {
                var sItem  = selectedItems.results[i],
                    sTitle = sItem.Title;

                for( var j = available.d.results.length-1; j > -1; j-- ) {
                    var aItem  = available.d.results[j],
                        aTitle = aItem.Title;

                    if (sTitle === aTitle) {
                        available.d.results.splice(j, 1);
                        break;
                    }
                }                
            }
            return available;
        }
    };

    // public function to make Edit panels
    var makeEditPanels = function(availableSorted, selected) {
        var editEl = $('div.nav-tiles-edit'),        
            availableParent    = $('#available-tiles'),
            selectedParent     = $('#selected-tiles');
        editPanel(availableSorted, availableParent);
        editPanel(selected, selectedParent);
        return false;
    };
    // public function to make Display tiles
    var makeDisplayTiles = function(selected) {
        var displayEl = $('div.nav-tiles-display');
        displayPanel(selected, displayEl);
    };

    return {
        getAvailableItems   : getAvailableItems,
        getSelectedItems    : getSelectedItems,
        setSelectedItems    : setSelectedItems,
        sortItems           : sortItems,
        makeEditPanels      : makeEditPanels,
        makeDisplayTiles    : makeDisplayTiles
    }

})(window.KEH.RequestREST, window.jQuery);

/**********************************************
 * Enable drag and drop sorting in edit mode
 * TODO - Pull into it's own module
 *********************************************/

window.KEH.Sorting = (function($) {

    var enableSorting = function(sortZones, sortSelector) {
        $(sortZones).sortable({
            connectWith: sortSelector,
            helper: 'clone',
            // toggle size of element from available to selected and visibility of url input
            receive: function( event, ui ) {
                if (ui.sender.attr('id') === 'available-tiles'){
                    ui.item.removeClass('col-sm-6').addClass('col-sm-12');
                    ui.item.children('input').removeClass('ms-hidden');
                }
                else{
                    ui.item.removeClass('col-sm-12').addClass('col-sm-6');
                    ui.item.children('input').addClass('ms-hidden');
                }

            },
            update: function( event, ui ) {

            }
        });
    };

    return {
        enableSorting : enableSorting
    }

})(window.jQuery);

/**************************************
 * init
 *************************************/

window.KEH.NavButtons = (function ( req, items, sort, $ ) {  
    var _webUrl,
        _siteUrl,
        _listTitle,
        _userId;

    // public init function
    var init = function() {
        _siteUrl            =    _spPageContextInfo.siteAbsoluteUrl,
        _webUrl             =    _spPageContextInfo.webAbsoluteUrl,
        _listTitle          =    window.KEH.NavButtons.listTitle,
        _userId             =    _spPageContextInfo.userId,
        iconEditTextArea    =    $('#keh-icon-json textarea');

        if(iconEditTextArea.length > 0) {        

            var selectedItems    =     items.getSelectedItems( _webUrl, 'Pages' ),
                availableItems   =     items.getAvailableItems( _siteUrl, _listTitle );

            jQuery.when(selectedItems, availableItems).done(function(sdata, adata){

                var selected    =     JSON.parse(sdata.KEH_x0020_Page_x0020_Icons_x0020_JSON_x0020_Object),
                    aObj        =     jQuery.extend(true, {}, adata),
                    sorted      =     items.sortItems(aObj, selected);

                items.makeEditPanels(sorted, selected);
                sort.enableSorting('#available-tiles, #selected-tiles','.connected-sortable');
                $('#selection-submit').click(function(){
                    items.setSelectedItems();
                });
            })
            .fail(function(e) { alert(e.message); });        
        }
        else {

            var selectedItems = items.getSelectedItems(_webUrl, 'Pages');        

            selectedItems
                .done(function(sdata) {
                    var selected = JSON.parse(sdata.KEH_x0020_Page_x0020_Icons_x0020_JSON_x0020_Object);
                    items.makeDisplayTiles(selected);
                })
                .fail(function(e) { alert(e.message); });
        }

    };

    return {
        init : init
    }

})(window.KEH.RequestREST, window.KEH.NavTileItems, window.KEH.Sorting, window.jQuery);

// initiate the above
(function() {
    ExecuteOrDelayUntilBodyLoaded(function () {
        SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
            SP.SOD.registerSod('sp.requestexecutor.js', '/_layouts/15/sp.requestexecutor.js');
            SP.SOD.executeFunc('sp.requestexecutor.js', 'SP.RequestExecutor', function () {
                window.KEH.NavButtons.listTitle = 'Site Icon Nav Buttons',    
                window.KEH.NavButtons.init();
            });
        });
    });
})();

Check all of the files that were created in, then head over to the new site you created, add a new page and select your new layout.