| ProfileAutoSpongePhotosBlogLists | Help |
|
August 29 FF and IE just can't get along: javascript event madnessI 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>
August 28 Conditional Validaton with JavascriptWhile 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> August 20 Print a Web Part with JavascriptAdd 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. August 18 EndUserSharePoint.com: The Fundamentals of Libraries and ListsAnyone 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.
August 13 Fixing ExpGroupBy in Core.js - International MOSS Community Comes ThroughI'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:"> 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 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>
July 28 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>
July 22 Advanced Search Form and Preselected ScopesAdvanced Search pages don't seem to do what they're told. If you want to narrow the Advanced Search results to a specific scope:
Now you can check the box to keep advanced results in the scope(s) you choose. To preselect all check boxes:
<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> July 15 Pop Quiz - Content Types and FoldersSharePoint Quiz! Scenario:
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. July 02 Cleanup Hyperlinks in Calculated ColumnsA 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/"; 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>
<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>
June 26 Road to Document Management pt4 -- Configuring Content TypesAfter 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
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. June 17 Focus on EndUserSharePoint.comIf you live in a hole, you may not know about the wonderful things going on at EndUserSharePoint.com (EUSP). Before I met Mark Miller at Bob Mixon's 5-day SharePoint workshop, I visited EUSP frequently. Since meeting Mark, I wrote a few end user-focused articles for his site:
I'm currently working on an article for EUSP which I hope will be available this week or early next week on Performance Management--a topic near and dear to my heart. In the past, I did not cross-post between this blog and EUSP and I plan to keep it that way. I think Mark and the EUSP community have a great thing going and if I write a popular article, I don't want to compete with them in the search bot rankings. However, I will continue to publish administrator-focused content here. So, subscribe to the EUSP weekly newsletter and grab the RSS feed to read my good stuff! June 10 Roadmap to Document Management - pt3: Full Circle - From Documents to HTML Forms to Documents AgainWe kicked off, we prepared the team, we educated ourselves, then we stopped dead in our tracks. I left off this series with Step 3, looking ahead to Step 4... An old file share full of documents: Step 3 (CURRENT) - High-level identification of content and project documentation Step 4 (PLANNED) - Continue content analysis SMEs must identify any subtypes within a filetype group. The high level identification went well. We knew what the key business drivers were and what we would need to incorporate first. Exactly how to implement those important documents was another story. Here's a short list of options:
The team was overwhelmed. The capability to turn every document into an online form meant that a lot of serious thinking needed to take place. We needed to consider document life cycle, workflow optimization, and information architecture for every document! Even the ones we knew nothing about. But with recent State budget cuts, I needed to ensure that any solution we presented would not need a third-party add-on, a team of consultants, or additional servers to make it functional. Sure, we could design the most complete information ontology imaginable but if the users couldn't adapt to it, we would fail. How do users use? I did more testing into client application integration and talked to the client's workers. I needed to find the comfortable medium between accessible data (for decision-makers) and accessible knowledge repositories (for the workers). And both groups needed something easy to manage, maintain, and change. Outlook quickly became my focus for convergence. Our workers know one way to communicate with the computer. They use email. They don't use IM, blogs, twitter, facebook, or RSS [yet]. When they check their email, they use Outlook or OWA (but mostly Outlook). To get the highest adoption rate, the smallest learning curve, and therefore the best success rate, we needed to focus on surfacing data in Outlook. Without 3rd-party tools, which we can't afford right now, our options for Outlook integration were few: Calendar, Discussion, Contact, Task, or Document. Calendar is nice because you can spawn meeting workspaces from it and you can display calendar items in a Gantt view. Contacts are helpful, but have limited application as documents. Tasks do some of the same neat things as calendar events but both have some major drawbacks. Extending the schema of a calendar or task-based content type does not integrate with Outlook. You can see all the data with an RSS feed, but you're not launching the client app, the display is rather dull, and our users don't currently use RSS. Full Circle: So that leaves documents as the best vehicle to implement a majority of our forms. InfoPath seemed like a natural choice (without Enterprise server's Forms Server, again due to cost). However, I was sorely disappointed when I realized that emailing an InfoPath form instance (like Form.xml) and using the Submit in Email function of InfoPath have very different results. InfoPath in a synchronized library as a form or document does the same thing: But InfoPath forms delivered as emails look awesome: InfoPath forms that are distributed by Submit look great in Outlook. You can edit the form which launches the client app, and then resubmit it. But putting those instances in a form library and using Connect to Outlook only shows an Outlook object with Form.xml attached and there is no file previewer for InfoPath forms. Sorry, InfoPath just isn't mature enough for us to use without the Forms Server. The Good News The team will probably be relieved that our mission now has less to do with re-engineering processes and more to do with creating content types for current document templates. If we do a good job with the metadata, then as the templates evolve, few if any changes will be needed in the information architecture. Since we haven't tested our backup system yet, planning to have multiple offline copies in a user's .PST file could be a great benefit if we have a disaster we can't recover from (knock on wood). We reduced the load on our training department. We still get versioning, basic approval workflows, and a central location for our files with a variety of access points. And our email system will still get a break from all of those attachments people love to forward. The trick now will be to get those wonderful documents to give up their metadata and make it something we can surface in reports. June 09 Write Better, Write Faster Using CQWP and dataTransferImagine you work in a place where everything you write can be made public, sifted out of context, analyzed with a microscope, and used against you. This could be a public or government institution, law office, or publicly held company. In any case, your institution has (or probably should have) approved language for specific activities such as termination letters, performance reviews, RFP responses--basically anything HR-related or public-facing. In SharePoint, we have the capability of adding this "boilerplate language" to a list for quick and easy reuse. But how do you use it with MOSS forms? You can't realistically use query strings to pre-populate the input boxes, and putting all that text on the same page for copy-paste could look messy. Using event.dataTransfer, some javascript, and some XSL-fu in our CQWP, we can make images represent our "common language" and drag these to text boxes. Ingredients:
<xsl:template name="Evals" match="Row[@Style='Evals']" mode="itemstyle"> <xsl:variable name="SafeLinkUrl"> <xsl:call-template name="OuterTemplate.GetSafeLink"> <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/> </xsl:call-template> </xsl:variable> <xsl:variable name="SafeImageUrl"> <xsl:call-template name="OuterTemplate.GetSafeStaticUrl"> <xsl:with-param name="UrlColumnName" select="'ImageUrl'"/> </xsl:call-template> </xsl:variable> <xsl:variable name="DisplayTitle"> <xsl:call-template name="OuterTemplate.GetTitle"> <xsl:with-param name="Title" select="@Title"/> <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/> </xsl:call-template> </xsl:variable> <xsl:variable name="LinkTarget"> <xsl:if test="@OpenInNewWindow = 'True'" >_blank</xsl:if> </xsl:variable> <script type='text/javascript' language='javascript' src='/Pages/dragAlt.js'></script> <div id="linkitem" class="item"> <xsl:if test="string-length($SafeImageUrl) != 0"> <div class="image-area-left"> <img class="image" src="{$SafeImageUrl}" alt="{@Description}"> <xsl:attribute name="ondragstart"> javascript:fnSetInfo(this.alt) </xsl:attribute> <xsl:attribute name="onmouseover"> javascript:this.style.cursor='pointer' </xsl:attribute> </img> </div> </xsl:if> <div class="link-item"> <xsl:call-template name="OuterTemplate.CallPresenceStatusIconTemplate"/> <a href="{$SafeLinkUrl}" alt="Edit This Rating" target="{$LinkTarget}" title="{@LinkToolTip}"> <xsl:value-of select="$DisplayTitle"/> </a> </div> </div>
</xsl:template> We're doing a couple things here to the XSL. First, we moved Comments into the ALT attribute of the IMG. Next, we called our script and added some events to image element that would fire our javascript and change the cursor when we hover over it. When your page loads, check out the list: June 02 You Want Fries With That? -- DataView Reports and One-click Email Results ListSometimes even us non-developers feel like order-takers. And while we certainly feel accomplished and appreciated when filling those orders, too many orders or orders complex beyond our capabilities only frustrate us. I recently created a site collection for registering courses. The user managing that list had some feedback, all positive, about how we can make it better. One suggestion was to create a simple way to communicate with each group of classmates. For instance, the instructor gets sick and you need to quickly cancel the class--send an announcement email. Great idea! How the heck am I going to do that? First, a little background. SPD is the devil. It's great for some things and I used it all the time when I started in MOSSland; but I have since avoided SPD like herpes (STD--see the similarities?). While workflow probably could do something for me, I wasn't willing to even consider it. These "messages" to the course participants were going to be sent at varying times with a variety of different messages. Simply put, not much would be repeatable. In my previous version of a registration project, I used permission groups for each class. From the UserList page, we have built-in javascript functionality similar to what my user wants to see. While this was great for a large cohort, multiple classes with a different make-up every time could be a nightmare to manage in permission groups. So that's out. Purposefully avoiding workflow, I delved into SPD for the dataview web part (swearing that I would immediately export the web part and continue to avoid SPD). I created a Zone on a blank ASPX page (set the zone properties to hoizontal layout) , added my master page, and dropped in two dataview web parts. The first web part used Course Events as a data source. The second used my Course Registrations. I opened the Web Part Connections on one of the dataview web parts and used a Send Row of Data/Filter Values action. I mapped Title (from Courses) to Course Title (in Course Registrations) and set the web part to create a hyperlink on Title with the "indicate current selection using: Title" checked. Then I trimmed down the columns from each dataset to only include Course Events.Title and Course Registrations.Created By. This created a functional page where selecting a Course Event makes that Course Event Title bold and only shows the list of registrations--this is a basic Master-Child data display. Next I needed to work on my "one click" functionality. I used my trick from a previous blog to filter @Author down to the SIP: <xsl:variable name="apos">'</xsl:variable> <xsl:value-of select="concat(normalize-space(translate(substring-before(substring-after(@Author,'sip='),'id='),$apos,'')),';')" disable-output-escaping="yes"/> This turns the normal created by HTML: <nobr><span><A HREF="/sites/site_name/_layouts/userdisp.aspx?ID=489">Smith, John</A><img border="0" height="1" width="3" src="/_layouts/images/blank.gif"/><a href='javascript:' onclick='IMNImageOnClick();return false;' class='ms-imnlink'><img name='imnmark' title='' border='0' height='12' width='12' src='/_layouts/images/blank.gif' alt='No presence information' sip='j.smith@somecompany.com' id='imn_1259,type=smtp'/></a></span></nobr> Into simple text: j.smith@somecompany.com; Next, I added an ID attribute to my <table> holding all of these email addresses so I could find them with javascript. The part in green is what I added, the rest should appear similar if not exactly the same for your web part: <xsl:when test="$dvt_IsEmpty"> <xsl:call-template name="dvt_1.empty" /> </xsl:when> <xsl:otherwise><table id="emails" border="0" width="100%" cellpadding="2" cellspacing="0"> <tr valign="top"> <xsl:if test="$dvt_1_automode = '1'" ddwrt:cf_ignore="1"> <th class="ms-vh" width="1%" nowrap="nowrap"></th> </xsl:if> I also needed to put an ID on the cell holding the current selection from Course Events. For this, I just copied and pasted the normal attribute change that SPD creates and made some changes: <xsl:attribute name="style"> <xsl:if test="$CurrentRowKey = $dvt_curselkey">font-weight: bold;</xsl:if> </xsl:attribute> <xsl:attribute name="id"> <xsl:if test="$CurrentRowKey = $dvt_curselkey">current</xsl:if> </xsl:attribute> Then I added the javascript to my page that could parse out the cell contents of my table, put the addresses together in an array, create the mailto: tag and set the subject of the current selection: <asp:Content id="Content1" runat="Server" contentplaceholderid="PlaceHolderMain"> <script type="text/javascript" xmlns:ddwrt2="urn:frontpage:internal"> function mailIt () { window.location = "mailto:&bcc="+getBasicEmails()+"?subject="+getCurrent(); //only using BCC on purpose } function getBasicEmails() { var elm=document.getElementById("emails"); var arr = []; repeat(elm); function repeat(elm) { var tagName = elm.tagName.toLowerCase() var isCell = tagName=="td" var k=0 var child; if(isCell) arr.push(elm.innerText); else while(child=elm.children[k++]) repeat(child); } return arr.join(''); } function getCurrent() { var current = document.getElementById("current"); return current.innerText; } //--> </script>
<asp:Button runat="server"
Text="Email This
List" id="Button1"
OnClientClick="mailIt()"/>
Now when someone navigates to the page, selects an event, and clicks the button (in IE) an email message is started with my parameters. Not sure why it doesn't fire in FF--anyone know? May 29 Stop Messing With My Metadata! -- Locking Down Form Fields With JavascriptThis blog assumes you are familiar with techniques to add javascript to a SharePoint page (specifically script that pre-populates fields in a form based on query string parameters), such as in this previous blog. In this example, I have a Course (based on Custom List Item) and a Course Event (based on Event--it's a hidden type but you can work with it) which is an instance of a Course. I also have a list of Course Registrations where people "sign up" for courses. To make reporting easy, I wanted to have both the Course and the Course Event in the Course Registration item's metadata. The instructions on the page were clear (in my mind): "If this is the course you want, click OK." But some people took that to mean they should change the drop down menus and click OK. All that does is raise doubt about which course they actually wanted and cause my reports (standard views with Group By) to be inconsistent. So, how can we lock these fields down? First I tried to use the element attribute disabled. That disabled the form fields, but when the form submitted, the data fields were empty (or using the first option). So I needed a way to keep the values from my query string even if someone changed the field. The answer I came up with is a mash-up of the tested fillDefaultValues function and some new javascript to add events (like onchange) to the form elements. Areas in green are the old fillDefaultValues function. <script type="text/javascript"> _spBodyOnLoadFunctionNames.push("fillDefaultValues"); function fillDefaultValues() { var qs = location.search.substring(1, location.search.length); var args = qs.split("&"); var vals = new Object(); for (var i=0; i < args.length; i++) { var nameVal = args[i].split("="); var temp = unescape(nameVal[1]).split('+'); nameVal[1] = temp.join(' '); vals[nameVal[0]] = nameVal[1]; } setLookupFromFieldName("Course", vals["CourseID"]); setLookupFromFieldName("Course Event", vals["EventID"]); switchback("Course"); switchback("Course Event"); } function setLookupFromFieldName(fieldName, value) { if (value == undefined) return; var theSelect = getTagFromIdentifierAndTitle("select","Lookup",fieldName); if (theSelect == null) { var theInput = getTagFromIdentifierAndTitle("input","",fieldName); ShowDropdown(theInput.id); //this function is provided by SharePoint var opt=document.getElementById(theInput.opt); setSelectedOption(opt, value); OptLoseFocus(opt); //this function is provided by SharePoint } else { setSelectedOption(theSelect, value); } } function setSelectedOption(select, value) { var opts = select.options; var l = opts.length; if (select == null) return; for (var i=0; i < l; i++) { if (opts[i].value == value) { select.selectedIndex = i; return true; } } return false; } 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; } //This function adds an onclick event to the drop down arrow image that fires fillDefaultValues function getDropDownImg () { var downArrow=document.getElementsByTagName('img') for(var i=0; i < downArrow.length; i++) { if(downArrow[i].alt ==('Display lookup values')) { downArrow[i].onclick = fillDefaultValues; } } } //This function adds an event that fires fillDefaultValues function switchback(fieldName){ var switchSelect = getTagFromIdentifierAndTitle("select","Lookup",fieldName); if (switchSelect == null) { var switchInput = getTagFromIdentifierAndTitle("input","",fieldName); switchInput.onmouseup = fillDefaultValues; getDropDownImg(); } else { switchSelect.onchange = fillDefaultValues; } } //--> </script> You'll notice, just like the fillDefaultValues function, we add parameters for our fields. Since fillDefaultValues is being "pushed," these parameters also push our new javascript that adds the new event. In my testing, only IE seemed to change long select lists into inputs. Additionally, the onchange event that MOSS has coded already [onchange="HandleChange()"] doesn't seem to like being overridden so I had to use onmouseup (other events already active in the input element are onfocusout, onkeypress, and onkeydown). Also, because the image element is not really part of the input element, we had to override its event [onclick="ShowDropdown] with our own in the getDropDownImg function. This was tested in IE, FF, and even Safari with great results. You can't "accidentally" change the metadata the page loads with based on the query string. Lastly, we don't want our friendly users mucking with the metadata on the EditForm.aspx page either (but we still want them to be able to delete the item). But my research into the disabled attribute was not in vain. I created another javascript function that is added to disable the select and input elements. Make sure you specify the elements. If you try to disable all input and select elements, you can't access ANY controls, including those in the page's configuration panel. <script type="text/javascript"> _spBodyOnLoadFunctionNames.push("disableForm"); function disableForm () { var disableInput=document.getElementsByTagName('input') var disableSelect=document.getElementsByTagName('select') for(var i=0; i < disableInput.length; i++) { if(disableInput[i].title == "Course" || disableInput[i].title == "Course Event") { disableInput[i].disabled = true; } } for(var i=0; i < disableSelect.length; i++) { if(disableSelect[i].title == "Course" || disableSelect[i].title == "Course Event") { disableSelect[i].disabled = true; } } } //--> </script> As you can see, I was a little sloppier with this and there are two places where each field name needs to be added (since at any time either list could exceed 20 items and switch from a select element to an input). The end result looks like this: Another approach I just thought of includes redirecting the user from the EditForm to the DispForm or change the list definition in SPD. My MOSS Site Looks Great In Safari -- I feel dirtyOn my way to a meeting at the Library, I stopped to check a few things I was working on last night (and it's good that I did because I forgot to publish the new ItemStyle.xsl file for my site--d'oh!).
The Library's public computers are all Mac's with Safari. I reluctantly fired up a browser window. Since my MOSS site uses Basic Auth over SSL, I figured even the infamous Safari would authenticate me but I wasn't expecting... this. My dropdowns, my administrative functions, my CSS--everything looked great! My new javascript (to be blogged later today) even worked perfectly; which really surprised me since I had to code for differences in FF and IE but never tested Safari or Mac. So, while I'm pleasantly surprised and somewhat proud that I no longer need to hide from the Mac users or Open Source proponents, I feel dirty. May 27 Want to Know How Good You Are?If you want to know how good you are at creating self-sustaining SharePoint sites, take a vacation. Build your site collection, configure the needed administrative functions, train your users, and leave. Well, you can't just leave, can you? Fine. Setup a special list to take help requests (obviously all but the infamous, "I can't log in") while you're gone. Give users a handy link to the list's NewForm.aspx page (you may even want to put this link in the .master pages and/or custom error page). For bonus points, add a redirect back to their current page and pre-populate the form with the referrer page. Use the Alert Me feature on the list to send yourself daily updates of any issues--I think a variation on the Task List works best. If you have a backup administrator (you're lucky), make sure the Status metadata column is populated by the time the Alert goes out. That way, you know what is and is not getting done by your backup(s). Check your alerts daily (or have them sent to a mobile device). If all goes well, you'll gain confidence with each disasterless day. Now, try to enjoy that vacation! May 14 Planning for End Users: Connect to OutlookPart of planning for a SharePoint deployment is planning for people to use (or ask to use) many of the features it offers. A great feature available to end users is the "Connect to Outlook" action. For many end users, seeing important data refreshed in Outlook saves time, keeps them more informed, and allows traveling employees to synchronize information for offline use. What you may not realize is that all of those Custom Lists you made with your fancy content types will NOT be available using the OOB action. You would need a third-party or custom tool to connect to that data. Only the following list and library types allow the Connect to Outlook action:
So, knowing this information, you may decide that customer invoices should be InfoPath forms instead of Custom List items so your traveling sales force can take them offline and the customers list should be based on a Contact List content type. Subtle changes in your information architecture can have a significant impact of your users. <edit> When connecting to a document library, only the following file types will be available for preview:
May 13 Reminder: RSS Needs a Publishing PageAfter getting my customized CQWP just right, I turned on RSS and saw a glorious "Unknown Error." (#$&*^#!) Remember to put your fancy CQWP on a publishing page then add RSS to it (Modify Shared Web Part > Presentation > Enable feed for this web part). IIS Virtual Directory Didn't Inherit Changes to AuthenticationIn my MOSS environment, I'm using Basic Authentication over SSL because the users are decentralized--virtually no one logs into the domain. With Basic Auth, I can set a default domain and therefore people just need to remember their account name and password. Until recently, I was getting an error on subsites that caused images to not load from /_layouts/images on system setting pages. I would get a password challenge for every single image before the page would finish loading--very annoying. Other pages worked fine, it just seemed like things touching Application.Master. So, I started there and also checked the content pages (like AreaNavigationSettings.aspx) but I couldn't find anything strange that would cause this. After all, the path worked because the page loaded, the only difference was /images. Then I peaked into IIS. Changing IIS Authentication and access control settings for the Web Application DID NOT update the settings for the virtual directories (images and inc), it only updated the directories (like 1033, Styles, Mobile, etc). When I changed from Integrated to Basic (to match the Web Application) the problem went away. |
|
|