UI Tips and Tricks – Inline Visualforce Resize

A real frustration with inline Visualforce pages (added to standard page layouts) is the static nature of the height setting. From the layout we can set a specific height, but ideally we want the height set dynamically from the content height. Sounds like a simple enough requirement, however, the fact that the VF page section is implemented as an iFrame loaded from another domain makes cross-domain communication a non-trivial task. Note, for security reasons browsers enforce a same-origin policy, preventing script running across domain boundaries. Workarounds to this restriction include the HTML5 postMessage function on the client and proxy services on the server. So the question becomes, how can the iFrame content communicate across domains to tell the host page the correct height for the iFrame? The answer to this is somewhat contrived, but hopefully my basic approach below tells a clear enough story.

Here we go.
1. The inline VF page contains an iFrame, into which we load a helper script file from the base salesforce domain with a height parameter in the querystring.

document.getElementById(‘helpframe’).src=’https://emea.salesforce.com{!$Resource.iFrameHelper}?height=’+h+’&iframename=MYPAGENAME&cacheb=’+Math.random();

The random parameter is there to avoid caching issues. Crucially as this helper script is running on the same domain as the standard page layout, it can call a script in the page itself. Note the helper script is loaded from a static resource. To keep the solution generic the page name is passed as a parameter also, handily the title attribute in the host page is set to the page name, we’ll use this later to find the id for the iFrame.

2. In the helper script we extract the 2 parameters from the querystring and call a script function in the host page (via parent.parent – which traverses up the DOM to the parent page).

3. In order to add script to the host page we use the Sidebar injection technique (or hack) and introduce a simple Javascript function (via a narrow component) which takes the page name and height, finding the former in the DOM using Ext.query (Ext is already referenced in the page), and setting the element height to the latter.

Example solution components::

0. Pre-requisites:
User Profiles must have the “Show Custom Sidebar On All Pages” General User Permission ticked.

1. Add a HTML file static resource, named iFrameHelper – content below.

<html>
  <body onload="parentIframeResize()">  
    <script>  
      // Tell the parent iframe what height the iframe needs to be 
      function parentIframeResize(){ 
         var height = getParam('height'); 
         var iframename = getParam('iframename');
         // This works as our parent's parent is on our domain.. 
         parent.parent.resizeIframe(height,iframename); 
      }  
      // Helper function, parse param from request string 
      function getParam(name){ 
        name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); 
        var regexS = "[\\?&]"+name+"=([^&#]*)"; 
        var regex = new RegExp( regexS ); 
        var results = regex.exec( window.location.href ); 
        if( results == null ) 
          return ""; 
        else 
          return results[1]; 
      }
    </script>  
  </body>  
</html>

2. Add a HTML sidebar component (narrow left) – click “Show HTML” and paste in markup below.

<script>
  function resizeIframe(h, ifn){
   var e = Ext.query("iframe[title='"+ifn+"']");  
   console.log(e); 
   var itarget = e[0].getAttribute('id'); 
   Ext.get(itarget).set({height: parseInt(h)+10});
  }
</script>

3. Add a Visualforce page named MyTestInlineVFPage – paste in markup below.

<apex:page docType="html-5.0" standardController="Account">
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  
  <script type="text/javascript">      
        function resizeParentIFrame(){
            var h = document.body.scrollHeight;
            //TODO - replace with the relevant page name.
            var iframename = 'MyTestInlineVFPage';
            //TODO - replace with the relevant base url - page runs on the VF domain so functions return the VF domain. 
            var baseUrlForInstance = 'https://emea.salesforce.com';
            document.getElementById('helpframe').src = baseUrlForInstance+'{!$Resource.iFrameHelper}?height='+h+'&iframename='+iframename+'&cacheb='+Math.random();
        }
        
        function forceParentIFrameResize(){
            document.getElementById('helpframe').src=document.getElementById('helpframe').src;
        }
        
        window.onload=function(){
            resizeParentIFrame();
        }
  </script>  
</apex:page>

4. Add the VF page to a new section on an Account layout.

The solution above needs further work in the areas below. I’m planning to improve this as part of a commercial AppExchange package I’m working on and will post the improved resize solution.

Code quality.
Exception handling.
Calculation of the base salesforce domain – currently hardcoded in the inline page.

I’d be delighted to hear about other improvements, or indeed alternative approaches.

UI Tips and Tricks – Country List

This tip introduces a simple pattern used by many AppExchange solutions to manipulate standard layouts. In short, a custom sidebar component is added which contains JavaScript code which manipulates the page at the DOM level. The following simple example shows how the country fields on the account layout can be changed to lists – a common customer request. A Country__c custom object with the Name field populated with country names is required. The Dojo library is also used. Please note, the code below is old and hasn’t been tested recently, I provide this for illustration of the pattern, it certainly won’t be winning any prizes.

So, in the example, Dojo runs the replaceCountryInputs() function when the DOM is ready, this finds the country fields by their ID (Firebug is a good way to browse the page markup), we then remove the original input element and replace with a select element with the same Id. The select element is then populated in JavaScript with the result of a soql query – using the Ajax API. Finally we need to add the inline editing handlers to the new select element – and we’re done.

On the configuration side, the sidebar component must be assigned to the home page layout for relevant users and the setting to enforce display of custom sidebar components must be enabled.

As I’ve said, the use case here isn’t significant it’s the possibility that this technique enables in terms of standard layout manipulation. Be aware that the field ids may be subject to change.


<script src="/js/dojo/0.4.1/dojo.js"></script>
<script src="/soap/ajax/19.0/connection.js" type="text/javascript"></script>
<script type="text/javascript">
var arCountries = getCountries();
var vProcess = replaceCountryInputs();

function replaceCountryInputs()
{
  if (document.getElementById('acc17country')!=null) {
    var defaultVal = document.getElementById('acc17country').value;
    var cInput = swapFieldType('acc17country');
    setCountryListWithDefault(cInput, defaultVal);
  }
  if (document.getElementById('acc18country')!=null) {
    var defaultVal = document.getElementById('acc18country').value;
    var cInput = swapFieldType('acc18country');
    setCountryListWithDefault(cInput, defaultVal);
  }
  if (document.getElementById('acc17_ilecell')!=null) {
    SetupInline('acc17');
  }
  if (document.getElementById('acc18_ilecell')!=null) {
    SetupInline('acc18');
  }
}

function swapFieldType(i)
{
  var cInput = document.getElementById(i);
  var cInputParent = cInput.parentNode;
  cInputParent.removeChild(cInput);
  cInput = document.createElement('select');
  cInput.size = 1;
  cInput.id = i;
  cInput.name = i;
  cInputParent.appendChild(cInput);
  return cInput;
}

function setCountryListWithDefault(i, d)
{
  if(i!=null) {
    if(arCountries.length>0) {
      for(x=0;x<arCountries.length;x++) {
        if(arCountries[x]==d) {
          i.options[x] = new Option(arCountries[x], arCountries[x], false, true);
        } else {
          i.options[x] = new Option(arCountries[x], arCountries[x], false, false);
        }
      }
    } else {
      i.options[x] = new Option('No countries found', 'No countries found', false, true);
    }
  }
}

function SetupInline(prefix) {
  var _element = document.getElementById(prefix + '_ilecell');	
  if (_element) {
    _element.ondblclick = function() {
      var _loaded = false;
      if (!sfdcPage.editMode)
        sfdcPage.activateInlineEditMode();
        
      if (!sfdcPage.inlineEditData.isCurrentField(sfdcPage.getFieldById(_element.id)))
        sfdcPage.inlineEditData.openField(sfdcPage.getFieldById(_element.id));
        
      var idInput = prefix+'country';

      if (document.getElementById(idInput)!=null) {
        var defaultVal = document.getElementById(idInput).value;
        var cInput = swapFieldType(idInput);
        setCountryListWithDefault(cInput, defaultVal);
      }		
    }
  }
}

function getCountries()
{
  sforce.sessionId = getCookie('sid');
  sforce.connection.sessionId=sforce.sessionId;
  var out = [];
  try {
    var queryCountries = sforce.connection.query("Select Id, Name FROM Country__c ORDER BY Name");	
    var countries = queryCountries.getArray('records');
    for(x=0;x<countries.length;x++) {
      out[out.length] = countries[x].Name;
    }					
  } catch(error) {
    alert(error);		
  }	
  return out;
}
dojo.addOnLoad(replaceCountryInputs);
</script>

UI Tips and Tricks – Picture Upload

In the very early 90s I was employed as a professional Visual Basic programmer (and no that isn’t a contradiction in terms) enjoying greatly the development of client-server accounting systems. Good times. In those days tips and tricks sites were a big part of how the community shared knowledge. Following some recent reminiscing, through this series of posts, I’ll share some UI tips and tricks that aren’t necessarily an architects concern however I hope will prove helpful to some.

Ok, so down to business. Today’s tip concerns the upload and display of record images in the context of standard functionality, for example a Product image or Contact photo. The latter I’ll use as an example.

1. Add a field to Contact named [Photo Document Id], of the text type, 18 char length.

2. Add a field to Contact named [Photo], of the text formula type, formula set as below.

<formula>IMAGE('/servlet/servlet.FileDownload?file='Photo_Document_Id__c'')</formula>

3. Add a Visualforce page named ContactPictureUploader, with no markup between the page tags.
4. Add an Apex class ContactPictureUploaderController, code as below, set as the VF page controller and ensure the page action is set to the initialise method.

public with sharing class ContactPictureUploaderController {
	private Id contactId;

	public ContactPictureUploaderController(){
		contactId = ApexPages.currentPage().getParameters().get('cid');
	}

	public PageReference initialise(){
		List<Attachment> listA = [select a.Id from Attachment a 
									where a.createdById =:UserInfo.getUserId() 
									and a.parentId=:contactId order by a.createdDate desc limit 1];														
		if (listA.size()>0){
			Id attachmentId = listA[0].Id;
			
			Contact c = [select Id from Contact where Id=:contactId];
			c.Photo_Document_Id__c = attachmentId;
			update c;
		}
		return new PageReference('/'+contactId);		
	}
}

VF Page markup.

<apex:page controller="ContactPictureUploaderController" action="{!initialise}">
<!-- Controller context page - no markup -->
</apex:page>

5. Add a custom button to the Contact object of the JavaScript type, named Upload Photo, script as below.

parent.window.location.href='p/attach/NoteAttach?pid={!Contact.Id}&retURL=/apex/ContactPictureUploader?cid={!Contact.Id}'

In short, the solution works by invoking the standard attachment page, which on submit redirects to the VF page which copies the uploaded document id to the Contact Photo Document Id field, then redirects back to the contact record. The image field on the Contact object then loads the image using the IMAGE() formula field, easy. The image field can then be used on the contact layout, related lists etc..