Create breadcrumbs (Notes)

The following code lets you store breadcrumbs on a document, based upon the parent-child relation.

Function GetDocumentCategory(doc As notesdocument) As String
Dim session As New notessession
Dim db As notesdatabase
Dim node As notesdocument
Dim Category As String
Set node = doc

Set db = session.currentdatabase

Do While node.isResponse
If node.isvalid Then
Set node = db.getdocumentbyunid(node.parentdocumentunid)
If Category = “” Then
Category = node.Title(0)
Category = node.Title(0) & “\” & Category
End If
End If

GetDocumentCategory = Category
End Function

Call the function somewhere, for example the querysave event on a form:

Sub Querysave(Source As Notesuidocument, Continue As Variant)
Dim doc As notesdocument
Set doc = source.document
Call doc.replaceitemvalue(“Path”, GetDocumentCategory(doc))
End Sub

Bildr – A photo sharing project

Bildr is a prototype application that never received enough funding to become a full blown (internal) Notes template. The idea is to offer a place where teams can share high resolution images.

Bild is  the Swedish word for photo and Bildr refers ofcourse to Flickr.

Another reason why I stopped prototyping the database is that I started looking at XPages and the limitations I experience with pre-XPages development. To shorten it: it takes just too many design elements to build it in the old-fashioned way to be able to offer a Web 2.5 experience. Maybe after a long break I will move the idea behind the application into an XPages based solution.

For now you can download the application and use it as a start for your development project.

What’s in it?

1. Easy import and thumbnailing of images (Notes client)

Open any of the Picture views and select the New Upload button.


The idea is that you can upload multiple images at once in a new or an existing category. You can add tags and a description to the to be created document(s). New uploads you may want to appear in the ImageFlow gallery in the startpage and you can enable commenting and read restrictions.


When you select the Upload Images action button you can select the pictures you want to upload. The code behind this function is taken from the NSFTools site (I guess). After uploading you can select to include more images or just leave it as it is.

Behind the scenes each picture is included in a Notes document with also a mid-size image and a thumbnail:


For the Notes client that is just it. Nothing more exciting here except some gaps in the completeness of the functionality (hey, I was talking about a prototype).

2. Presentation (Browser)

For the way of presenting different elements I guess I was so bold to use Jake Howlet’s DomBlog Template (for search, calendar, commenting, RSS etc.) which was for a long time a learning application for many Domino developer. I guess if you compare the layout with that of LinkedIn you will see some comparison.


3. Tagcloud (Browser)

For the Tagcloud I got some help.


4. ImageFlow Gallery


5. Lightbox

I guess Lightbox alike functions are almost unthinkable when looking at high resolution images so this I also included when clicking on a mid-size or thumbnail image:


What is it not?

You can not call this app a production ready application. Some things are not in place (like cleaning up categories and corresponding image documents). However it can be a nice starting point for a mature application. Untill now I experienced little or none errors so please give it a try and send me the end result of you modification.

Steal with pride and improve! (11 MB)

Pagination on a View in the Notes client

A problem with Notes Views is that when they contain a lot of documents scrolling becomes inevitable. Even when you apply categorized columns it will become necessary that users click through the Views. An option would be that you just start typing and hopefully Notes will lead you to the nearest corresponding document.

When you apply categorized views the categories are displayed vertically below each other so for travelling users with smaller screens it will become tasly to open and close the categories just to see thse categories and corresponding documents displayed in a properly in the overview.

Maybe XPages in the Notes client will bring us some relief, but we are not there yet.

So how do we cope with the issue for the time being?

On the web it is common practice to have some sort of pagination on top / below lists of documents so you can navigate through the list without the need to scroll up and down or move you mouse across the screen. The pagination can be based on number of available pages or just available short cuts (example: the first letter of an available lastname). I myself have written some articles about pagination on this blog which you may check out yourself.

So this is all about the web, I adress this article to pagination in the Notes client.

The simplest way I found is to create a list of similar shortcuts, containing javascript, and render pass thru HTML in Notes. In this example I explain how I added pagination to a list (a Notes View) of persons. The thought is to show a list of persons form which their lastname start with a certain letter. What I needed to accomplish this:

  • A Form with an Embedded View. This View should be categorized by the first letter of the lastname @LowerCase(@Trim(Tx_LastName);1). SaveOptions are set to zero ofcourse.
  • The Embedded View has the ‘Show single category option’ in use.  As formula write the name of the Field that will acts as a temporary container (Tmp_LastName).
  • Add a Field named TmpLastName on top of the Embedded View. Make it editable. As default value give it the value “a” (you could check if this value is really available or not).
  • On top of the Form add the following JavaScript and CSS code:

<script language=”Javascript”>
var entryNumber = “”;
function showTab(lastName) {
entryNumber = lastName;
document.forms[0].LastName.value = entryNumber;
entryNumber = “”;

<style type=”text/css”>
font {
font-family: “Default Sans Serif”, sans-serif;
font-style: normal;
font-weight: normal;
font-size: 8pt;
color: black;
text-decoration: none;
text-align: left;
text-indent: 1ex;
A:link {color: blue; text-decoration: none}
A:visited {color: blue; text-decoration: none}
A:active {color: blue; text-decoration: none}
A:hover {color: blue; text-decoration: none}

  • As final step you need to add a computed text just above the Embedded View. Mark the text as passthru HTML. A Value enter:

varList:=@DbColumn( “”: “NoCache” ; “” : “” ; “PersViewCategorized” ; 1 );
tmpList:=@Implode(“<a href=\”javascript:showTab(‘” + varList + “‘)\”>” + varList+ “</a>” + ” | ” );

Here we make a call to first column in the same Embedded View. Around each found category we wrap a JavaScript call which will put the value in the temporary field and finally we call the button to update the UI.

Here is a screendump that shows how it could look like:


A problem discovered

Well everything  looked shiny untill I tried to load the Form in a Frameset. When pressing on one of the links I got the message:


Some work to do IBM….

Mimicing the preview-switch option

I thought I give this Thursday a ‘SnTT’ swing (what happened with that phenomena?).

My collegue Tomas can be sometime tough with progressive ideas, but this makes you just try that extra effort to make something in Notes a little bit better than expected.

When I asked him for ideas for a standard development template he liked the ideas to have multiple collapsible preview panes BUT he wanted the option for the user to switch in preview in bottom or preview on side, just like in the Notes 8 mail template.


Since we know that the mail is a composite application with it specific functionality a different approach to provide something similar in the ‘classic’ client.


The following image shows the frameset for my application:


So in frame ‘main’ I have a frameset. By default the frameset is the one that contains a preview on the bottom. This frameset is called ‘$fs-notes-documents-vr’ (vr for vertical). I have a second frameset that is called ‘$fs-notes-documents-hr’ (hr vor horizontal) that will be displayed in frame ‘main’ when the user chooses for a preview on side.

In my horizontal menu (alternative menu) I have added 2 action buttons:

  • Preview on Bottom.

@Command( [OpenFrameset] ; “$fs-notes-documents-vr” );

  • Preview on Side.

@Command( [OpenFrameset] ; “$fs-notes-documents-hr” );

These actions will place the appropiate frameset in frame ‘main’ and set an environment variable. This variable is being read when the ‘default’ frameset (the frameset that contains frame ‘main’) is being used when opening the application.

For frame ‘main’ I have as computed value:

varFrSetDefault:=@Environment( “AppCode-AppPreview” );
@If ( varFrmSetDefault = “” ; “$fs-notes-documents-vr” ; varFrmSetDefault)

So if the user has used the application before and has set a preview preference the next time the user comes in the application the ‘prefered’ frameset / preview option is being used!

Too bad there is no option to read which design element is currently used in a frame (like in a @GetTargetFrame). In that way I would have been able to re-open the currently opened view / folder (okej I could use another environment variable for that).

Top 10 custom JavaScript/LotusScript functions of all time

Just a quickie:

Top 10 lists are always nice (and handy) so I read with great interest Dustin Diaz’s posting on his Top 10 of custom JavaScript functions of all time.

After reading I googled after the Top 10 custom LotusScript functions of all time. I guess there is room for such a posting (hint).

Since I mostly do more Web-development than Notes-development the number of custom LS functions for me is limited, but nevertheless I use them in agents.

So here you find 10 custom functions than I (have) use(d) on a regular base (not ranked in importance). Please feel free to add your supertrooper LS functions:


Function DBColumn (strClass As String, strNoCache As String, strServer As String, strDatabase As String, strView As String, strKey As String) As Variant
 Dim strFormula As String,quotes As String
 quotes = Chr(34)
 strFormula = “@DbColumn(” & quotes & strClass & quotes & “:” & quotes & strNoCache & quotes & “;” & quotes & strServer & quotes & “:” & quotes & strDatabase & quotes & “;” & quotes & strView & quotes & “;” & strKey & “)”
 DbColumn = Evaluate( strFormula )
End Function


Function DBLookup (strClass As String, strNoCache As String, strServer As String, strDatabase As String, strView As String, strKey As String, strReturn As String) As Variant
 Dim strFormula As String,quotes As String
 quotes = Chr(34)
 strFormula = “@DbLookup(” & quotes & strClass & quotes & “:” & quotes & strNoCache & quotes & “;” & quotes & strServer & quotes & “:” & quotes & strDatabase & quotes & “;” & quotes & strView & quotes & “;” & quotes & strKey & quotes & “;” & strReturn & “)”
 DbLookup = Evaluate( strFormula )
End Function


Function LeftStr(OrigStr, LeftOf ) As String
 Dim Pos As Integer
 Dim OrigStrLen As Integer
 Pos = Instr( Lcase(OrigStr), Lcase(LeftOf) )
 OrigStrLen = Len(OrigStr)
 If pos>0 Then
  LeftStr = Left( OrigStr, (Pos-1))
  LeftStr = OrigStr
 End If
End Function


Function RightStr(OrigStr, RightOf ) As String
 Dim Pos As Integer
 Dim OrigStrLen As Integer
 Dim RightOfLen As Integer
 Pos = Instr( Lcase(OrigStr), Lcase(RightOf) )
 OrigStrLen = Len(OrigStr)
 RightOfLen = Len(RightOf)
 If Pos>0 Then
  RightStr = Right( OrigStr, OrigStrLen -(RightOfLen+Pos-1))
  RightStr = OrigStr
 End If
End Function


Function ReplaceSubString(SourceS As String, SearchS As String, ReplaceS As String) As String
 While Instr(SourceS, SearchS) > 0
  SourceS = Left$(SourceS, Instr(SourceS, SearchS) – 1) + ReplaceS + Right$(SourceS, Len(SourceS) – Instr(SourceS, SearchS) – Len(SearchS) + 1)
 ReplaceSubstring = SourceS
End Function


Function Unique(vIn As Variant) As Variant
 Dim lsTemp List As String     ‘Create the list
 Dim astemp() As String        ‘A place to store the compacted array
 Dim iCount As Integer         ‘Count how many uniques we find
  ‘Make sure they sent us an array of strings
 If Not Isarray(vIn) Then
  Msgbox “Unique requires an array as input”
  UniqueA = vIn
  Exit Function
 Elseif Typename( vIn(0) ) <> “STRING” Then
  Msgbox “Unique requires an array of strings as input. vIn(0) is a ” _
  + Typename( vIn(0) )
  UniqueA = vIn
  Exit Function
 End If
 Forall s In vIn
     ‘If the entry isn’t in the list…
  If Not Iselement( lsTemp(s) ) Then
       ‘Add it to the list
   lsTemp(s) = “”
   iCount = iCount + 1
  End If
 End Forall
 ‘Note that there’s no “preserve” keyword here so this is relatively quick
 Redim asTemp(iCount-1)
 iCount = 0
  ‘Copy all the unique elements into the temp array
 Forall v In lsTemp
  asTemp(iCount) = Listtag(v)
  iCount = iCount + 1
 End Forall
  ‘Return the temp array
 UniqueA = asTemp
End Function


Function SortCollection(coll As NotesDocumentCollection, fieldnames() As String) As NotesDocumentCollection
‘ ————————————————
‘ — You may use and/or change this code freely
‘ — provided you keep this message
‘ —
‘ — Description:
‘ — Sorts and returns a NotesDocumentCollection
‘ — Fieldnames parameter is an array of strings
‘ — with the field names to be sorted on
‘ —
‘ — By Max Flodén 2005 –
‘ ————————————————
 Dim session As New NotesSession
 Dim db As NotesDatabase
 Dim collSorted As NotesDocumentCollection
 Dim doc As NotesDocument
 Dim i As Integer, n As Integer
 Dim arrFieldValueLength() As Long
 Dim arrSort, strSort As String
 Dim viewname As String, fakesearchstring As String
 viewname = “$All” ‘This could be any existing view in database with first column sorted
 fakesearchstring = “zzzzzzz” ‘This search string must NOT match anything in view
 Set db = session.CurrentDatabase
‘ —
‘ — 1) Build array to be sorted
‘ —
‘Fill array with fieldvalues and docid and get max field length
 Redim arrSort(0 To coll.Count -1, 0 To Ubound(fieldnames) + 1)
 Redim arrFieldValueLength(0 To Ubound(fieldnames) + 1)
 For i = 0 To coll.Count – 1
  Set doc = coll.GetNthDocument(i + 1)
  For n = 0 To Ubound(fieldnames) + 1
   If n = Ubound(fieldnames) + 1 Then
    arrSort(i,n) = doc.UniversalID
    arrFieldValueLength(n) = 32
    arrSort(i,n) = “” & doc.GetItemValue(fieldnames(n))(0)
‘ Check length of field value
    If Len(arrSort(i,n)) > arrFieldValueLength(n) Then
     arrFieldValueLength(n) = Len(arrSort(i,n))
    End If
   End If
  Next n
 Next i
‘Merge fields into list that can be used for sorting using @Sort function
 For i = 0 To coll.Count – 1
  If Not strSort = “” Then strSort = strSort & “:”
  strSort = strSort & “”””
  For n = Lbound(fieldnames) To Ubound(fieldnames) + 1
   strSort = strSort & Left(arrSort(i,n) & Space(arrFieldValueLength(n)), arrFieldValueLength(n))
  Next n
  strSort = strSort & “”””
 Next i
‘ —
‘ — 2) Sort array
‘ —
 arrSort = Evaluate(“@Sort(” & strSort & “)”)
‘ —
‘ — 3) Use sorted array to sort collection
‘ —
 Set collSorted = coll.Parent.GetView(viewname).GetAllDocumentsByKey(fakesearchstring)
 For i = 0 To Ubound(arrSort)
  Set doc = db.GetDocumentByUNID(Right(arrSort(i), 32))
  Call collSorted.AddDocument(doc)
 Next i
‘ —
‘ — 4) Return collection
‘ —
 Set SortCollection = collSorted
End Function


Function AddToList (Value As Variant, ValueList As Variant)
 Dim tmpValueList As Variant
 ‘ Load the array element by element so that the datatypeis preserved
 Redim tmpValueList(Ubound(ValueList))
 For i = 0 To Ubound(ValueList)
  tmpValueList(i) = ValueList(i)
 ‘ Determine if we are dealing with a new list, if absolutely no values in the first entry, then add new value to 0
 If Ubound(tmpValueList) = 0 And Cstr(tmpValueList(0)) = “” Then
  x = 0
  x = Ubound(tmpValueList) + 1
 End If
 Redim Preserve tmpValueList(x)
 tmpValueList(x) = Value
 AddToList = tmpValueList
End Function


Function createTable(FldTitles As Variant ,FldNames As Variant, doccoll  As notesdocumentcollection ,rtitem As NotesRichTextItem,msgTitle As String,msgBody As String ) As NotesRichTextItem
 ‘Takes Documentcollection & creates tabular information on to the passed  rtitem (rich text item)
 Dim TempNitem As NotesItem
 Dim TempNm As NotesName
 Set ritem=rtitem
 Set rtnav = ritem.CreateNavigator
 Set rstyle=session.CreateRichTextStyle
 ‘heading in the body section of the mail
 Call  ritem.AppendStyle(rstyle)
 Call  ritem.AppendStyle(rstyle)
 rows=doccoll.Count +1
 Call ritem.AppendTable(rows,cols)
 Call rtnav.FindFirstElement(RTELEM_TYPE_TABLECELL)
 ‘heading of the table
 Call  ritem.AppendStyle(rstyle)
 For i=0 To Ubound(FldTitles)
  Call ritem.BeginInsert(rtnav)
  Call ritem.AppendText(FldTitles(i))
  Call ritem.EndInsert
  Call rtnav.FindNextElement(RTELEM_TYPE_TABLECELL)
 Call  ritem.AppendStyle(rstyle)
 Set  doc=doccoll.GetFirstDocument
 While Not (doc Is Nothing)
  For i=0 To Ubound(FldNames)
  ‘check for date/ names document link
   Call ritem.BeginInsert(rtnav)
   If FldNames(i)=”Doc_Link” Then
    Call ritem.AppendDocLink(doc,doc.Created)
    Set TempNitem=doc.GetFirstItem(FldNames(i))   
    If TempNitem.IsNames Then
     Set TempNm=Nothing
     Set TempNm=New NotesName(TempNitem.Values(0))
     Call ritem.AppendText(TempNm.Common)
    Elseif Isdate(TempNitem.Values(0)) Then
     Call ritem.AppendText(Format(TempNitem.Values(0),”DD-MMM-YYYY”))  
     Call ritem.AppendText(TempNitem.Values(0))    
    End If
   End If
   Call ritem.EndInsert
   Call rtnav.FindNextElement(RTELEM_TYPE_TABLECELL)  
  Set doc=doccoll.GetNextDocument(doc)
 Set CreateTable=ritem
End Function


Function GetProfile(ProfName As String) As notesdocument
     ‘Get Database Profile and return to variable
 On Error Resume Next
 Print “Opening Database Profile”
 Set getProfile=Db.GetProfileDocument( profName)
 If Not getProfile Is Nothing Then
  If Not var Then Set getProfile=Nothing
 End If
End Function

WebQueryOpen and Computed fields

Well finally I am getting a bit more back on the development track. So I might spend some times here with dropping some code.

One of my latest ‘how do I do that?’ challenges was to write values collected by a WQO agent into a Form. 

So how do you do this with computed fields?

Well you get the documentcontext just like you would normally do:

Dim s As New notessession
Dim db As notesdatabase
Set db = s.currentdatabase 
Dim doc As notesdocument
Set doc = s.documentcontext

An editable field you would set in this way:

doc.fieldX = “collected data”

But as expected this will come up as editable value in the field name in the web form. You could att the htmlattribute “type = hidden” but I could navigate to that document via the DOM structure of the page.

If you would avoid that you could try writing to a computed field using these statements:

doc.cfieldX=”collected data”
doc.cfieldXWQO=”collected data””

  • cfieldX is the editable field, set type=”hidden”
  • cfieldXWQO is the computed field, set as computed value ‘cfieldXWQO’.


If you would save the document via a normal way (@command([FileSave]) or a JS submit() the value in the field cfieldXWQO is not saved. So in order to save the collected data you need to use a WebQuerySave agent…

Validate email address (LotusScript)

Yesterday I spent some time on googling looking for a decent LS function that validates an email internet address. 

The evaluate method on

@ValidateInternetAddress( [ addressFormat ] ; Address )

validates only on characters before and after the @ sign, so it does not check the top level domain (.com, .org or .se) and was therefor of no use.

Well, what did Google bring me then? Nothing of interest unfortunately.

Tomas sent me the function below but when testing it it does not properly checked if a username is given (‘’ would validate).

A simple combination of these two checks will do the job but this looks a bit poor. Maybe you have a better solution?

Function CheckSMTPAddressChar( smtpAddress As String) As Boolean
%REM – Check to see if any incorrect characters is used in the SMTP address
smtpAddress  : the SMTP address to validate
return    : will return true if valid email address
 Dim notAllowed As Variant
 Dim noNotAllowed As Integer
 Dim char As String
 Dim charNum As Integer
 Dim noAtChar As Integer
 Dim i As Integer
 CheckSMTPAddressChar = True
 For i = 1 To Len( smtpAddress)
  char = Mid( smtpAddress, i, 1)
  If char = ” ” Then char = “<space>”
  charNum = Asc( char)
  REM  No local characters is allowed only … normal once
  Select Case charNum
  Case 65 To 90  ‘ 65 = a, 90 = z
  Case 97 To 122  ‘ 97 = A, 122 = Z
  Case 48 To 58  ‘ 48 = 0, 58 = 9
  Case 45    ‘ 45 = –
  Case 46    ‘ 46 = .
  Case 64    ‘ 64 = @
  REM  We can have 1 char 46 = @ but not 2
   noAtChar = noAtChar + 1
   If noAtChar > 1 Then
    CheckSMTPAddressChar = False
   End If
  Case 95    ‘ 57 = _
  Case Else
   CheckSMTPAddressChar = False
  End Select
End Function


Dim smtpAddress As String
 smtpAddress = “
 If CheckSMTPAddressChar(smtpAddress) And smtpAddress Like “*@*.*” Then
  ‘do a double check
  Dim emailChecker As Variant
  Dim checkThis()
  Redim checkThis(0)
  checkThis(0) = smtpAddress
  emailChecker = Evaluate(|@ValidateInternetAddress([Address821];”| + checkThis(0) + |”)|)
  If emailChecker(0) = “” Then
   Print “Valid Email address!”
   Print “Email address not valid!”
  End If    
  Print “Email address not valid!”
 End If
End Sub

Combine column lookups

Hejsan. I will pick the quickest way to post something in the SNTT category by just adding an image which explains itself. It is probably taken from a LotuSphere session (guess the session?).

I recently used in in updating an application that was dated somewhere from 1998 and used @DBColumns in a form more than 30 times, so adding this technique was a major performance improvement…

combining multiple dbcolumns
combining multiple dbcolumns

Hide paragraph if formula is true – HTML comment

The one thing I hate about the ‘hide paragraph if…’ feature is that, without opening the text properties box and navigate to the fifth tab, you hardly can tell if the paragraph is hidden or displayed and for which system (Notes client, Mobile or Web client).

In order to save some time and make it more visual I decided to add some HTML comment tagging saying that this piece of paragraph is hidden (or when conditional what the formula is).

Maybe you have a better suggestion?

hide when paragraph

My CSS Toolbox

I do not know how many times I have written a class for aligning a text right or left. Too many, that’s for sure.

The idea with a CSS toolbox is to include a separate stylesheet for these “utility” styles. A CSS toolbox contains styling information that is nothing unique to any particular web application. It’s a collection of common styles that can be useful on any web development project.

A CSS toolbox is not a CSS reset and it is not a CSS framework. And it contains none of the special styling that makes any web application so unique.

Using a CSS toolbox will save you some time. It saves you from writing the same styles over and over again.  If you use the same toolbox your markup will share the same common class names and makes it easier for you to jump back into and understand it.

Here is mine:


* { margin: 0; padding: 0; }

html { overflow-y: scroll; }

body { font: 62.5% Helvetica, sans-serif; }

ul { list-style: none inside; }

p { font: 1.3em/1.3em; margin-bottom: 1.3em; }

a { outline: none; }

a img { border: none; }
.floatleft { float: left; }

.floatright { float: right; }

.clear { clear: both; }

.transpBlack { background: url(transpBlack.png); }
.layoutCenter { margin: 0 auto; }
.textCenter { text-align: center; }
.textRight { text-align: right; }
.textLeft { text-align: left; }

.error { border: 1px solid #fb4343; padding:3px; color: #fb4343; }
.warning { border: 1px solid #d4ac0a; padding: 3px; color: #d4ac0a; }
.success { border: 1px solid #149b0d; padding: 3px; color: #149b0d; }
.callOut { font-size: 125%; font-weight: bold; }
.strikeOut { text-decoration: line-through; }
.underline { text-decoration: underline; }
.resetTypeStyle { font-weight: normal; font-style:normal; font-size: 100%; text-decoration: none; background-color: none; word-spacing: normal; letter-spacing: 0px; text-

transform: none; text-indent: 0px; }


#page-wrap {
 width: 775px;
 margin: 0 auto;


.page-break { page-break-before: always; }

.hide { display: none; }
.show { display: block; }
.invisible { visibility: hidden; }