Author Archives: Daniel

ListAttachments for SharePoint updated: v2.5.0

It has been a while since I last worked on this script but now I could finally fix the bug that caused the bubble to disappear right away in Internet Explorer.

Go grab the latest version from the Product Page. and please let me now if it works for you.

Changelog v2.5.0

  1. Fix: The bubble will no longer disappear right away in Internet Explorer

StickyHeaders for SharePoint Update v3.2.0

I have updated StickyHeaders for SharePoint to version 3.2.0. Those are the changes:

v.3.2.0 Changelog

  1. Fix: Fixed a bug where Sticky Headers are not working in Datasheet-view when the user has less than Edit-permission
  2. New feature: Implemented a function that will load jQuery for you if it is not already loaded. No need to load jQuery via a separate script-tag any longer.
    NOTE: Uncomment line 23-29 and remove or comment out line 32-50 if you don’t want this feature
  3. Improvement: Added some semicolons for a better minification performance
  4. Cosmetic fix: Corrected a typo

Big thanks to everyone for the bug reports!

Please go to the product page to download the latest version.

Daniel

Office UI Fabric – Indeterminate Checkbox State

Recently I was playing a lot with the Office UI Fabric front-end framework and in one situation I needed a checkbox to be indeterminate. Up to Office UI Fabric 2.6.1 there are no CSS rules for an indeterminate checkbox, though. There are only CSS rules for the two states Checked and Unchecked and setting the DOM property indeterminate to true has no effect whatsoever.

In case you come across the same issue, here is the fix. Simply add the following CSS rules to your style sheet:

.ms-ChoiceField-input:indeterminate + .ms-ChoiceField-field:before {
  background-color: #666666;
  border-color: #666666;
  color: #666666;
  border-radius: 50%;
  content: '\00a0';
  display: inline-block;
  position: absolute;
  top: 4px;
  right: 0;
  bottom: 0;
  left: 4px;
  width: 11px;
  height: 11px;
  box-sizing: border-box;
}

.ms-ChoiceField-input:indeterminate + .ms-ChoiceField-field:hover:before {
  background-color: #212121;
  color: #212121;
}

.ms-ChoiceField-input[type='checkbox']:indeterminate + .ms-ChoiceField-field:before {
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  display: inline-block;
  font-family: 'Office365Icons';
  font-style: normal;
  font-weight: normal;
  line-height: 1;
  speak: none;
  content: '\e005'; /*instead of \e041 (tick)*/
  background-color: transparent;
  border-radius: 0;
  font-size: 13px;
  top: 3px;
  left: 3px;
}

These rules are the same as the rules for the pseudo property :checked with one small difference in the 3rd rule (look for the comment).

Here are the three different states after adding the CSS rules:

Unchecked:
Checked:
Indeterminate:

If you want to have a different icon for the indeterminate state, you can choose one from here and replace ‘\e005’ with the icon of your choice.

Change default form button redirect – The simple way

There are many questions on sharepoint.stackexchange.com related to redirecting the user to a specific page after form submission. Most answers include A) code that adds a custom PreSaveAction-function which in turn does a window.location = “…” or executes WebForm_DoPostBackWithOptions function with a custom target destination or B) they rely on providing a customized link to the form itself with a modified Source parameter in the query string.

Option A fails at the default form validation and option B is complicated to maintain.

Let’s have a look what happens when a form is submitted. The Save button has this in it’s onclick-attribute:

if (!PreSaveItem()) 
	return false;
if (SPClientForms.ClientFormManager.SubmitClientForm('WPQ1')) 
	return false;
WebForm_DoPostBackWithOptions(
	new WebForm_PostBackOptions(
		"*long save button ID*", 
		"", 
		true, 
		"", 
		"", 
		false, 
		true
	)
)

First, PreSaveAction is executed. This function allows you to write custom validation rules or execute any kind of code before a form is submitted. After that SPClientForms.ClientFormManager.SubmitClientForm is executed, this function validates the form and submits it if the validation is successful. WebForm_DoPostBackWithOptions, a function that allows you to specify your own redirect destination, is actually never reached.

What we know so far is that SPClientForms.ClientFormManager.SubmitClientForm validates the form but you can’t specify your own redirect destination. With WebForm_DoPostBackWithOptions you can specify your own redirect destination but the default form validation is not executed.

Now you could start writing your own validations and put it into PreSaveAction which can be quite cumbersome if you have a complex form. Or, you somehow trick SPClientForms.ClientFormManager.SubmitClientForm into redirecting to a different URL. Let’s have a look into SPClientForms.ClientFormManager.SubmitClientForm to check out how the redirect-destination is determined:

(The corresponding code resides in clientforms.js, the code below is the debug-version and unimportant parts are omitted)

var _ctx = window[qualifier + "FormCtx"];
var redirectInfo = _ctx.RedirectInfo;
var url = redirectInfo != null ? redirectInfo.redirectUrl : '';
var source = GetUrlKeyValue("Source");
source = source == null || source == '' ? GetUrlKeyValue("NextPage") : source;
source = source == null || source == '' ? GetUrlKeyValue("NextUsing") : source;
url = source != null && source != '' ? source : url;
STSNavigate(url); //on-premises SharePoint 2013 
Nav.navigate(url); //SharePoint Online

What is says is that if no Source query string parameter is present then navigate to the URL given in _ctx.RedirectInfo.redirectUrl. The Source parameter may or may not be present but if it present it takes precedence. We don’t want to change this parameter, though, because that would mean that we either have to update the URL programmatically when the form is loaded or that we have to change every link pointing to the form.

But what we can do is changing what STSNavigate/Nav.navigate does.
We don’t really care what those functions do internally, eventually their main purpose is to navigate to the given URL. I have written three different functions that do the job. Choose the one that suits you best:

function changeRedirect(options) {
	for(var i = 0, buttons = document.querySelectorAll(options.selector); i < buttons.length; i++){
		buttons[i].setAttribute(
			"onclick", 
			"Nav.navigate = STSNavigate = function(){"+
			"	window.location = '" + options.redirectTo + "'"+
			"};"+
			buttons[i].getAttribute("onclick")
		)
	}
}

document.addEventListener("DOMContentLoaded", function(event) {
	changeRedirect({
		selector:	"input[value=Save]",
		redirectTo:	"https://google.com"
	})
	changeRedirect({
		selector:	"input[value=Cancel]",
		redirectTo:	"https://bing.com"
	})
})
function changeRedirect(options) {
	for (var i = 0, buttons = document.querySelectorAll(options.selector); i < buttons.length; i++) {
		var newOnClick = function(originalOnClick) {
			return function(){
				Nav.navigate = STSNavigate = function() {
					window.location = options.redirectTo
				}
				originalOnClick()
			}
		}
		buttons[i].onclick = newOnClick(buttons[i].onclick)
	}
}

document.addEventListener("DOMContentLoaded", function(event) {
	changeRedirect({
		selector:	"input[value=Save]",
		redirectTo:	"https://google.com"
	})
	changeRedirect({
		selector:	"input[value=Cancel]",
		redirectTo:	"https://bing.com"
	})
})
NodeList.prototype.changeRedirect = function(redirectTo){
	for (var i = 0; i < this.length; i++) {
		var newOnClick = function(originalOnClick) {
			return function(){
				Nav.navigate = STSNavigate = function() {
					window.location = redirectTo
				}
				originalOnClick()
			}
		}
		this.onclick = newOnClick(this.onclick)
	}
}

document.addEventListener("DOMContentLoaded", function(event) {
	document.querySelectorAll("input[value=Save]").changeRedirect("https://google.com")
	document.querySelectorAll("input[value=Cancel]").changeRedirect("https://bing.com")
})

All three functions change STSNavigate/Nav.navigate depending on which button has been clicked and on default forms this works for the Save/Cancel-buttons below the form as well as the buttons in the ribbon.

Happy SharePointing!

ListAttachments for SharePoint BETA release: v2.5.0BETA2

I released a new BETA version of ListAttachments for SharePoint 2013 and SharePoint Online.

Changelog BETA1:

  • Almost completely rewritten to make it more robust and efficient
  • Fixed the issue in IE where the bubble pops up and disappears immediately
  • Changed from SOAP to REST to minimize the payload and speed up the whole process of retrieving the required data from the server
  • Lots of other little fixes and improvements

Changelog BETA2:

  • Fixes a bug where the script fails for Read-Only users because SP.Ribbon.js is initialized too late

You can get the latest version on the Product Page. If you find any bugs, I would appreciate if you’d report them in either the comment section or the forum. Thanks!

Get all attachments of a particular list item via REST and with minimal overhead

In this post I want to show you how I retrieve the attachments of a particular list item in my ListAttachments-script.

In order for this code to work you will need jQuery since we are are using jQuery’s Ajax-function.

Check out the code below:

<script>
function getAttachments(listName, itemId){
  var attachments = {
    count: 0,
    items: []
  };
  jQuery.ajax({
    url: L_Menu_BaseUrl + "/_api/web/lists/getbytitle('" + listName + "')/items('" + itemId + "')/AttachmentFiles",
    method: "GET",
    async: false,
    headers: { "Accept": "application/json; odata=nometadata" },
    success: function (data) {
      jQuery(data.value).each(function (i) {
        attachments.items.push({
          extension: this.FileName.substring(this.FileName.lastIndexOf('.') + 1),
          name: this.FileName,
          path: this.ServerRelativeUrl
        });
        attachments.count++;
      });
    }
  });
  return allAttachments;
}

console.log(getAttachments("Custom List", 1));
</script>

What’s happening here is that we have a function called getAttachments which takes two arguments, listName and itemId.

This function will simply retrieve a JSON object by sending a GET request to
L_Menu_BaseUrl + “/_api/web/lists/getbytitle(‘” + listName + “‘)/items(‘” + itemId + “‘)/AttachmentFiles”.
L_Menu_BaseUrl is a global variable on your SharePoint-site that contains the relative path of the current site.

This script also sends odata=nometadata in the header which will reduce the amount of data that is sent back by roughly 50% by omitting a lot of meta data (which isn’t required in my case anyway).

I’m also using a synchronous API call (“async: false”) since the rest of my script depends on the returned value of that function.

Here is what is returned by the server when this code is executed on an item with 3 attachments:

{
    "value": [
        {
            "FileName": "attachment1.doc",
            "ServerRelativeUrl": "/Lists/Custom List/Attachments/1/attachment1.doc"
        },
        {
            "FileName": "attachment2.pdf",
            "ServerRelativeUrl": "/Lists/Custom List/Attachments/1/attachment2.pdf"
        },
        {
            "FileName": "attachment3.jpg",
            "ServerRelativeUrl": "/Lists/Custom List/Attachments/1/attachment1.jpg"
        }
    ]
}

In order to make the returned object a bit easier to work with, I then put all the information into my own object that has an attachment-counter and an array of objects that contain the file-extension, name and path of each attachment.

And that’s the content of the final object which is returned by the getAttachments-function:

Object {
	count: 3,
	items:[	Object { extension="doc",  name="attachment1.doc",  fullPath="/Lists/Custom List/Attachments/1/attachment1.doc"}, 
		    Object { extension="pdf",  name="attachment2.pdf",  fullPath="/Lists/Custom List/Attachments/1/attachment2.pdf"}, 
		    Object { extension="jpg",  name="attachment3.jpg",  fullPath="/Lists/Custom List/Attachments/1/attachment3.jpg"}]
}

StickyHeaders for SharePoint Update v3.1.1

I forgot to write a post when I released v3.1.0 so this post lists the changes made in v.3.1.0 as well as v.3.1.1 of StickyHeaders for SharePoint.

v.3.1.0 Changelog:

  1. Fix: The header doesn’t adjust to screen resizing
  2. Fix: The header suddenly disappears when scrolling through a grouped list
  3. Fix: Lists which have the Newsletter or Shaded style or are rendered on the server are not correctly displayed when sticky headers are displayed
  4. Improved: Some minor cosmetic issues

v.3.1.1 Changelog:

  1. Fix: Sorting in quick edit mode when the list is scrolled down works properly now
  2. Fix: Strict mode will not cause an error with this script anymore

Please go to the product page to download the latest version.

Daniel

I’m looking for a new job

I’m currently looking for a new job, preferably in the Asia-Pacific region but I would consider other regions as well. If you are hiring or have any leads, I would appreciate if you would drop me a message via the contact form, e-mail (daniel(at)spoodoo.com), Twitter, or LinkedIn.

Daniel

StickyHeaders for SharePoint Update v3.0.1

This is only a very minor update which fixes some speed issues regarding the appearance of the sticky header for custom lists and document libraries with many columns. Starting with this version, I will also start to use the concept of semantic versioning.

Please go to the product page to download the latest version.

Daniel