Archive for March, 2007

Bob’s ultimate view navigator – categorized view

I love Bob Obringer’s solution for a view navigator. Especially you can easily improve the user’s navigation experience in existing applications.

For example I received a few days ago a request to investigate the options to improve performance on a view. After a quick investigation default a view was opened with the URL parameter &Count=1000. Happy waiting! Further the application enables you to navigate through the documents by opening a categorized view with &RestrictToCategory= parameter, again followed by the &Count=1000 parameter.

Implementing Bob’s solution for the flat was was piece of cake, just copy & paste.

Unfortunately Bob’s approach only works on a flat view and some of the functionality it is is not suitable for a categorized view:

  • the cookie function
  • the function to enter a page number directly in an input field

 Therefor I rewrote the script a bit, preserving 95% of Bob’s code. Thanks Bob!

viewnav02.jpg

URL opened:

http://server.domain.com/path/database.nsf/web_internetRegionCat?OpenView&RestrictToCategory=USA&Count=15

// initViewNav function, loaded at onLoad event of form
/************************************************/
function initViewNav(range, cache, div1, div2) {
 navRange = range;
 navCache = cache;
 navDiv1 = div1;
 navDiv2 = div2;
 startDoc = queryString(’start’);
 startDoc = (startDoc == “”) ? 1 : parseInt(startDoc);
 docsPerPage = queryString(‘count’);
 docsPerPage = (docsPerPage == “”) ? 20 : parseInt(docsPerPage);
 waitForDocCount()
}

// QueryString function
/****************************************************/
function queryString(key){
 var page = new PageQuery(window.location.search);
 return unescape(page.getValue(key.toLowerCase()));
}

function PageQuery(q) {
 if(q.length > 1) this.q = q.substring(1, q.length);
  else this.q = null;
 this.keyValuePairs = new Array();
 if(q) {
  for(var i=0; i < this.q.split(“&”).length; i++) {
   this.keyValuePairs[i] = this.q.split(“&”)[i].toLowerCase();
  }
 }
 this.getKeyValuePairs = function() { return this.keyValuePairs; }
 this.getValue = function(s) {
  for(var j=0; j < this.keyValuePairs.length; j++) {
   if(this.keyValuePairs[j].split(“=”)[0] == s)
   return this.keyValuePairs[j].split(“=”)[1];
  }
  return “”;
 }
 this.getParameters = function() {
  var a = new Array(this.getLength());
  for(var j=0; j < this.keyValuePairs.length; j++) {
   a[j] = this.keyValuePairs[j].split(“=”)[0];
  }
  return a;
 }
 this.getLength = function() { return this.keyValuePairs.length; }
}
/****************************************************/

// Cookie functions
/****************************************************/
function getExpiryDate(minutes){
 var UTCstring;
 Today = new Date();
 nomilli = Date.parse(Today);
 // Today.setTime(nomilli + (minutes * 60000));
 // Today.setTime(nomilli + (minutes * 0)); 
 // Means Cookies disabled!
 // This done because we work with a &RestrictToCategory command in a categorized view and we switch all the time from categories…
 Today.setTime(nomilli + (minutes * 0));
 UTCstring = Today.toUTCString();
 return UTCstring;
}

function setCookie(name, value, duration){
 cookiestring = name + “=” + escape(value) + “; EXPIRES=” + getExpiryDate(duration);
 document.cookie = cookiestring;
}

function getCookie(cookiename) {
 var cookiestring = “” + document.cookie;
 var index1 = cookiestring.indexOf(cookiename);
 if (index1 == -1 || cookiename == “”) return “”;
  var index2 = cookiestring.indexOf(‘;’, index1);
 if (index2 == -1) index2 = cookiestring.length;
  return unescape(cookiestring.substring(index1 + cookiename.length + 1, index2));
}
/*****************************************************/

// Get View Navigator HTML
/*****************************************************/
var startDoc = 0;
var docsPerPage = 0;
var totalPages = 0;
var navRange = 0;
var navCache = 0;
var navDiv1 = “”;
var navDiv2 = “”;
// var checkCookie = true;
// cookies has not use with a &RestrictToCategory command in a categorized view
var checkCookie = false;
var totalDocs = “”;
function waitForDocCount() {
 if (checkCookie) {
  totalDocs = getCookie(‘totaldocs’);
  checkCookie = false;
 }
 if (totalDocs == “”) {
  getDocCount();
  setTimeout(“waitForDocCount()”, 100);
 } else {
  drawViewNav();
 }
}
/*******************************************************/

// Function to get document count using XMLHTTP
/*******************************************************/
function getDocCount () {
 var xmlHttp = getXMLHTTP();
 xmlHttp.open(“GET”, dbPath + viewAlias + “?ReadViewEntries&RestrictToCategory=”+ Categories );
 xmlHttp.onreadystatechange = function() {
  if (xmlHttp.readyState == 4 && xmlHttp.responseText) {
   var resp = xmlHttp.responseText;
   var countTag = resp.toLowerCase().indexOf(‘toplevelentries’);
   if (countTag > 0) {
    resp = resp.substr(resp.indexOf(‘”‘, countTag) + 1);
    totalDocs = resp.substring(0, resp.indexOf(‘”‘));
    setCookie(‘totaldocs’, totalDocs, navCache)
   }
  }
 };
 xmlHttp.send(null);
}
/**************************************************/

// Wrapper function to get a cross browser XMLHTTP object
/**************************************************/
function getXMLHTTP() {
 // function to create an XmlHttp object
 var xmlHttp = null
 try {
  xmlHttp = new ActiveXObject(“Msxml2.XMLHTTP”)
 } catch(e) {
  try {
   xmlHttp = new ActiveXObject(“Microsoft.XMLHTTP”)
  } catch(oc) {
   xmlHttp = null
  }
 }
 if(!xmlHttp && typeof XMLHttpRequest != “undefined”) {
  xmlHttp = new XMLHttpRequest()
 }
 return xmlHttp
}
/**************************************************/
// viewnav.js file
/**************************************************/
function drawViewNav() {
 // Do all our calculations to find out where we are in the view
 partialPages = totalDocs / docsPerPage;
 extraPage = (partialPages == Math.floor(partialPages)) ? 0 : 1;
 totalPages = Math.floor(partialPages) + extraPage;
 curPage = Math.floor(startDoc / docsPerPage) + 1
 // Figure out the number of the first and last pages to display on the navigator
 startLink = (curPage < (navRange + 1)) ? 1 : curPage – navRange;
 endLink = ((curPage + navRange) > totalPages) ? totalPages : curPage + navRange;
 // Start writing our menu 
 navHTML = “<div class=’nav’ align=’center’><table class=’navtable’ cellpadding=’0′ cellspacing=’0′><tr>”;
 // Write out the “First”, “Jump”, and “Previous” links when applicable
 if (startLink > 1) {
  navHTML = navHTML + buildLink(1, “First”);
  navHTML = navHTML + buildLink(curPage – (navRange + 1), “<<”);
 } 
 if (curPage > 1) navHTML = navHTML + buildLink(curPage – 1, “<”);
 // Generate all the page # links we want to display
 for (i = startLink; i <= endLink; i++) {
  if (i == curPage) {
   navHTML = navHTML + “<td class=’navtablecur’>” + i + “</td>”
  } else {
   navHTML = navHTML + buildLink(i, i);
  }
 } 
 // Write out the “End”, “Next”, and “Jump” links when applicable
 if (curPage < totalPages) navHTML = navHTML + buildLink(curPage + 1, “>”);
 if (endLink < totalPages) {
  navHTML = navHTML + buildLink(curPage + (navRange + 1), “>>”);
  navHTML = navHTML + buildLink(totalPages, “Last”);
 }
 // Close the list of links
 navHTML = navHTML + “</td>” 
 // Write out the “Page x of y” text and create input box
 //navHTML = navHTML + “<td class=’navpages’>Page “;
 navHTML = navHTML + “<td class=’navtablelink’>Page “;
 // input field for page number is disabled because this does not work for a categorized view
 //navHTML = navHTML + “<input title=\”Click to enter a new page number here\” onKeyUp=\”void(getPage(event, this))\” “;
 //navHTML = navHTML + “onClick=’this.select()’ onFocus=’this.select()’ type=’text’ value=’” + curPage + “‘ />”;
 navHTML = navHTML + curPage ;
 navHTML = navHTML + ” of ” + totalPages + “</td>”;  
 // Close out the menu
 navHTML = navHTML + “</tr></table></div>”;
 // Write out the navigator
 document.getElementById(navDiv1).innerHTML = navHTML;
 if (typeof navDiv2 != “undefined”) document.getElementById(navDiv2).innerHTML = navHTML;
  document.getElementById(‘view’).style.display = ‘block’;
}

function buildLink(pageNum, text) {
 startLinkDoc = (((pageNum – 1) * docsPerPage) + 1);
 endDoc = (pageNum == totalPages) ? totalDocs : startLinkDoc + docsPerPage  // Check for last page when creating tooltip range
 linkHTML = “<td class=’navtablelink’ onmouseover=\”this.className=’navtablelink_on’\” onmouseout=\”this.className=’navtablelink’\” “
 linkHTML = linkHTML + “title=’Page ” + pageNum + ” : Documents ” + startLinkDoc + ” through ” + endDoc + “‘ “;
 linkHTML = linkHTML + “onclick=document.location.href=’” + viewAlias + “?OpenView&RestrictToCategory=“  + Categories + “&Start=” + startLinkDoc + “&Count=” + docsPerPage + “‘>” + text + “</td>”;
 return linkHTML;
}

function getPage(event, field) {
 var keyCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
 if (keyCode == 13) {
  newPage = field.value;
  if (newPage > totalPages || newPage < 1) {
   field.select();
   return false;
  }
   document.location.href = dbPath + viewAlias + “?OpenView&RestrictToCategory=”  + Categories + “&Start=” + (((newPage – 1) * docsPerPage) + 1) + “&count=” + docsPerPage
 }
}
/************************************************************/

The variable Categories is a value I receive from one of the dialoglists on top of the screen. The screen is a frameset with 2 frames.

In the dialoglist I have a JS OnChange event opening the url in the bottom frame:

varURL = “web_internetRegionCat?OpenView&RestrictToCategory=” + varSelect + “&Count=” + varNumPage}

In the bottom frame I open a View via a ‘$$ViewTemplate for viewname’ Form. In the HTML Head section of the Form I calculate the variable Categories, via:

varCategories:=@Right(Query_String_Decoded;”OpenView&RestrictToCategory=”);
varCategories:=@If(@Contains(varCategories;”&Start”);@Left(varCategories;”&Start”);@Left(varCategories;”&Count”));

Comments (12)

Playing with XSLT – Domino view

In a recent project I was asked to find a solution to display the entries in a view in a more advanced navigational way. Currently this functionality is being built by an agent.

The general quest is to limit the amount of using agents and try to use already available views instead. Also the agent has it’s limitations.

There were some conditions this ‘navigator’ must have:

  • documents must be displayed in a familiar way, like a menu-tree structure 
  • a document may have maximum 4 categories depth
  • not each document will have 4 categories depth
  • when clicking on a twistie (plus/minus sign) the tree must be opened
  • when clicking on a category name all the corresponding documents must be presented as thumbnails and the user must be able to navigate through these thumbnails
  • when clicking on a final document entry (the product name) the document must be presented
  • the navigator may only show documents a person is entitled to see, therefor a person may only see documents for his organisation

Nice!

The view

Since I did not had any experience in transforming a view in XML format via a XSL stylesheet I use some old articles available on the IBM website here and here.

Finally my view looked like this:

notes categorized view

Transformation

I first setup my XSL file in Eclipse, just because I just got it installed. But since the view was only allowed to show the documents a person is entitled to somehere in the XSL document capture which OU1 unit a person is listed in.

By placing the XSL on a page and add some good old <computed text> it was easy to create some automated text:

@Name([OU1];@UserName) 

Validating the code I managed by making everytime an export to a flat file, import it in Eclipse again and there do the validation :-(

Finally my code looked like this:

<?xml version=“1.0″ encoding=“UTF-8″?>

<xsl:stylesheet xmlns:xsl=“http://www.w3.org/1999/XSL/Transform” version=“1.0″ xmlns:xalan=“http://xml.apache.org/xslt”>

<xsl:template match=“viewentry[@children]“>

<div>

<xsl:attribute name=“ID”>

<xsl:value-of select=“@position”/>

</xsl:attribute>

 

<span style=“cursor:arrow;” onclick=“toggleSection(this)”>

<img border=“0″ src=“./navigation_plus.gif”/>

</span>

 

<span style=“font-size: 8pt;font-weight:normal;cursor:hand;color:#000000;” onclick=“collectCategory(this)”>

<xsl:attribute name=“NextEntrySrc”><Computed Value>/<Computed Value><Computed Value>&Start=

<xsl:value-of select=“@position”/>.1&Count=

<xsl:value-of select=“@children”/>&Collapse= <xsl:value-of select=“@position”/>1.1

</xsl:attribute>

<xsl:attribute name=“CatPos”>

<xsl:value-of select=“@position”/>

</xsl:attribute>

<span><xsl:value-of select=“entrydata”/></span>

</span>

 

<div style=“margin-left:15px;display:none;”/></div>

</xsl:template>

<xsl:template match=“viewentry”>

 

<table border=“0″ width=“120″>

<tr bgcolor=“#FFFFFF”>

<xsl:if test=“position() mod 2 != 0″>

<xsl:attribute name=“bgcolor”>#FFFFFF</xsl:attribute>

</xsl:if>

<td width=“1%” valign=“top”>

<xsl:variable name=“unid” select=“@unid”/>

 

<span style=“font-weight:normal;color:#000000;cursor:hand;”><a href=<Computed Value>/0/{$unid}?opendocument” target=”main”>

<img border=“0″ src=/<Computed Value>/dot.gif”/></a>

</span>

</td>

<span><a href=<Computed Value>/0/{$unid}?opendocument” target=”main”><xsl:apply-templates select=“entrydata”/></a></span>

</tr>

</table>

 

</xsl:template>

<xsl:template match=“entrydata[@columnnumber=1]“>

<td width=“39%”>

<span style=“font-weight:normal;color:#0060C0;cursor:hand;”><xsl:value-of select=“text”/></span>

</td>

</xsl:template></xsl:stylesheet>What this code does:

  • basically it goes through the view, looks if the entry has got children (which means it’s a category) if so it will display a + sign and the entrydata (category name).
  • the + sign got a toggle() function assigned which is described in the related IBM article
  • the entry data (the category) will get a collectCategory() function assigned which will make an AJAX call which collects all documents under the selected category

The result looks finally with some little help of a CSS file like this:

view in web

Collecting the right documents

Because I wanted to display the exact subset of documents under a certain category structure I add the value ‘this’ in the collectCategory() function:

onclick=“collectCategory(this)”What is i get in return is the object (the viewentry) I click on. Via obj.CatPosI get the position (for example 1.2.2) which also tells me how many levels / categories I am in (3) .In order to get the right subset I needed an categorized view that has a flat construction of the categories at one level, for example:organisation||Cruiser||Shadow||2006||Gray  ororganisation||Touring||Goldwing||2007||BlackFinally I could just show that view to the browser via an ?OpenView command with &RestrictToCategory parameterfunction collectCategory(obj){

varPosSelect = obj.CatPos

varPosSelectSplit = varPosSelect.split(“.”)

var varPosLevels = new Array()

var varPosCat = new Array()

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

varPosLevels[i]=varPosSelectSplit[i]

}

n=0

varCat=“”

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

// for each level we are going to perform a AJAXRequest

switch (i+1) {

// check which position we want to search in te view

case 1: searchPos=eval(varPosLevels[0]); break

case 2: searchPos=eval(varPosLevels[0]) + “.” + eval(varPosLevels[1]); break

case 3: searchPos=eval(varPosLevels[0]) + “.” + eval(varPosLevels[1]) + “.” + eval(varPosLevels[2]); break

case 4: searchPos=eval(varPosLevels[0]) + “.” + eval(varPosLevels[1]) + “.” + eval(varPosLevels[2]) + “.” + eval(varPosLevels[3]); break

default: result = ‘unknown’

}

varURL= ExtDBName + “/” + XMLCategories + UserOrg + “&start=” + searchPos;createAJAXRequest (varURL,“processReturnValue”)

varPosCat[n] = varCat

n++

 

}

varPosCatString = varPosCat.join(“||”)

parent.main.location.href = ExtDBName + “/OrgProdSingleCat?OpenView&RestrictToCategory=” + UserOrg + “||” + varPosCatString + “&Count=12″

}

The parameter &Count=12 indicates that I only want to display 12 documents at a time (to avoid long loading).

In the view the documents are being displayed in <div> tags, and via CSS I display them as a pure CSS picture alike gallery:

/* settings for view display product*/
.thumbnail { float: left;
width: 250px;
height: 400px;
border: 1px solid #E1E1E1;
margin: 0 15px 15px 0;
background-color:#FFF;
}
.clearboth {
clear: both;
}

Wrapping up

Transforming normal Domino views can be very interesting and add user-experience value to your applications.If you want to go deeper into, I believe more experience in XSL is necesarry. I only have seen a top of the iceberg, but in combination with some AJAX the results can be quite amazing!

Comments (6)

My first AJAX ‘type-ahead suggestion’ function

In a recent project I had to include a search field that would perform a full-text search on a catalog (.nsf). In order to provide a better assistance for the users to find the product they are actually looking for I had in mind to build a ‘type-ahead’ function.

If I am searching on the internet I do not find myself stupid so if I am looking for the latest CD of an artist I already know them by name, I just don’t know the name of the record. And I am not really interested in who produced the album or what record-company released the album. All I know is the artist’s name.

I pressumed that my users already know 70% sure what they are looking for.

In this example I will be using motorcycles as products , a topic I adore :)

Maybe it is good how the products are indexed:

search box empty

A product may be indexed under maximum 4 categories, but this is not necessary. For example the Repsol model is delivered in only colorsetting.

After the user has typed in the first three characters a box will appear with the products that have the searchquery somewhere in their articlename:

suggestion box

In this example I search for ‘0RR’ and get all documents with that query in their articlename field.

Easy to implement?

First in the OnKeyPress event of the field I need to invoke the AJAX call:

var charNum = this.value.length

if (charNum>=3){

// 1 visible, 0 hidden

SearchBox(’searchSuggest’,1)

createAJAXRequest (ExtDBName + “/” + SearchTAView + “?readviewentries&RestrictToCategory=” + UserOrg + “&Count=-1&dummy=’+ new Date().getTime();”,“processSearchValue”);}

  • SearchBox: the suggestion box that pops up with documents (as HREF links) matching the query 
  • readviewentries&RestrictToCategory: in the database example there is a restriction who from which organisation (UserOrg) is entitled to see what document…

Basically this request gets me all the documents in return the user is entitled to see.

The SearchBox function

The SearchBox function shows / hides a certain layer that will be placed just below the search field so the user sees the suggestions change along when he types a query in the search field.

function SearchBox(szDivID, iState) {

// 1 = visible, 0 = hidden

var obj = document.layers ? document.layers[szDivID] :document.getElementById ? document.getElementById(szDivID).style :document.all[szDivID].style;obj.visibility = document.layers ? (iState ? “show” : “hide”) : (iState ? “visible” : “hidden”);}

AJAX request

The AJAX is nothing unusual. Often I use the approach described on SitePoint how to make AJAX call.

The final function that generated the list in the ‘SearchBox’ layer looks like this:

function getProductName(xmlViewData){

// ** handle all the rows in the view

var viewRows = xmlViewData.getElementsByTagName (“viewentry”)

var prodList=“<div id=’ProdList’>”

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

var prodName = getViewRowValues(viewRows[i])[0] numbQueryValue =

document.forms[0].Tx_SearchField.value

numbQuery = numbQueryValue.length

var searchstring=prodName.toLowerCase()

var searchfor = document.forms[0].Tx_SearchField.value.toLowerCase()

// indexOf -1 means not found

if (searchstring.lastIndexOf(searchfor)!=-1) {

prodList += “<div style=\”margin-top: 2px;\”>” + getViewRowValues(viewRows[i])[0]+ “</div>”

}

}

prodList +=“</div>”

document.getElementById(“XMLSearchArea”).innerHTML = prodList}

 What is does:

  • it checks if the search query is some where found (searchstring.lastIndexOf(searchfor)!=-1) in the first column in the XML object, this is a complete html code containing the <a href>product name</a> code
  • if so, the product is added to the product list
  • and finally the productlist is placed in the innerHTML part of the <div> with ID: XMLSearchArea.

If the user clicks on one of the product results the corresponding document is presented directly on the right, the SearchBox layer is being hidden and the search query remains in the search field.

Via an onFocus event in the searchfield I can directly re-produce the list for the user.

So that was my first setup. Any suggestions or setups are always welcome :)

Comments (4)

Automated tabbed navigation – Agent search results

When implementing the very nice DomGle solution available on OpenNTF I faced the problem that the results were presented in a very long list. Not really user-friendly. After implementing the ’Ultimate Domino View Navigator‘ by Bob  Obringer which shows a sort of tabbed navigation on top of the view results I wondered if it would be possible to create a mixture, so that the DomGle approach would also contain tabbed navigation.

The DomGle presents the results as one big list. Therefor I needed an approach that would rewrite that list. After going to several searches on Google i found an approach called ‘Semantic tabs‘. 

Semantic tabs

Basically this approach uses a JavaScript libray that rewrites the list you present as tabbed tables when it is being presented to the screen. With a CSS file you define how the tabs will be presented.

After making some modifications in the CSS file my searchresults from the agent looks like this:

tabbed results

What do you need?

  • the JS file tabbed.js
  • the CSS file tabbed.css
  • a LotusScript code that brings the logic to the screen

The first two items you can find under the ‘Semantic tabs’ link. The LotusScript code I set is you find here:

  Set collection = db.search(searchformula,Nothing,0)
 
 Print “Content-Type:text/plain”
 Print “Content-Type:text/html” 
 Print |<html>|
 Print |<head>|
 Print |<title>Search Results</title>|
 Print |<script type=”text/javascript” src=”tabber.js”></script>|
 Print | <link rel=”stylesheet” href=”tabber.css” TYPE=”text/css” MEDIA=”screen”>|
 Print |</head>|
 
 Set doc = collection.GetFirstDocument 
 numberDocs = 50 ‘how many docs to be displayed, make this a variable 
 docCounter = 1 
 endCounter=0
 
 Print |<body>|
 Print | <div class=”tabber”>|  
 For x = 0 To collection.count  
  If x Mod numberDocs = 0  Then
   Print |<div class=”tabbertab”>|
   Print |<h2 style=”text-align:center;”>| & docCounter & |</h2>|
   Print |<div>| & “Created: ” & doc.Created  & ” , Article: ” & doc.Tx_ArtName(0)  & |</div>|   
   docCounter = docCounter + 1
   endCounter= endCounter +1
  Else
   Print |<div>| & “Created: ” & doc.Created  & ” , Article: ” & doc.Tx_ArtName(0)  & |</div>|   
   endCounter = endCounter+ 1
   If endCounter = numberDocs  Then
    Print |</div>|
    endCounter=0
   End If
  End If  
  Set doc = collection.GetNextDocument(doc)
 Next  
 Print |</div>| 
 Print |</body>|
 Print |</html>|

Comments (1)