Centric connect.engage.succeed

Building an interactive floor plan using out-of-the-box SharePoint components (and some JavaScript) – Part 2

Geschreven door Erik Nell - 01 november 2016

Erik Nell
So, now that we have set the preliminaries we are ready to start building the solution. First, let’s get the infrastructure for storing all our information.

Interactive floor plan

Lists and Libraries

As stated in part 1, I need two lists, and two libraries:

  1. Conference Rooms: This list contains information about individual conference rooms
  2. Conference Room Layout: A list of all available floor plans
  3. Conference Room Pictures: An asset library containing the actual pictures you want to show with the conference room information
  4. SiteScriptsCSS: A document library for storing all scripting assets

The Conference Room list is relatively straightforward. It contains columns for room name, phone number and equipment information. The most important column is Conference Room ID. This is the unique identifier of the room and is used throughout the solution. See Figure 1 for an example of one room.

The Conference Room Layout list (figure 2) contains one item for each floor (plan). Noteworthy here are the Building Number column, which will be the main column used for identifying the correct floor plan, and the HotSpots column, which I will explain later on when discussing the display part of the solution.

Figure 1 A conference room item                

      Figure 2 Conference room layout item           

The PictureWidth column gives an additional option to scale the final picture. This is entered as a percentage.

Figure 3 Conference room picture

The Conference Room Pictures library (figure 3) is a standard asset library with a couple of additional columns. Building Number ties the picture together with the Conference Room Layout list, ConferenceRoomID is the unique identifier of the room and ties this list together. The Include in Room Overview column determines whether the picture should be included in the detail view. This allows you to have more pictures in the library than you want to display. This is handy if you want to change the photos you display in the overview from time to time.

Getting it on screen

So, now that we have a structure to store our information, how do we get it all displayed?

We start with an empty page. All data will be loaded from javascript using JSOM after the page has loaded. The page can be called using an additional query string parameter called “Building”. This will serve as the main filter for loading all additional data. So let’s say that our page is called “ConfOverview.aspx”. In that case, a call to .../confOverview.aspx?Building=1 will load data for building 1 only.

To start it all, I added an html file that will take care of loading all additionally needed javascript, CSS, etc.

<script type="text/javascript" src="/FloorPlanDemo/SiteScriptsCSS/ConferenceRooms/scripts/ConferenceRooms.js"></script>
<script type="text/javascript" src="/FloorPlanDemo/SiteScriptsCSS/ConferenceRooms/scripts/mustache.min.js"></script>
<script type="text/javascript" src="/FloorPlanDemo/SiteScriptsCSS/ConferenceRooms/scripts/jquery.reveal.js"></script>

<link href="/FloorPlanDemo/SiteScriptsCSS/ConferenceRooms/scripts/ConferenceRooms.css" rel="stylesheet">
<link href="/FloorPlanDemo/SiteScriptsCSS/ConferenceRooms/scripts/reveal.css" rel="stylesheet">

……..

The start of that file looks like this:

As you can see in the code, I will use two plugins to get things done: Mustache and jQuery.Reveal. More on that later.

The heart of the solution is the code in the ConferenceRooms.js file. This is where all the magic happens. Let’s go through it step by step.

First, get the query string parameter and load the data:

$(document).ready(function(){
    //Make sure we can call JSRequest
    JSRequest.EnsureSetup();
    window.buildingNumber = JSRequest.QueryString["Building"];
    // Omitted some variable instancing
    //….

    //Make sure that sp.js is loaded and then execute function loadData
    ExecuteOrDelayUntilScriptLoaded(loadData, "sp.js");
});

Loading the data is where JSOM comes into play. We have to prepare some queries on the two lists and the picture library to get the data we need:

function loadData(){
    var context = new SP.ClientContext();
    var oWebsite = context.get_web();
    var oRoomList = oWebsite.get_lists().getByTitle('Conference Rooms');
    var oPictureList = oWebsite.get_lists().getByTitle('Conference Room Pictures');
    var oLayoutList = oWebsite.get_lists().getByTitle('Conference Room Layout');
    var camlQuery = new SP.CamlQuery();
    var camlQueryPictures = new SP.CamlQuery();
    var camlQueryLayout = new SP.CamlQuery();
    if(buildingNumber != undefined){
        camlQuery.set_viewXml("<View><Query><Where><Eq><FieldRef Name='Building_x0020_Number' /><Value Type='Text'>" + buildingNumber.toString() + "</Value></Eq></Where></Query></View>");    
           camlQueryPictures.set_viewXml("<View Scope='RecursiveAll'><Query> <Where><And><And><Eq><FieldRef Name='Building_x0020_Number' /><Value Type='Text'>"+ buildingNumber.toString() + "</Value></Eq><Eq><FieldRef Name='FSObjType' /><Value Type='Integer'>0</Value></Eq></And><Eq><FieldRef Name='IncludeInRoomOverview' /><Value Type='Boolean'>1</Value></Eq></And></Where></Query></View>");        
           camlQueryLayout.set_viewXml("<View><Query><Where><Eq><FieldRef Name='Building_x0020_Number' /><Value Type='Text'>"+ buildingNumber.toString() + "</Value></Eq></Where></Query></View>");    
    }
    else{
        camlQuery.set_viewXml("<View><Query></Query></View>");    
           camlQueryPictures.set_viewXml("<View Scope='RecursiveAll'><Query><Where><And><Eq><FieldRef Name='FSObjType' /><Value Type='Integer'>0</Value></Eq><Eq><FieldRef Name='IncludeInRoomOverview' /><Value Type='Boolean'>1</Value></Eq></And></Where></Query></View>");
           camlQueryLayout.set_viewXml("<View><Query></Query></View>");
    }
    this.colRoomList = oRoomList.getItems(camlQuery);
    this.colPictureList = oPictureList.getItems(camlQueryPictures);
    this.colLayoutList = oLayoutList.getItems(camlQueryLayout);
    context.load(colPictureList,'Include(Title, Building_x0020_Number, ConferenceRoomID, FileRef)');
    context.load(colRoomList, 'Include(Title, Capacity, Equipment, Phone_x0020_Number, Remarks, ConferenceRoomID, Building_x0020_Number, ID, Address_x0020_Book_x0020_Name)');    
    context.load(colLayoutList, 'Include(Title, PictureURL, HotSpots, PictureWidth)');
    context.executeQueryAsync(RoomCheckSucceeded, checkFailed);
}

This code will instruct SharePoint to load data from Conference Rooms, Conference Room Pictures and Conference Room Layout, filtered by Building Number. If no Building Number was given in the query string, this code will load everything. The final row of code will then trigger the actual loading of the data. Because this will take some time, the call is asynchronous. That’s why you have to give two callback functions as parameters, the first for when the call is successful (let’s hope it always will be) and the second is for possible failure to load. All I will do in that case is display an alert that the data cannot be retrieved (tough luck!).

Note: In this demo solution, I take the all-or-nothing approach. Either loading all three lists succeeds or it all fails. In the actual solution, I did take a more subtle approach. It will load the floor plan, the conference room information and the pictures separately and will fail more gracefully. For demonstration purposes, this would complicate the code too much.

So, let’s assume that the loading was successful, meaning that the RoomCheckSucceeded function will be called:

function RoomCheckSucceeded(sender, args){
    //Build an array with room information
    var listItemEnumerator = colRoomList.getEnumerator();
    while (listItemEnumerator.moveNext()) {
        var oListItem = listItemEnumerator.get_current();	
        rooms.push({
        	Title: oListItem.get_item('Title'),
        	Capacity: oListItem.get_item('Capacity'),
        	Equipment: oListItem.get_item('Equipment'),
	PhoneNumber: oListItem.get_item('Phone_x0020_Number'),
	Remarks: oListItem.get_item('Remarks'),
	ConferenceRoomID: oListItem.get_item('ConferenceRoomID'),
	BuildingNumber: oListItem.get_item('Building_x0020_Number'),
	OutlookName: oListItem.get_item('Address_x0020_Book_x0020_Name'),
	ID: oListItem.get_item('ID')
        });
    }
    //Build an array with picture information
    listItemEnumerator = colPictureList.getEnumerator();
    while (listItemEnumerator.moveNext()) {
        var oListItem = listItemEnumerator.get_current();	
        pictures.push({
        	Title: oListItem.get_item('Title'),
	ConferenceRoomID: oListItem.get_item('ConferenceRoomID'),
	BuildingNumber: oListItem.get_item('Building_x0020_Number'),
	FileRef: oListItem.get_item('FileRef')
        });
    }
    //Map the pictures over the correct room elements so they become an array within the room object
    window.roomList = $.map(rooms,function(room){
        roomPictures = pictures.filter(function(picture){
            return picture.ConferenceRoomID === room.ConferenceRoomID;
        });	
        room.Pictures = roomPictures;
        return room;
    }); 
    //Get the floor plan information	
    listItemEnumerator = colLayoutList.getEnumerator();
    listItemEnumerator.moveNext();
    var oListItem = listItemEnumerator.get_current();
    layout.Title = 	oListItem.get_item('Title');
    layout.PictureURL = oListItem.get_item('PictureURL').get_url();
    layout.Width = oListItem.get_item('PictureWidth');
    layout.Hotspots=[];
	var oHotspots = JSON.parse(oListItem.get_item('HotSpots')); 
	layout.Hotspots=oHotspots; 
    renderRoomList();
}
This function will collect all data from the three lists and will remake them into an array of room objects called rooms and a floor plan object called layout.

Why all the trouble, you might ask. Well… this is where Mustache enters the stage. Mustache is a very neat and lightweight templating system that allows you to separate layout from content. Because we are building the page from code, the alternative to Mustache would have been to include all the necessary HTML tags right into our javascript. With Mustache, we can keep our code clean because Mustache will take care of this.

Our loader html file also contains the Mustache templates to display our data. The first one is for displaying the floor plan:

<div class="buildingWrapper" id="roomLayout">
    <div style="float:left;top:200px;left:100px">
        <img src='/_layouts/images/gears_an.gif' />
    </div>
    <script type="x-tmpl-mustache" id="roomLayoutTemplate">
        <div class="buildingTitle">
            {{Title}}
        </div>
        <div class="buildingPicture" style="left:0px;top:0px">
            <img src="{{PictureURL}}" style="width:{{Width}}%;left:-10px;z-index: 1"/>
        </div>
        {{#Hotspots}}
            <div class="buildingHotspot" data-roomName="room{{RoomID}}" style="cursor:pointer;position:absolute; left:{{left}}%; top:{{top}}%; height:{{height}}%; width:{{width}}%; z-index: 10" onclick="javascript:openRoomPopup('room{{RoomID}}')">
            </div>
        {{/Hotspots}}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    </script>
</div>

The second template will load all necessary room information (for this building). The accompanying CSS will keep it hidden until the user clicks on a room.

<div class="roomDescriptions"id="RoomDescriptions">Loading...</div>
<script type="x-tmpl-mustache" id="roomDescriptionTemplate">
    {{#.}}
        <div id="room{{ConferenceRoomID}}">
	<div class="roomDescription">
		<div class="roomName">
			<div class="roomTitle">
				{{Title}}
			</div>
			<div class="roomOutlookName">
				{{OutlookName}}
			</div>
		</div>
		<div class="roomPictures">
			{{#Pictures}}
			<img class="roomPicture" src="{{FileRef}}" />
			{{/Pictures}}
		</div>
		<div class="roomText">
			<div class="roomSeats">
				<div class="roomColumn">Capacity:</div>
				<div class="roomValue">{{Capacity}}</div>
			</div>
				<div class="roomEquipment">
					<div class="roomColumn">Equipment:</div>
					<div class="roomValue">{{Equipment}}</div>
				</div>
				<div class="roomPhoneNumber">
					<div class="roomColumn">Phone Number:</div>
					<div class="roomValue">{{PhoneNumber}}</div>
				</div>
				<div class="roomRemarks">
					<div class="roomColumn">Remarks:</div>
					<div class="roomValue">{{Remarks}}</div>
				</div>
			</div>
		</div>
	</div>	
    {{/.}}
</script>


During runtime, these templates will be rendered by feeding them the relevant object (in our case the floor plan object and the array of room objects). The parts with the double curly brackets will be replaced by the corresponding values from the data object. The {{#.} part will instruct Mustache to repeat the section for each object it finds in the collection (in this case our array).

In our code, the renderRoomList() function will take care of this rendering:

function renderRoomList(){
    //Big picture with hotspots
    var template = $('#roomLayoutTemplate').html();
    var rendered = Mustache.render(template, layout);
    $('#roomLayout').html(rendered);

    //Room descriptions
    template = $('#roomDescriptionTemplate').html();
    rendered = Mustache.render(template, roomList);
    $('#RoomDescriptions').html(rendered);
}


After rendering, the floor plan will be visible on the page and all room information, including the pictures, is loaded on the page (although still invisible).

Wrapping up

Well, that’s it for this time. We managed to get the room information and the floor plan on the page. That leaves us with implementing the behaviour that we want. We still have nothing in place for actually displaying the room information. It’s all on the page, but it is hidden, waiting for the user to click on a room in the floor plan.

In the next (and probably last) instalment of this series, we will tackle the issue of getting the interaction working and I will discuss some thoughts and experiences that popped up during the evaluation of this job.

Until next time!

Craft Expert Erik Nellis part of Team Office 365 within Craft, the development programme for IT professionals (powered by Centric). If you would like to follow his blog, sign up for Craft updates.

     
Schrijf een reactie
  • Captcha image
  • Verzenden