Angular playground: applying an infinitescroll for Domino Access Services

Intro

Inspired by a serie of blogposts from Marky Roden and popularity in the web dev community I decided to step (once again) from the XPages path (a trend?) and feed my curiosity on AngularJS. An excellent stepping stone is the example database provided by Marky and the AngularJS Fundamentals In 60-ish Minutes presentation by Dan Wahlin.

Domino Access Services

The example database contains examples for CRUD operations via Domino Access Services (way to go!) but is limited in the display of documents from a list/view. The default number of documents returned for view/folder entries is set to 10. Not much of value for a real world application. So either I had to provide some sort of pagination or look for more smartphone/tablet common feature: infinite scroll.

The pagination examples I found only handled a one time loaded data-set and a proper application can contain thousands of records (I find it a common pattern that customers underestimate the number of documents that are being created in a Notes application). Angular can be extended with custom directives and ngInfiniteScroll is such a great example.

So what did I need to do to get this directive applied to the example database?

Implementation

index.html

First I installed the ng-infinite-scroll.min script in my database and updated the header section in index.html

<head lang=”en”>
<meta charset=”UTF-8″>
<title></title>

<link href=”bootstrap/css/bootstrap.min.css” rel=”stylesheet”/>
<link href=”css/custom.css” rel=”stylesheet”/>
<script data-require=”jquery@*” data-semver=”2.0.3″ src=”http://code.jquery.com/jquery-2.0.3.min.js”></script&gt;

<script type=’text/javascript’ src=”angularjs/angular.min.js”></script>
<script type=’text/javascript’ src=”angularjs/angular-route.js”></script>

<script type=’text/javascript’ src=”js/app.js”></script>
<script type=’text/javascript’ src=”js/controller.js”></script>

<script type=’text/javascript’ src=’js/ng-infinite-scroll.min.js’></script>
</head>

I also needed to include the complete jQuery library because Angular has jQuery lite built in which doesn’t have seem to have the features for dynamic height. You should also load the jQuery script before jQlite.

app.js

Next you need to register the infinitescroll directive for my angular application:

var personApp = angular.module(‘personApp’, [
‘ngRoute’,
‘peopleControllers’,
‘infinite-scroll’
]);

factory

The examples for ngInfiniteScroll demonstrate a factory which is not used in the example database. From my limited knowledge on Angular I have understood that a factory is an injectable function.

personApp.factory(‘DAS’, function($http) {

var DAS = function() {
this.items = [];
this.busy = false;
this.after = 0;
this.count = 30;
this.order = ‘firstname';

};

DAS.prototype.nextPage = function() {
if (this.busy) return;
this.busy = true;
var url = ‘//dev1/apps/others/angular/ainx.nsf/api/data/collections/name/byFirstName5Col?open’ + ‘&count=’ + this.count + ‘&start=’ + this.after;

$http.get(url).success(function(data) {
var items = data;
for (var i = 0; i < items.length; i++) {
this.items.push(items[i]);
}
this.after = this.after + this.count;
this.busy = false;
}.bind(this));
};
return DAS;
});

My factory is called DAS (I guess I break here the naming convention). You can store your factories in a separate (new) file e.g. main.js.

Controller.js

I removed the existing PeopleListCtrl controller and replaced it with the following one:

personApp.controller(‘PeopleListCtrl’, function($scope, DAS) {
$scope.DAS = new DAS();
});

partial-list.html

With everything in place I now needed to update the display of the list, which is defined in a partial. Besides using the factory I also wanted to add some additional features such as search and sorting. This turned out to be really simple.

At the end I wanted to have something as followed:

Screenshot_3

 

Search & Sorting

For a search feature I added an input control and used the directive ngModel called query to apply a filtering via a search query

<div class=”row” >
<div class=”col-md-1″><label>Search:</label></div>
<div class=”col-md-2″>
<form class=”form-search”>
<input ng-model=”query” placeholder=”Search for person” autofocus class=”input-medium search-query”/>
</form>
</div>…

For a sorting feature I added a select control and bound that to the order property for the data via a custom directive. Further I added a radio-button group and used the directive ngModel called direction:

…<div class=”col-md-1″><label>Sorting:</label></div>
<div class=”col-md-1″>
<select ng-model=”DAS.order“>
<option value=”firstname”>First Name</option>
<option value=”lastname”>Last Name</option>
<option value=”zip”>ZIP</option>
</select>
</div>
<div class=”col-md-3″>
<label class=”formgroup”>
<input type=”radio” ng-model=”direction” name=”direction” checked> ascending
</label>
<label class=”formgroup”>
<input type=”radio” ng-model=”direction” name=”direction” value=”reverse”> descending
</label>
</div>
</div>

Note that the default sorting is set to ‘firstname’ in the factory.

Apply infinitescroll to the data-table

Finally we need to apply the nextPage function in the DAS function by wrapping the data-table with a div and add the infinite-scroll attribute to it.

<div infinite-scroll=”DAS.nextPage()” infinite-scroll-distance=”3″>
<table class=”table table-striped”>
<thead>
<tr>
<th>Position</th><th>First Name</th><th>Last Name</th><th>Zip</th><th></th><th></th>
</tr>
</thead>
<tbody>…

Then we use the ng-repeat directive and for each item or person in the data we display a new row. Here is also were we apply the filter and sorting options:

…<tr ng-repeat=”person in DAS.items | filter:query | orderBy:DAS.order:direction”>
<td>{{person[“@position”]}}</td>
<td>{{person.firstname}}</td>
<td>{{person.lastname}}</td>
<td>{{person.zip}}</td>
<td><a class=”btn btn-info” href=”#/person/{{person[‘@unid’]}}”>Edit</a></td>
<td><a class=”btn btn-warning” href=”#/person/{{person[‘@unid’]}}/delete”>Delete</a></td>
</tr>

</tbody>
</table>
<div ng-show=’DAS.busy’>Loading data…</div>
</div>

Wrap up

That’s it! Every time you hit (almost) the bottom of the list 30 new rows will be added. The search only applies to the loaded data, not all the data that is the view but not loaded/displayed yet. That requires a different set up.

Some thoughts

Angular is an exciting new world for Domino and XPages developers. It offers lot out-of-the-box with directives and so on but XPages also does that (e.g. security, data-binding). In combination with Domino Access Services you can create real world CRUD applications like Marky’s example demonstrates.

I am very curious about your findings and code examples mixing Domino and Angular!

Visitors per day statistics with zooming and weekends (prototype)

I created a visitor statistics prototype for an application. With the statistics we would like to check if an application is used frequently or if it’s sensitive to corporate news updates.

The application help the user to select what type of meeting they should setup according to several parameters the user provide (e.g.  number of participants, duration, share & edit, see others, participants outside firewall, type of communication (top down, horizontal), type of meeting subject).

The application also is intended to promote the usage of electronic solutions instead of “expensive” “on-site” visits.

Conditions

  • In the first step we are only interested in the number of visits. In the next phase we are also interested what parameters the user provide when searching.
  • Only unique sessions are recorded.
  • Users are expected to be anonymous.
  • An export should be possible for spreadsheet addicts (not mentioning a specific Office product).

Architecture

The solution exists of the following (design) elements:

  • A custom control to record the visit.
    • Each unique visit is a Notes document storing date, browser, user, dateunix, page name.
      • We record also the browser type using the CGIVariables() function by Thomas Gumz.
  • A custom control to display the statistics. We have chosen to use a plot a time serie using Flot as a starter.
    • A Notes view categorized by a unix timestamp to provide data for the chart.
  • An XPage for the providing a spreadsheet output.
    • A Notes view that is used by the XPage for providing data for the output.

Implementation

  • Download and install the Flot resources via Package Explorer.
  • Create a custom control e.g. ccVisitRegister:

<xp:this.resources>
<xp:script src=”/xpCGIVariables.jss” clientSide=”false”></xp:script>
<xp:script src=”/xpFunctions.jss” clientSide=”false”></xp:script>
</xp:this.resources>
<xp:this.beforePageLoad><![CDATA[#{javascript:var v = sessionScope.get(“visit”);
var dt:NotesDateTime = session.createDateTime(“Today”);
dt.setNow();
function createDoc(){
var doc = database.createDocument(); // Create the document
doc.appendItemValue(“Form”,”count”); // Assign the form
doc.appendItemValue(“date”,dt); // Append something to a field

var udt:NotesDateTime = session.createDateTime(“Today”);

var unixDate = udt.toJavaDate().getTime()/1000;

doc.appendItemValue(“dateunix”,unixDate); // Append something to a field

var cgi = new CGIVariables()
var browser = cgi.HTTP_USER_AGENT
doc.appendItemValue(“browser”,browser); // Append something to a field
var user = @UserName();
doc.appendItemValue(“user”,user); // Append something to a field

var page = facesContext.getExternalContext().getRequest().getRequestURI();
doc.appendItemValue(“pagename”,@RightBack(page,”/”)); // Append something to a field

doc.save();
}
if (v == null){
//New session -> register
sessionScope.put(“visit”,dt);
sessionScope.put(“create”,”yes”);
if (isAuthor()==true){
createDoc();
}
}
else{
sessionScope.put(“visit”,dt);
sessionScope.put(“create”,”no”);
}}]]></xp:this.beforePageLoad>

xpFunctions is a library with helper functions e.g. the isAuthor() function:

function isAuthor(){
var level = database.getCurrentAccessLevel();
if (level >= NotesACL.LEVEL_AUTHOR) {
return true;
} else if (level < NotesACL.LEVEL_AUTHOR) {
return false;
}
}

  • Create a custom control to display the statistics:

<?xml version=”1.0″ encoding=”UTF-8″?>
<xp:view xmlns:xp=”http://www.ibm.com/xsp/core”&gt;
<link href=”flot/examples/examples.css” rel=”stylesheet” type=”text/css”></link>
<!–[if lte IE 8]><script language=”javascript” type=”text/javascript” src=”flot/excanvas.min.js”></script><![endif]–>
<script language=”javascript” type=”text/javascript” src=”flot/jquery.js”></script>
<script language=”javascript” type=”text/javascript” src=”flot/jquery.flot.js”></script>
<script language=”javascript” type=”text/javascript” src=”flot/jquery.flot.time.js”></script>
<script language=”javascript” type=”text/javascript” src=”flot/jquery.flot.selection.js”></script>
<xp:text escape=”false” id=”scriptBlock”>
<xp:this.value><![CDATA[#{javascript:
var v:NotesView = database.getView(“countsbyunixdate”);
v.setAutoUpdate(false);
var nav:NotesViewNavigator = v.createViewNav();
var entry:NotesViewEntry = nav.getFirst();
var nEntry:NotesViewEntry = null;
var newArr = new Array();
while(entry != null){
nEntry = nav.getNextCategory();
if(entry.isCategory()){
var values = entry.getColumnValues();
var children = entry.getChildCount();
newArr.push (“[” + values.get(0) + “,” + children + “]”);
}
try{
entry.recycle();
}catch(e){
}
entry = nEntry;
}
try{
nav.recycle();
v.recycle();
}catch(e){
}
var newArrString:String = newArr.join(“,”);

“<script type=\”text/javascript\”>”+
“$(function() {” +
“var d = [” + newArrString + “];” +
“for (var i = 0; i < d.length; ++i) {d[i][0] += 60 * 60 * 1000;}” +
“function weekendAreas(axes) {“+
“var markings = [],”+
“d = new Date(axes.xaxis.min);”+

“d.setUTCDate(d.getUTCDate() – ((d.getUTCDay() + 1) % 7));”+
“d.setUTCSeconds(0);”+
“d.setUTCMinutes(0);”+
“d.setUTCHours(0);”+
“var i = d.getTime();”+
“do {“+
“markings.push({ xaxis: { from: i, to: i + 2 * 24 * 60 * 60 * 1000} });”+
“i += 7 * 24 * 60 * 60 * 1000;”+
“} while (i < axes.xaxis.max);”+
“return markings;”+
“}”+
“var options = {“+
“xaxis: {“+
“mode: \”time\”,”+
“tickLength: 5″+
“},”+
“selection: {“+
“mode: \”x\””+
“},”+
“grid: {“+
“markings: weekendAreas”+
“}”+
“};”+

“var plot = $.plot(\”#placeholder\”, [d], options);”+
“var overview = $.plot(\”#overview\”, [d], {“+
“series: {“+
“lines: {“+
“show: true,”+
“lineWidth: 1″+
“},”+
“shadowSize: 0″+
“},”+
“xaxis: {“+
“ticks: [],”+
“mode: \”time\””+
“},”+
“yaxis: {“+
“ticks: [],”+
“min: 0,”+
“autoscaleMargin: 0.1″+
“},”+
“selection: {“+
“mode: \”x\””+
“}”+
“});”+
“$(\”#placeholder\”).bind(\”plotselected\”, function (event, ranges) {“+
“plot = $.plot(\”#placeholder\”, [d], $.extend(true, {}, options, {“+
“xaxis: {“+
“min: ranges.xaxis.from,”+
“max: ranges.xaxis.to”+
“}”+
“}));”+
“overview.setSelection(ranges, true);”+
“});”+
“$(\”#overview\”).bind(\”plotselected\”, function (event, ranges) {“+
“plot.setSelection(ranges);”+
“});”+
“$(\”#footer\”).prepend(\”Flot \” + $.plot.version + \” &ndash; \”);”+
“});”+

“</script>”

}]]>
</xp:this.value>
</xp:text>
<xp:panel tagName=”h1″>Visitors</xp:panel>
<xp:panel id=”content” tagName=”div”>
<div class=”demo-container”>
<div id=”placeholder” class=”demo-placeholder”></div>
</div>

<div class=”demo-container” style=”height:150px;”>
<div id=”overview” class=”demo-placeholder”></div>
</div>
</xp:panel></xp:view>

  • Create a Notes view categorized by a unix timestamp:

notesview

Result

plot

These are the main items on the statistics page (beside a link to the export function). You can zoom in/out on the time line.

Feedback

The idea is simple, also the implementation. I will create a small project on OpenNTF later this week. In case you have some valuable additions/suggestions please let me know.

Job Wanted

Looking for a creative brain? Choose me!

job-wanted

CALENDARIO: A responsive calendar plugin

Calendars can be a very tricky thing when it comes to their layout and responsiveness. Calendrio is a jQuery plugin to display a calendar layout for both, small and big screens and keeping the calendar structure fluid when possible.

Here follows a brief description how you can utilize the plugin in your IBM Notes – XPages application.

Resources

Copy the resources to the WebContent folder via the Package Explorer:

webfolder

Calendar display

Next I created an XPage to display the calendar:

<?xml version=”1.0″ encoding=”UTF-8″?>
<xp:view xmlns:xp=”http://www.ibm.com/xsp/core&#8221;
pageTitle=”Responsive Calendar with calendario.js”
createForm=”false”>
<!–[if IE 9]><html class=”no-js ie9″><![endif]–>
<!–[if gt IE 9]><!–>
<xp:this.resources>
<xp:metaData name=”charset” content=”utf-8″></xp:metaData>
<xp:metaData name=”http-equiv” content=”IE=edge,chrome=1″></xp:metaData>
<xp:metaData name=”viewport” content=”width=device-width, initial-scale=1.0″></xp:metaData>
<xp:script src=”scripts/modernizr.custom.63321.js” clientSide=”true”></xp:script>
<xp:script src=”http://code.jquery.com/jquery-1.9.1.min.js&#8221; clientSide=”true”></xp:script>
<xp:script src=”http://code.jquery.com/jquery-migrate-1.1.1.min.js&#8221; clientSide=”true”></xp:script>
<xp:script src=”scripts/jquery.calendario.js” clientSide=”true”></xp:script>
<xp:script src=”data.xsp” clientSide=”true”></xp:script>
<xp:linkResource href=”styles/screen.css” rel=”stylesheet” media=”all” target=”_self” charset=”UTF-8″></xp:linkResource>
</xp:this.resources>
<html class=”no-js”><!–<![endif]–>
<script type=”text/javascript”>
$(document).ready(function(){ var cal = $( ‘#calendar’).calendario( { onDayClick : function( $el, $contentEl,
dateProperties ) {
for( var key in dateProperties ) { console.log( key + ‘ = ‘ + dateProperties[ key ] ); }}, caldata : webdesigner } ), $month = $( ‘#custom-month’).html( cal.getMonthName() ), $year = $( ‘#custom-year’).html( cal.getYear() );
$( ‘#custom-next’ ).on( ‘click’, function() {
cal.gotoNextMonth( updateMonthYear ); } ); $( ‘#custom-prev’).on( ‘click’, function() { cal.gotoPreviousMonth(updateMonthYear ); } ); $( ‘#custom-current’ ).on( ‘click’,function() { cal.gotoNow( updateMonthYear ); } );
function updateMonthYear() { $month.html( cal.getMonthName()); $year.html( cal.getYear() ); }
});
</script>
<body>
<div class=”container”>
<div class=”custom-calendar-wrap custom-calendar-full”>
<div class=”custom-header clearfix”>
<h2>Responsive Calendar</h2>
<h3 class=”custom-month-year”>
<span id=”custom-month” class=”custom-month”></span>
<span id=”custom-year” class=”custom-year”></span>
<nav>
<span id=”custom-prev” class=”custom-prev”></span>
<span id=”custom-next” class=”custom-next”></span>
<span id=”custom-current” class=”custom-current” title=”Got to current date”></span>
</nav>
</h3>
</div>
<div id=”calendar” class=”fc-calendar-container”></div>
</div>
</div><!– /container –>
</body>
</html>
</xp:view>

Data

To create content for the data I created a Notes form with 2 fields: date and title. Also create a Notes view that is sorted by the date field.

To collect the data for the calendar.xsp create another XPage that will behave like an “XAgent”:

<?xml version=”1.0″ encoding=”UTF-8″?>
<xp:view xmlns:xp=”http://www.ibm.com/xsp/core&#8221; rendered=”false” viewState=”false”>
<xp:this.afterRenderResponse><![CDATA[#{javascript:
var externalContext = facesContext.getExternalContext();
var response = externalContext.getResponse();
var writer = response.getWriter();

response.setContentType(“application/javascript”);
response.setHeader(“Cache-Control”, “no-cache”);
var v:NotesView = database.getView(“entries”);
v.AutoUpdate = false;
var viewData = “var webdesigner = {“;
if (v.getEntryCount() != 0){
var vDoc:NotesDocument = v.getFirstDocument();
while (vDoc != null){
var nextvDoc:NotesDocument = v.getNextDocument(vDoc);

//viewDataDT:NotesDateTime = session.createDateTime(vDoc.getItemValueDateTimeArray(“date”));
//viewDataDT = vDoc.getItemValueDateTimeArray(“date”);
//viewData = viewData + viewDataDT.getDateOnly();
var dt:NotesDateTime = vDoc.getItemValueDateTimeArray(“date”).elementAt(0);
var day = @Day(dt.getDateOnly());
var str = “” + day;
var pad = “00”
var day = pad.substring(0, pad.length – str.length) + str;

var month = @Month(dt.getDateOnly());
var str = “” + month;
var pad = “00”
var month = pad.substring(0, pad.length – str.length) + str;
var year = @Year(dt.getDateOnly());
var cDate = month + “-” + day + “-” + year;

viewData = viewData + “‘” + cDate + “‘ : “;
var title = vDoc.getItemValueString(“title”);

viewData = viewData + “‘<a href=\”” + “entry.xsp?documentId=”+ vDoc.getUniversalID() + “\”>” + title + “</a>’,”;
vDoc.recycle();
var vDoc:NotesDocument = nextvDoc;
}
viewData = @LeftBack(viewData,1);
}
viewData = viewData + “};”;
writer.write( viewData);
writer.endDocument();
facesContext.responseComplete();
}]]></xp:this.afterRenderResponse>

</xp:view>

Result

Open the calendar.xsp. In small screen it should look like:

small

For larger screens (tablets/desktop) the calendar should look like:

large

Not bad ain’t it? I am not sure how responsive iNotes 9 is, my 8.5.3 version could use some.

Demo download

I have uploaded a sample database which can be found here.

Also look at the following site to read the options for this interesting jQuery plugin:

http://tympanus.net/codrops/2012/11/27/calendario-a-flexible-calendar-plugin/

Job Wanted

Looking for a creative brain? Try me!

job-wanted

New control submitted for the XPages development contest

I have submitted a new control to OpenNTF for the XPages development contest. The control is an abstract of the implementation of Galleria in the Bildr project.

In short words: the control enables to publish an image slider on your Xpage. A sample is demonstrated in the following image:

 

You can download the control here. Have fun!

A Slidr for Bildr

For the next release of my Bildr app I would like to redo the startpage. Untill now I have not really focussed on it.

The current startscreen is using the Dojo Fisheye plugin but this requires user action before you start noticing it. Also it does not work well with the OneUI (baseline=bottom is I think is used for the content).

In a previous version I included a slider with some nice animation. For the upcoming version I am thinking about using Fancy Transitions, a jQuery based plugin. On this page you can see 2 nice examples of transitions.

So what do you think? Will it make the startpage too busy, or is it just a great opportunity to leave the page open and watch photo’s passing by?

Below you can see a sample screen:

Breadcrumbs using JQuery

Bob Balfe asked some time ago who is using JQuery for their Domino development projects. I hoped it was gonna rain with examples of implementation of JQuery plugins in Lotus Notes but so far it remains a bit quiet. So why not describe an example myself?

Figure: Screenshot from the website.

xBreadcrumbs is a nice plugin to JQuery to provide horizontal navigation. It uses an unordered list (UL) as source so when you provide that data dynamically with Notes data it becomes interesting.

In my case the customer wanted to have breadcrumbs on top of documents that are stored in a Notes view. Documents are categorized in a parent-response hierarchy so for each document I have to calculate the complete path from the opened document to its uber-parent (some points on that u) aka as ‘Home’.

Figure: Document location in Notes view.

Figure: Document location in breadcrumb.

Implementation

Download the zip file, extract it and upload the files in your Notes application.

Create a subform that you will embed on the form of your document. Add the following code to the subform:

<script type=”text/javascript” src=”../jquery-1.3.2.min.js”></script>
<script type=”text/javascript” src=”../breadcrumbs/js/xbreadcrumbs.js”></script>
<link rel=”stylesheet” href=”../breadcrumbs/css/xbreadcrumbs.css” />

<script type=”text/javascript”>
$(document).ready(function(){
$(‘#breadcrumbs-2′).xBreadcrumbs({ collapsible: true });
});
</script>

<style type=”text/css”>
.xbreadcrumbs {
background:#FFF none repeat scroll 0 0;
}
.xbreadcrumbs LI {
border-right: none;
background: url(../img/frmwrk/breadcrumb/separator.gif) no-repeat right center;
padding-right: 15px;
}
.xbreadcrumbs LI.current { background: none; }
.xbreadcrumbs LI UL LI { background: none; }
.xbreadcrumbs LI A.home {
background: url(../img/frmwrk/breadcrumb/home.gif) no-repeat left center;
padding-left: 20px;
}
/*  Custom styles for breadcrums (#breadcrumbs-3)  */
.xbreadcrumbs#breadcrumbs-3 {
background: none;
}
.xbreadcrumbs#breadcrumbs-3 LI A {
text-decoration: underline;
color: #0A8ECC;
}
.xbreadcrumbs#breadcrumbs-3 LI A:HOVER, .xbreadcrumbs#breadcrumbs-3 LI.hover A { text-decoration: none; }
.xbreadcrumbs#breadcrumbs-3 LI.current A {
color: #333333;
text-decoration: none;
}
.xbreadcrumbs#breadcrumbs-3 LI {
border-right: none;
background: url(../img/frmwrk/breadcrumb/separator.gif) no-repeat right center;
padding-right: 15px;
padding-left:0px;
}
.xbreadcrumbs#breadcrumbs-3 LI.current { background: none; }
.xbreadcrumbs#breadcrumbs-3 LI UL LI { background: none; padding: 0;  }
</style>

<script src=”../$a-get-bread-crumb?OpenAgent&breadcrumb=<Computed Value>&ul&view=$v-treeview&id=myCrumb&class=xbreadcrumbs&activecrumbclass=current”></script>

The source for my unordered list is the agent $a-get-bread-crumb.

The formula for the computed value is as followed:

@Text(@DocumentUniqueID)

The agent will use the Notes view ‘$v-treeview’, find the document by its document UNID and navigates via each parent all the way up in the Notes view. While doing so the required information to build the unordered list is collected.

By the way, the code for this agent was co-written by my collegue Tomas Ekström.

Reflection

In theory the breadcrumbs were displayed without any problem on the fly. However when moving into production with a database with more than 10.000 documents performance became a problem. It took about 3 seconds to collect the information and thereafter the complete document would be displayed.

Since the customer did not find such a load time acceptable BUT could no longer live without the breadcrumbs any longer I decided to place the subform instead of on top of the form to the bottom of the form and place it via CSS back on top of the document via absolute positioning.

The result is that the content of the document is displayed instantly and the breadcrumbs will follow a few seconds later. Not the nicest solution if you would ask me.

But for small websites (tested with 400 documents) the function works like a charm.

To end this post I will include the code for the agent + some script library.

Code

ULTreeFromView script

class CGI script

$a-get-bread-crumb