vrijdag 28 januari 2011

Easy way to add document links to listitems

The task seems easy enough: Using Sharepoint 2010, attach documents to a listitem.

Sure, no problem. Sharepoint offers out of the box attachments for any type of list.

Done. One would say. At least, I said so.
No, the documents are already sitting and waiting in a document library. We don't want to add new documents, and we sure don't want to have the same document multiple times in our database.

First, when using default attachments, the document are "physically" uploaded for each listitem. 
Second, managing these attachments is, well, not the way one wants to manage documents.

We wanted to attach existing documents to multiple listitems and one place to manage those documents. That way new document revisions would be up to date for each listitem the document is linked to. 

Despite some great tools that could accomplish this task, we found these often to be too complicated when it comes to background management. The multiple lookup field also wasn't quite what we wanted.

We came up with a quick and dirty solution: Just add hyperlinks to a listitem's rich text field by clicking on the desired document.

How we did what:
  1. Add a rich text field to a list. For this example, call it MyAttachments
  2. In Sharepoint Designer, open the NewForm.aspx and EditForm.aspx of the list.
  3. In both these pages, add the desired document library at the bottom of the page: Insert -> DataView.
  4. Add a javascript reference to the latest jQuery.js to the PlaceHolderAdditionalPageHead section
  5. Add this script:
_spBodyOnLoadFunctionNames.push('SetDocumentSelector');

var LinksHTML = '';
var Fields = null;
var RtfField = null;
var RtfTextBox = null;

function SetDocumentSelector() {
// Get all fields of this list. 
Fields = init_fields();
// Get the contentfield of the rich text field and hide it.
RtfField = $(Fields['MyAttachments']).find('div[contenteditable]:first');
RtfField.parents('tr:first').hide();
// if the contentfield was found, get the hidden textbox where the plain input is stored.
if (RtfField.length>0 && typeof(RtfField.attr('inputfieldid'))!='undefined') {
RtfTextBox = $('#'+RtfField.attr('inputfieldid'));
}
// The textbox is present? 
if (RtfTextBox!=null && RtfTextBox.length>0) {
// process possible existing value.
if (RtfTextBox.val().length>0) {
SetStartupCheckboxes();
}
// Attach onclick event to the checkbox of each listitem in the documentlibrary.
$('tr.ms-itmhover, input:checkbox.s4-itm-cbx').click(ProcessSelection);
}
}

// Gather all document IDs stored in the id attribute in the hyperlinks in the rich text field.
function SetStartupCheckboxes() {
var reItemID = new RegExp('\\sid=\\d+','gi');
var reID = new RegExp('\\d+','g');
var ItemIDs = RtfTextBox.val().match(reItemID);
if (ItemIDs!=null && ItemIDs.length>0) {
ItemIDs = ItemIDs.join(',').match(reID);
while (ItemIDs!=null && ItemIDs.length>0) {
SetCheckbox(ItemIDs.pop());
}
}
}

// set the checkbox of a listitem in the documentlibrary.
function SetCheckbox(id) {
var DocumentLinks = $('tr.ms-itmhover div.itx[id='+id+']').parents('tr.ms-itmhover:first');
if (DocumentLinks.length>0) {
var Row = DocumentLinks.get(0);
ToggleItemRowSelection2(CtxFromRow(Row), Row, true, true );
}
}

// Find all checked documents and generate html hyperlinks to these documents.
function ProcessSelection() {
RtfField.empty();
var CheckedBoxes = $('input:checkbox.s4-itm-cbx:checked');
if (CheckedBoxes.length>0) {
var CheckedRows = CheckedBoxes.parents('tr.ms-itmhover');
var DocumentCell = CheckedRows.find('div.itx');
LinksHTML = '';
DocumentCell.each(ParseDocument);
RtfField.append(LinksHTML);
}
}

// Create HTML for a single hyperlink to a document.
function ParseDocument(index) {
var Cell = $(this);
var Link = Cell.children('a:first');
var DocumentID = Cell.attr('id');
var DocumentName = Link.text();
var DocumentLink = Link.attr('href');
var NewLink = '<a href="'.concat(DocumentLink,'" id="',DocumentID,'">',DocumentName,'</a><br/>');
LinksHTML = NewLink.concat(LinksHTML);
 }

// find all formfields. Quick and dirty. 
function init_fields() {  
var res = {};
$("td.ms-formbody").each(function(){  
var Local = $(this).html();
var FINindex = Local.indexOf('FieldInternalName="');
if(FINindex>=0) {  
var start = FINindex+19;  
var stop = Local.indexOf('FieldType="')-7;  
var nm = Local.substring(start,stop);  
res[nm] = this.parentNode; 
}
});  
return res;  
}

All you need to do is check one ore more documents which then will be added as a hyperlink to your listitem. Everytime these forms is loaded, the linked documents will already be checked in the document library on the page.

Looking at the picture below (it's Dutch, but as a sharepoint user, you'll understand), you will see an other nice side effect of the approach: while creating a new listitem, you can manage the library on the page itself.


Ofcourse, as stated before, it's quick and it's dirty: when a document is removed from the library, clicking on that hyperlink will result in a Not Found error. 

The HTML can be made into whatever form you want, but be aware that the rich text field won't allow just anything: if it doesn't like your input, it will remove or replace parts of it. 

Hope you can make some good use of it.

Regards,
Ruud.

dinsdag 23 maart 2010

Render ChoiceField RadioButtons and CheckBoxes horizontally

When making a simple questionaire sharepoint list, we found the vertical orientation of the choices not the best option. We wanted the choices to render on one single line.

Instead of looking at the server's side of rendering things, I directly went for the clientside approach.

First, want we do not want:


Moving on to what we do want:


Here's how:

When looking into the HTML we find each group of options wrapped in a table:

<table cellpadding="0" cellspacing="1">
  <tr>
    <td>
       <span class="ms-RadioText" title="Answer 1.1">
         <input type="checkbox"/>
         <label>Answer 1.1</label>
       </span>
    </td>
  </tr>
  ... (remaining options)
</table>

My idea was to get the spans out of the table, leaving us with 4 spans in a row. Not a hard thing to do when you take jQuery in consideration: We have to unwrap it!

So, for starters, let me show an example of what unwrapping means.

<Tag1><Tag2><Tag3>InnerText</Tag3></Tag2></Tag1>

Unwrap Tag3 results in:

<Tag1><Tag3>InnerText</Tag3></Tag1>

Taking this approach to our choicefields:
  1. Unwrap each span with class RadioText out of its cell
  2. Unwrap the span out of the row it just landed in
  3. Unwrap the group table's first span out of it's tbody
  4. Unwrap this first span out of the table.

How? Look below:

function _spBodyOnLoadWrapper() {
var Spans = $('SPAN.ms-RadioText');
Spans.unwrap();
Spans.unwrap();

Spans = $('SPAN.ms-RadioText:first-child');
Spans.unwrap();
Spans.unwrap();
}

or, the short version:

function _spBodyOnLoadWrapper() {
$('SPAN.ms-RadioText').unwrap().unwrap();
$('SPAN.ms-RadioText:first-child').unwrap().unwrap();
}

Hope this was of some value for you. 

donderdag 25 februari 2010

Catch the clientside onchange events on a date/time field

While spending my time in designing Sharepoint Forms, I often face the task of pre-populating fields according to who's logged, and under which conditions the user got to this page.

I'll write a post on prepopulating different types of fields in the near future, but before that let's talk about the datefield, and how to handle the changes made to this field.





Did I say field? Fields is what I should have said. This control has 3 input fields, and the very handy datepicker control (the little calendar). To fully catch the onchange event, we have to take a look at those 4 controls and make them fire our function upon change.

First things first.

1) The Datefield

Let's have a look at the HTML for this field:

<INPUT id=ctl00_m_g_30b0923a_0f4a_4deb_bc52_524e5708b391_ff10_1_ctl00_ctl00_DateTimeField_DateTimeFieldDate class=ms-input title=(Start)Date value=1-1-3000 maxLength=45
name=ctl00$m$g_30b0923a_0f4a_4deb_bc52_524e5708b391$ff10_1$ctl00$ctl00$DateTimeField$DateTimeFieldDate
AutoPostBack="0">

I've highlited the parts that makes this control easy for us to find. For javascript that is.
It's an INPUT field, with an id that ends with DateTimeField_DateTimeFieldDate and has a title that equal's the public name of the field ((Start)Date in this example).

Assuming you know your jquery (if not, go check it out!) here's my function to get the field.

function GetSPField(tagName, identifier, title) {
return $(tagName+'[id$='+identifier+'][title='+title+']').first();
}

var MyDateField = GetSPField('INPUT', 'DateTimeField_DateTimeFieldDate', '(Start)Date');


2) The Timefields

Now that we've found MyDatefield, we can get the Hourfield and MinuteField. fortunately Sharepoint doesn't make this very difficult. Just append 'Hours' and 'Minutes' to the datefield's id, and we're good to go:

var MyHourField = $('#'+MyDateField.attr('id')+'Hours');
var MyMinuteField = $('#'+MyDateField.attr('id')+'Minutes');


3) Events for the fields

With the 3 fields in our reach, we can add the event. Once again, jquery is our friend:

function TimeHasChanged() {
alert('welcome in this new day and age');
}

MyDateField.bind('change', TimeHasChanged);
MyMinuteField.bind('change', TimeHasChanged);
MyHourField.bind('change', TimeHasChanged);


4) Handling the DatePicker

When clicking the datepicker, we get a handy calendar enabling us to easily pick a date. Hence the name....
However after selecting an appropriate date, all it does is close and fill in the datefield. No events or nothing. At least no OnChange event.

Digging into the datepicker.js, in the _layouts folder, I found the following piece of code:

if (typeof(resultfield.onvaluesetfrompicker)=='function')
{
resultfield.onvaluesetfrompicker();
}
else
{
eval(resultfield.onvaluesetfrompicker);
}
Meaning we can set an onvaluesetfrompicker event on the datefield, that will be called when a date is picked. As simple as that.

Our final piece of code here should then be (quick and dirty):

MyDateField.get(0).onvaluesetfrompicker = TimeHasChanged;

Hope you get good use for it. Enjoy!