Monthly Archives: September 2016

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!