FF and IE just can’t get along: javascript event madness

I tried helping someone on the Mastering SharePoint forum today.  shalinparmar was trying to get a simple form button to fire off a redirect based on a text control’s value and stumbled into a hornet’s nest of FF vs. IE junk.  I stayed with it because I really wanted to learn why this wasn’t working and what the fix would be for both browsers.

One thing I learned was that IE and FF handle events like keypress, keydown, and keyup differently.  IE especially makes it difficult when it auto-submits the form (and everything in MOSS content areas belongs to a form) on ENTER (char 13).  The result can be a long ddddiiiiiinnnngggggg as the event fires off–far from ideal.  For these reasons, I used keyup to fire the next javascript function and keypress was used to supress IE’s auto-submit.

One browser passes event.which while the other uses event.keycode.  So there’s a special case for detecting which DOM element exists before you can check which key was pressed.

Another difference lies in the way FF and IE handle redirects.  Windows.navigate simply doesn’t exist in FF’s DOM, so we use window.location.

Lastly, a button tag will auto-submit in FF so you need to use an input tag with type="button".

At any rate, the final working version is below.  Don’t ask what doIdNumSearch() does, I didn’t write that.

<table border="0">

    <tr>

        <td class="ms-advsrchHeadingText" colspan="2">

            <h3 class="ms-standardheader" style="font-size:
1em">

                Search by number…</h3>

        </td>

    </tr>

    <tr>

        <td class="ms-advsrchText">

            <label for="my_list_idnum">

                <b>Number</b>:</label>

        </td>

        <td class="ms-advsrchText">

            <input name="my_list_idnum" type="text" maxlength="200" id="my_list_idnum" onkeypress="return killEnter(event);" onkeyup="doSearch(event);">

        </td>

    </tr>

    <tr>

        <td>

        </td>

        <td>

            <input type="button" value="Search" onclick="doIdNumSearch();" />

        </td>

    </tr>

</table>

 

<script type="text/javascript">

function
doIdNumSearch() {

  var k =
document.getElementById(‘my_list_idnum’).value;

  var loc = ‘results.aspx?k=’ + k + ‘contentclass:STS_ListItem_GenericList&s=my
list’
;

  window.location = loc;

}

 

function
doSearch(e) {              //must fire on keyup because of IE

    var
characterCode;

    if(e
&& e.which){          

    e = e;

    characterCode = e.which;        //for most
browsers

    }else{

    e = event;

    characterCode = e.keyCode;      //for IE

    }

    if(characterCode
== 13){

    doIdNumSearch();

  }

}

 

function
killEnter(e){                      //this stops IE from auto-submitting on ENTER

    if(e.keyCode
== 13 || e.which == 13){

    return false;

    }

  return true;

}

</script>

 

 

Conditional Validaton with Javascript

While I try to stay away from javascript validation, sometimes security concerns get trumped by "quick and dirty."  Such is the case when you need simple, conditional validation on forms in SharePoint.  Logic like "if Due Date is not NULL then make Justification required."  This was the example used in the question from STP today. 

As usual, I was onto the solution when someone’s blog straight up answered it for me.  Thanks to Edin Kapic for posting his date logic soluton.  So far this only works while the "Justification" field is a Plain text field.  I had too much trouble getting the RTF field to properly identify any attributes I could use the logic portion.  If someone else can get it to work with an RTF field, please drop me a line or post on EndUserSharePoint.com.

<script type="text/javascript">

 

function PreSaveAction()

{

    var findDate = getTagFromIdentifierAndTitle("input","DateTimeFieldDate","Due Date");

    var findText = getTagFromIdentifierAndTitle("textarea","","Test");

    if(findDate.value != "" && findText.value == "")

    {

        alert("You must provide a Justification for the Due Date");

        return false; // Cancel the item save process

    }

    return true;  // OK to proceed with the save item

}

 

function getTagFromIdentifierAndTitle(tagName, identifier, title) {

  var len = identifier.length;

  var tags = document.getElementsByTagName(tagName);

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

    var tempString = tags[i].id;

    if (tags[i].title == title && (identifier == "" || tempString.indexOf(identifier) == tempString.length – len)) {

      return tags[i];

    }

  }

  return null;

}

//–>

</script>

Print a Web Part with Javascript

Add a content editor web part to your page with the following inside:

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("printWebPart");

 

function
printWebPart(wpNum) {

    if (wpNum
&& document.getElementById != null)    {

        var
html = ‘<HTML>n<HEAD>n’;

        if
(document.getElementsByTagName != null)  {

            var
headTags = document.getElementsByTagName("head");

            if
(headTags.length > 0) html += headTags[0].innerHTML;

        }

        html += ‘n</HEAD>n<BODY>n’;

        var
printDiv = ‘WebPartWPQ’;

            printDiv += wpNum;

        var
printWPElem = document.getElementById(printDiv);

        if
(printWPElem != null){     

            html += printWPElem.innerHTML;

            }else{

            alert("Could
not find the printDiv div"
);

            return;

        }

        html += ‘n</BODY>n</HTML>’;

        var
printWP = window.open("","printWebPart");

        printWP.document.open();

        printWP.document.write(html);

        printWP.document.close();

        printWP.print();

    }

}

</script>

Now place a link anywhere you want that looks like this:

<a href="javascript:void(printWebPart(3))">Print
this WebPart</a>

The number (3) represents the 3rd web part on the page.  You have to count the CEWP we added and the default Search Box in the title area.  So these two together will print your "main" web part.

If I have time I might be able to make something that helps "select" the web part.  But alas, I have other items to tend to tonight! :)

EndUserSharePoint.com: The Fundamentals of Libraries and Lists

Anyone getting their feet wet with SharePoint this year should look into Mark Miller’s online workshop.  Most users will get access and a little training from their project team but a lot of project teams struggle with end user training before rollout.  I’m also concerned where I’ve seen end user training that doesn’t follow best practices for even the simplest processes.

Mark will help you understand the basics of "Contributor-level" access and at this price point, may be a great replacement for the hurried and spotty training some project teams produce when they’re under the gun.  Maybe if you tell Mark that AutoSponge sent you, you’ll get a free cookie.


EndUserSharePoint.com Author Profile

Fixing ExpGroupBy in Core.js – International MOSS Community Comes Through

I’ll say it again, I love the MOSS community.  By adopting high standards and documenting our successes, we all win.  Case in point:  I made a data view web part for administering a human "state machine" workflow (article coming soon) that used grouping, but the expand/collapse groups did not work in FireFox.  Thanks to MOSS community awesomeness, I fixed the issue in just a few minutes.

The javascript in core.js that expands and collapses the grouped sections did not work in FireFox.  I opened Firebug to have a look.

<a onclick="javascript:ExpGroupBy(this);return false;"
href="javascript:">

This code covers every + image, so I checked the various javascript files on the page for the ExpGroupBy function.  When my CTRL+F landed me on line 1500 of core.js, I was horrified to see this:

function
ExpGroupBy(formObj)

{

      if (browseris.nav)

            return;

The first line of the script stops FF from running the script but there’s no real reason.  "Google will save me from this insanity!"

search: sharepoint expgroupby firefox

First result: sharepoint + Firefox – SWiKI found a link to an Italian page:  Fix per ExpGroupBy di WSS3 su FireFox

I went back to google:

search: site:www.marcobellinaso.com/blog-ita/post/Fix-per-ExpGroupBy-di-WSS3-su-FireFox.aspx click [Translate]

On the translated page, I could see that Marco noticed the same problem with ExpGroupBy.  He fixed the function and provided a download.  That was awesome.  Problem fixed in minutes instead of hours.  I’m reposting the script here for my documentation.  I suggest, for a quick fix on a single page, add a Content Editor Web Part with the new function like I have below; set the web part to "hidden."

<script type="text/javascript">

function
ExpGroupBy(formObj)

{

  if
((browseris.w3c) && (!browseris.ie)) {

       
document.all=document.getElementsByTagName("*");

  }

  docElts=document.all;

  numElts=docElts.length;

  images=formObj.getElementsByTagName("IMG");

  img=images[0];

  srcPath=img.src;

  index=srcPath.lastIndexOf("/");

  imgName=srcPath.slice(index+1);

  if (imgName==‘plus.gif’)

  {

        fOpen=true;

        displayStr="";

        img.src=‘/_layouts/images/minus.gif’;

  }

  else

  {

        fOpen=false;

        displayStr="none";

        img.src=‘/_layouts/images/plus.gif’;

  }

  oldName=img.name;

  img.name=img.alt;

  img.alt=oldName;

  spanNode=img;

  while(spanNode
!=null)

  {

       
spanNode=spanNode.parentNode;

        if (spanNode !=null &&

              spanNode.id !=null &&

             
spanNode.id.length > 5 &&

             
spanNode.id.substr(0, 5)=="group")

              break;

  }

  parentNode=spanNode;

  while(parentNode
!=null)

  {

       
parentNode=parentNode.parentNode;

        if (parentNode !=null &&

             
parentNode.tagName=="TABLE")

              break;

  }

  lastNode=null;

  if
(parentNode !=null)

  {

       
lastNode=parentNode.lastChild;

        if (lastNode !=null &&
lastNode.tagName=="TBODY")

             
lastNode=lastNode.lastChild;

        if (lastNode !=null &&
lastNode.tagName=="TR" &&
lastNode.lastChild !=null)

             
lastNode=lastNode.lastChild;

  }

  for(var i=0;i<numElts;i++)

  {

        var childObj=docElts[i];

        if (childObj==spanNode)

              break;

  }

  ID=spanNode.id.slice(5);

  for(var j=i+1; j<numElts; j++)

  {

        var childObj=docElts[j];

        if (childObj.id.length > 5 &&

             
childObj.id.substr(0, 5)=="group")

        {

             
curID=childObj.id.slice(5);

              if (curID <=ID)

                    return;

        }

        parentNode=childObj;

        while(parentNode !=null)

        {

             
parentNode=parentNode.parentElement;

              if (parentNode==spanNode)

                    break;

        }

        if (parentNode==spanNode)

              continue;

        if (childObj !=img &&

             
childObj.tagName=="IMG"
&&

              childObj.src
&&

             
childObj.src.slice(childObj.src.length – 25)==‘/_layouts/images/plus.gif’)

        {

              childObj.src=‘/_layouts/images/minus.gif’;

             
oldName=childObj.name;

             
childObj.name=childObj.alt;

             
childObj.alt=oldName;

        }

        if (childObj.tagName==spanNode.tagName &&

              childObj.id !="footer")

        {

              childObj.style.display=displayStr;

        }

        if ((childObj.tagName=="TABLE"
&& lastNode==null) ||
childObj==lastNode)

              break;

  }

}

//–>

</script>

Important Surveys Don’t Like “Save”

While building a survey, I noticed that the "Save" and "Next" buttons appear on every segment of the survey except the final page where "Finish" appears.

I don’t want my users to get confused and "Save" their survey, assuming they’re done, when they really need to use "Next" to complete the remaining questions.

So, I opened the Edit page option on both the NewForm.aspx and EditForm.aspx using the URL trick (?PageView=Shared&ToolPaneView=2) and added the following javascript using a Content Editor Web Part:

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("setValue");

 

function setValue()
{

  hideButton("Save");

}

 

//This function
hides a button on the page

function
hideButton(valueDef){

  var frm =
document.forms[0];

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

    if
(frm.elements[i].type == "button"
&& frm.elements[i].value == valueDef) {

    frm.elements[i].style.display = "none";

        }

    }

}

//–>

</script>

Advanced Search Form and Preselected Scopes

Advanced Search pages don’t seem to do what they’re told.  If you want to narrow the Advanced Search results to a specific scope:

  1. Go to your Advanced Search Page.
  2. Edit the Page.
  3.  Expand the Scopes portion of the menu.
  4. Check the box "Show Scope Picker."

Now you can check the box to keep advanced results in the scope(s) you choose.

To preselect all check boxes:

  1. Go back into Edit mode.
  2.  Add a Content Editor Web Part to the page. 
  3. Set the Chrome Type to "None" (this hides the CEWP).
  4. Open the Source Editor and enter the following script:

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("checkAll");

 

function checkAll()
{

    var frm =
document.forms[0];

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

    if
(frm.elements[i].type == "checkbox")
{

    frm.elements[i].checked = true;

        }

    }

}

//–>

</script>

Pop Quiz – Content Types and Folders

SharePoint Quiz!

Scenario

  1. Create three Content Types using the following information architecture:  Document > Company Document > HR Document > HR Policy. 
  2. Create two Document Libraries; one at the Site Collection level and the other in an HR Sub-site. 
  3. In the Site Collection Document Library, set the Content Type to only use Company Document. 
  4. In the HR Document Library, set the Content Types to use HR Document and HR Policy. 
  5. Create a template in Word and add the template to the Site Collection Document Library.  Then point the HR Policy Content Type to the template.
  6. Create a folder in the HR Document Library called "Policies." 
  7. Remove the HR Policy Content Type from the HR Document Library visibility and remove the HR Document Content Type from the folder’s new item visibility.

When you create a new document in the Policies folder (by selecting New HR Policy), what Content Type will the document have after saving it?

A.  HR Policy, what you picked from the New Item menu.
B.  Company Document, the same as the template.
C.  Company Document when opened then HR Policy once saved to the library.
D.  None of the above.

Answer (spoiler)
.
.
.
.
.
.
.
.
.
.
D.  None of the Above

Explanation:  Normally, when you open a template with the Company Document Content Type and save it to a library with only one Content Type, the system changes the document’s Content Type to match the library.  However, in this case, even though you chose "New HR Policy" from the menu, the result is different.  The template has the Company Document Content Type which does not exist in the target HR Document Library so the system uses the default Content Type for the target library, HR Document.  HR Document became the default Content Type for the library when we removed HR Policy from visibility.

Lesson:   If your target library has more than one Content Type allowed, make sure to type your template.

Cleanup Hyperlinks in Calculated Columns

A common request from end users:  "Can we make a calculated column of hyperlinks that don’t display the whole url?"

Because MOSS filters "dangerous code" from metadata columns, calculated columns can not resolve to something with code in it and the Excel function "HYPERLINK" does not work in MOSS.  So, what can we do?

Admins and power users can clean this up with SPD or custom web parts.  However, the end user has fewer options.  If the end user has the Edit Page permission and access to the Content Editor Web Part, they can use some javascript to clean up the links.  If you don’t normally allow that, you may want to add the second technique’s script to your default.master as it will clean up any ugly links on your page.

Technique 1

Let’s say, you have a bunch of document names in a list and you want to create a url to another site using a calculated column like this:  ="http://google.com/"&Title

We know the "base URL" and we want to dynamically separate the appended portion from the base (e.g., displaying http://www.google.com/labs as labs),  so we add this script to the list view page using a CEWP:

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("cleanLinks");

 

function
cleanLinks(){

  var baseURL =
"http://google.com/&quot;;

  var len =
baseURL.length;

  var links =
document.getElementsByTagName("a");

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

    if
(links[i].href && links[i].href.substring(0,len) == baseURL){

      if
(links[i].innerText==undefined) {

      links[i].textContent =
links[i].textContent.substring(len);

      }

      else{

      links[i].innerText =
links[i].innerText.substring(len); //for IE

      }

    }

  }

}

 

//–>

</script>

Just replace the value of the variable ‘baseURL’ with whatever you use for a base in your calculated column.  If you also want to clean up the text (like in the case of url-encoded file names or underscores that replace spaces), you’ll need the replace() method in both lines that replace the display text (one for Firefox and one for IE).

Technique 2

In this case, we can not predict the "base URL."  So we will clean up our links by replacing the url with the word "link" for any tag using the href attribute as its display text.

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("cleanLinks");

 

function
cleanLinks(){

  var links =
document.getElementsByTagName("a");

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

    if
(links[i].href && links[i].href == links[i].innerText){

      links[i].innerText = "link";

      }else{

      if
(links[i].href && links[i].href == links[i].textContent){

      links[i].textContent = "link";

      }

    }

  }

}

 

//–>

</script>

A slightly fancier version of that technique adds an image using some embedded CSS:

<STYLE TYPE="text/css" MEDIA=screen>

<!–

.mylink

{

    background-image:
url(/_layouts/images/link.gif);

    background-repeat:
no-repeat;

    font-family:
tahoma,sans-serif;

    font-size: 8pt;

    padding-left:
25px;

}

–>

</STYLE>

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("cleanLinks");

 

function
cleanLinks(){

  var links =
document.getElementsByTagName("a");

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

    if
(links[i].href && links[i].href == links[i].innerText){

      links[i].parentNode.className="mylink";

      links[i].innerText = "link";

      }else{

      if
(links[i].href && links[i].href == links[i].textContent){

      links[i].parentNode.className="mylink";

      links[i].textContent = "link";

      }

    }

  }

}

//–>

</script>


Road to Document Management pt4 — Configuring Content Types

After researching our options and discussing the benefits of each alternative, out team set out to start configuring the document content types for the system.

That puts us around Step 5 of our original outline:

Step 5 – Convert document metadata to policy

SMEs will write policies for each filetype and subtype
based on the metadata collected from the analysis.  These policies will
be followed for conversion/integration from the legacy system as well
as document life-cycle within the new MOSS system.  This policy must
reflect all technical and practical limits of MOSS including filetype,
filesize, and characters used in the filename.  Any changes needed to
MOSS to accomodate certain files must be reviewed by the technical
resources.

That description assumed we would convert a lot of legacy documents–which still remains to be seen.  Our main goal aims at creating the best possible system to manage future documents.  To start, we created a document content type based on Document and named it Company Document.  This gives us total control over any document Content Type without modifying the OOB Document Content Type.  We then replaced the Content Type in the top-level Site Collection’s document library with Company Document.

The top-level Site Collection’s library will hold our document templates used by other Content Types.  So now we need to identify what metadata will apply to all Company Documents (if any) and add that to the Content Type as a Site Column.  We created a list called Records Retention Schedules (these are mandated for us by the State).  We added the schedules that pertained to our organization to the list then added a new Lookup site column to the Company Document Content Type.

Next, we created a Request content type and a child type called New Site Request.  Although most sites include this feature as a Custom List, we chose documents because of the integration with Outlook and the ability to create Document Workspaces that could help our administrators collaborate with new teams to develop their requirements.  The Request type inherits from Company Document which now has a Site Column called Retention Schedule.

By adding the New Site Request template to the Site Collection’s document library and setting it as the template in the New Site Request Content Type, we can pre-select the Retention Schedule used for all New Site Requests.  Usually, lookups don’t work too well in the bod y of Word documents because the Content Control displays the integer ID value of the target lookup object instead of the column specified (or the column shown in the DIP–Document Information Panel).  However, when setting a Lookup value in the template, new instances of documents based on that template will display the proper Lookup value in the document body.  In our case, we wanted to note the document retention schedule in the document control footer.

In addition, because we used a Lookup data type for the document retention schedule, users can drill down into the definition of that schedule.  By clicking the document in the library and selecting View Properties, the user can see the Retention Schedule identifier (e.g., S1-020).  This code links to the Retention Schedule record.  That record references the disposition method (by another Lookup column).  By clicking that, the user can see exactly what process to follow for record disposition (e.g., destroy with permission).

We now know that we will need to design the architecture of our system in two ways:  Information Architecture (how the Content Types and metadata work to organize information) and Storage Architecture (how the lists and libraries aid in managing workflow, security permissions, and reporting). 

I identified three models of Storage Architecture which I documented for an article appearing on EndUserSharePoint.com soon.  I’ll address how these models come into play for Document Management in the next installment.