Показаны сообщения с ярлыком Sharepoint customization. Показать все сообщения
Показаны сообщения с ярлыком Sharepoint customization. Показать все сообщения

четверг, 28 августа 2014 г.

SharePoint 2013. ClientPeoplePicker & Custom forms

Многие уже знают, что в SharePoint 2013 появился новый замечательный элемент управления для работы с данными типа "Пользователь". Выглядит он так:
И именно этот элемент управления, с возможность предлагать возможные значения, отображается во всех формах элементов списка по умолчанию. И это очень хорошо.
Проблемы возникают когда Вы создаете новую форму.
Напомню, что в формах по умолчанию, за генерацию полей отвечает ListFormWebPart. Но если Вы создаете новую форму, то функцию генерации html полей на себя берет DataFormWebPart. При этом каждое отдельное поле генерируется элементом управления FormField. Кастомизировать форму становиться намного легче. Но при этом вместо нового ClientPeoplePicker мы видим старый контрол, без автокомплита и с кнопками проверки пользователя и кнопки для открытия всплывающего окна для поиска пользователя:
Как я уже упомянул, каждое поле генерируется SharePoint:FormField. Я пытался найти возможность "попросить" его вывести именно новый ClientPeoplePicker, но тщетно.
Мы сами можем добавить новый контрол на страницу, для этого добавим на форму такой код:


После этого у нас появится прекрасно работающий ClientPeoplePicker. Вот только нужно его привязать к полю и делать это нужно самому. Я сделал это спрятав основной контрол, который без автокомплита, но только спрятал, с самой формы не убирал. Дальше, используя JavaScript, в методе PreSaveAction() перед сохранением формы брал данные из ClientPeoplePicker и вставлял их в старый PeoplePicker. Что бы мне было удобно работать, я использовал библиотеку SPSservices.
// Заполняем поля в PeoplePicker
var arrUserField = [];
$("#UserFieldCPP_TopSpan span span span[class='ms-entity-resolved']").each(function(index, elem){
    arrUserField.push($(this).text());
});
var strUserField = arrUserField.join(";");

$().SPServices.SPFindPeoplePicker({
 peoplePickerDisplayName: "Сотрудник",
 valueToSet: strUserField ,
 checkNames: true
});
Т.е. перед сохранение формы данные из ClientPeoplePicker копируются в обычный PeoplePicker.
Приведенный код подходит для формы создания элемента, для формы редактирования нам нужно из обычного PeoplePicker'а скопировать в ClientPeoplePicker. Используйте этот код:

// Заполняем поля в ClientPeoplePicker
var UserFieldPP = $().SPServices.SPFindPeoplePicker({
 peoplePickerDisplayName: "Сотрудник" 
});

var UserFieldPPPeopleList = UserFieldPP.currentValue.split(";");
UserFieldPPPeopleList.pop(); 
var picker = SPClientPeoplePicker.SPClientPeoplePickerDict["UserFieldCPP_TopSpan"]

for (i = 0; i < UserFieldPPPeopleList.length; i++) { 
    var name = UserFieldPPPeopleList[i];
 $("#UserFieldCPP_TopSpan_EditorInput").val(name);
 picker.AddUnresolvedUserFromEditor(true)
}
Теперь кастомные формы могут иметь ClientPeoplePicker привязанный к полю в списке.
Код работает в т.ч. для множественных значений. Так же, что бы приведенный выше код работал, ID у ClientPeoplePicker должно быть "UserFieldCPP".

пятница, 6 июня 2014 г.

Доработка SPServices Autocomplete

Я, как и многие, давно использую библиотеку SPServices для решения многих задач.
У этой библиотеки есть функционал по автокомплиту:
Но есть у этого механизма несколько недоработок:
  • нет задержки перед показом вариантов
  • ничего не отображается, если нет вариантов
  • нельзя предлагать варианты действий, если ничего не найдено
Т.е. необходимо поменять поведение, что бы варианты появлялись с задержкой в секунду, если ничего не нашли, то показываем как на картинке:
Тут же даем возможность добавить элемент:
Извещаем о добавлении:
Вот код уже поправленной функции $.fn.SPServices.SPAutocomplete:
$.fn.SPServices.SPAutocomplete = function(options) {

        var opt = $.extend({}, {
            webURL: "", // [Optional] The name of the Web (site) which contains the sourceList
            sourceList: "", // The name of the list which contains the values
            sourceColumn: "", // The static name of the column which contains the values
            columnName: "", // The display name of the column in the form
            CAMLQuery: "", // [Optional] For power users, this CAML fragment will be Anded with the default query on the relatedList
            CAMLQueryOptions: "", // [Optional] For power users, allows specifying the CAMLQueryOptions for the GetListItems call
            CAMLRowLimit: 0, // [Optional] Override the default view rowlimit and get all appropriate rows
            filterType: "BeginsWith", // Type of filtering: [BeginsWith, Contains]
            numChars: 0, // Wait until this number of characters has been typed before attempting any actions
            ignoreCase: false, // If set to true, the function ignores case, if false it looks for an exact match
            highlightClass: "", // If a class is supplied, highlight the matched characters in the values by applying that class to a wrapping span
            uniqueVals: false, // If set to true, the function only adds unique values to the list (no duplicates)
            maxHeight: 99999, // Sets the maximum number of values to display before scrolling occurs
            slideDownSpeed: "fast", // Speed at which the div should slide down when values match (milliseconds or ["fast" | "slow"])
            processingIndicator: "_layouts/images/REFRESH.GIF", // If present, show this while processing
			htmlifnothingfound: "Ничего не найдено!", 
            debug: false // If true, show error messages;if false, run silent
        }, options);

        var matchNum;

		
		
        // Find the input control for the column and save some of its attributes
        var columnObj = $("input[Title='" + opt.columnName + "']");
        columnObj.css("position", "");
        var columnObjColor = columnObj.css("color");
        var columnObjWidth = columnObj.css("width");
		// myAdd Добавляем переменную columnObjPadding
		var columnObjPadding = columnObj.css("padding");
        if (columnObj.html() === null && opt.debug) {
            errBox("SPServices.SPAutocomplete",
                "columnName: " + opt.columnName,
                "Column is not an input control or is not found on page");
            return;
        }

        // Remove the 
 which isn't needed and messes up the formatting
        columnObj.closest("span").find("br").remove();
        columnObj.wrap("
"); // Create a div to contain the matching values and add it to the DOM var containerId = genContainerId("SPAutocomplete", opt.columnName); // myAdd изменен цвет на #2A8DD4 как в 2013 по умолчанию columnObj.after("
"); // Set the width to match the width of the input control $("#" + containerId).css("width", columnObjWidth); // Handle keypresses $(columnObj).keyup(function() { // Get the column's value var columnValue = $(this).val(); // Hide the container while we're working on it $("#" + containerId).hide(); // Have enough characters been typed yet? if (columnValue.length < opt.numChars) { return false; } // Show the the processingIndicator as a background image in the input element columnObj.css({ "background-image": "url(" + opt.processingIndicator + ")", "background-position": "right", "background-repeat": "no-repeat" }); // Array to hold the matched values var matchArray = []; // Build the appropriate CAMLQuery var camlQuery = ""; if (opt.CAMLQuery.length > 0) { camlQuery += ""; } camlQuery += "<" + opt.filterType + ">" + columnValue + "</" + opt.filterType + ">"; if (opt.CAMLQuery.length > 0) { camlQuery += opt.CAMLQuery + ""; } camlQuery += ""; // Call GetListItems to find all of the potential values $().SPServices({ operation: "GetListItems", async: false, webURL: opt.WebURL, listName: opt.sourceList, CAMLQuery: camlQuery, CAMLQueryOptions: opt.CAMLQueryOptions, CAMLViewFields: "", CAMLRowLimit: opt.CAMLRowLimit, completefunc: function(xData) { // Handle upper/lower case if ignoreCase = true var testValue = opt.ignoreCase ? columnValue.toUpperCase() : columnValue; // See which values match and add the ones that do to matchArray $(xData.responseXML).SPFilterNode("z:row").each(function() { var thisValue = $(this).attr("ows_" + opt.sourceColumn); var thisValueTest = opt.ignoreCase ? $(this).attr("ows_" + opt.sourceColumn).toUpperCase() : $(this).attr("ows_" + opt.sourceColumn); // Make sure we have a match... if (opt.filterType === "Contains") { var firstMatch = thisValueTest.indexOf(testValue); if ((firstMatch >= 0) && // ...and that the match is not already in the array if we want uniqueness (!opt.uniqueVals || ($.inArray(thisValue, matchArray) === -1))) { matchArray.push($(this).attr("ows_" + opt.sourceColumn)); } } else { // Handles normal case, which is BeginsWith and and other unknown values if (testValue === thisValueTest.substr(0, testValue.length) && // ...and that the match is not already in the array if we want uniqueness (!opt.uniqueVals || ($.inArray(thisValue, matchArray) === -1))) { matchArray.push($(this).attr("ows_" + opt.sourceColumn)); } } }); } }); // Build out the set of list elements to contain the available values var out = ""; // !myAdd if(matchArray.length==0){ matchArray.push(opt.htmlifnothingfound); } for (i = 0; i < matchArray.length; i++) { // If a highlightClass has been supplied, wrap a span around each match if (opt.highlightClass.length > 0) { // Set up Regex based on whether we want to ignore case var thisRegex = new RegExp(columnValue, opt.ignoreCase ? "gi" : "g"); // Look for all occurrences var matches = matchArray[i].match(thisRegex); var startLoc = 0; // Loop for each occurrence, wrapping each in a span with the highlightClass CSS class for (matchNum = 0; matchNum < matches.length; matchNum++) { var thisPos = matchArray[i].indexOf(matches[matchNum], startLoc); var endPos = thisPos + matches[matchNum].length; var thisSpan = "" + matches[matchNum] + ""; matchArray[i] = matchArray[i].substr(0, thisPos) + thisSpan + matchArray[i].substr(endPos); startLoc = thisPos + thisSpan.length; } } // Add the value to the markup for the container out += "

  • " + matchArray[i] + "
  • "; } // Add all the list elements to the containerId container $("#" + containerId).html(out); // Set up hehavior for the available values in the list element // !myAdd if(matchArray[0] != opt.htmlifnothingfound) if(matchArray[0] != opt.htmlifnothingfound){ $("#" + containerId + " li").click(function() { $("#" + containerId).fadeOut(opt.slideUpSpeed); columnObj.val($(this).text()); }).mouseover(function() { var mouseoverCss = { "cursor": "hand", "color": "#ffffff", "background": "#3399ff" }; $(this).css(mouseoverCss); }).mouseout(function() { var mouseoutCss = { "cursor": "inherit", "color": columnObjColor, "background": "transparent" }; $(this).css(mouseoutCss); });} // If we've got some values to show, then show 'em! if (matchArray.length > 0) { // !myAdd delay(1000) $("#" + containerId).slideDown(opt.slideDownSpeed).delay(2000); } // Remove the processing indicator columnObj.css("background-image", ""); }); };

    Просто замените код из библиотеки SPServices кодом выше.
    Правда теперь и прикреплять автокомплит надо несколько иначе:
    function AddAutoCompleteContractor() {
    	console.log("Начало выполнения функции 'AddAutoCompleteContractor()");
    	//Добавляем autocomplete для поля "Наименование контрагента"
    	$().SPServices.SPAutocomplete({	
    			sourceList: "Contractors",
    			sourceColumn: "Title",
    			columnName: "Наименование контрагента",
    			ignoreCase: true,
    			filterType: "Contains", 
    			numChars: 3,
    			slideDownSpeed: 400,
    			htmlifnothingfound: "Ничего не найдено! <a href='#' onClick='ShowPopupContractor()'>Добавить?</a>", 
    			debug: true
    		});
    	}
    
    Добавили опцию "htmlifnothingfound" - в ней указывайте html, который будет отображаться, если ничего не найдено.

    Так же был изменен цвет, под 2013 и ширину, что бы вровень было. В 2013 padding появился.
    Теперь нам необходимо добавить код, который покажет popup:

    function ShowPopupContractor() {
    	console.log("Начало выполнения функции 'ShowPopupContractor()");
    	$("#Anchor").append("Добавьте контрагента:
    
    
    
    
    
    ");
    	$('#popupDivContractor').show();
    		SP.UI.ModalDialog.showModalDialog({
    			html: document.getElementById('popupDivContractor'),
    			title: "Введите комментарий",
    			allowMaximize: false,
    			showClose: false,
    			autoSize: true,
    			dialogReturnValueCallback: onPopUpCloseCallBackContractor
    		});
    	}
    

    Дальше добавим функцию добавление элемента списка и колбэки, которые отработают при удачном\неудачном завершении процесса создания элемента:
    function createListItemContractor() {
    	console.log("Начало выполнения функции 'createListItemContractor()");
        var clientContext = new SP.ClientContext(currentWebUrl);
        var oList = clientContext.get_web().get_lists().getByTitle('Contractors');        
        var itemCreateInfo = new SP.ListItemCreationInformation();
        this.oListItem = oList.addItem(itemCreateInfo);
        oListItem.set_item('Title', $("#ContractorInput").val());
        oListItem.update();
        clientContext.load(oListItem);
        clientContext.executeQueryAsync(
            Function.createDelegate(this, this.onQuerySucceededСreateListItemContractor), 
            Function.createDelegate(this, this.onQueryFailedСreateListItemContractor)
        );
    }
    
    function onQuerySucceededСreateListItemContractor() {
    	console.log("Начало выполнения функции 'onQuerySucceededСreateListItemContractor()");
        console.log('Элемент создан: ' + oListItem.get_id());
    }
    
    function onQueryFailedСreateListItemContractor(sender, args) {
    	console.log("Начало выполнения функции 'onQueryFailedСreateListItemContractor()");
        console.log('Ошибка при создании элемента. ' + args.get_message() + '\n' + args.get_stackTrace());
    }
    

    Теперь добавим функции, которые закрепим за кнопкой Добавить и Отмена.
    При добавлении будет создавать элемент и закрытие формы, в случае нажатия по кнопке Отмена - только закрытие формы. Важно отметить, что в этих функциях мы сообщаем и результат для колбэка закрытия формы. Именно в колбэке логика по которой мы показываем уведомление.
    function onPopUpCloseCallBackContractor(result, returnValue) {
        console.log("Начало выполнения функции 'onPopUpCloseCallBackContractor()");
    	$("nobr:contains('Наименование контрагента')").closest('tr').find("input").val("");
    	$("nobr:contains('Наименование контрагента')").closest('tr').find("input").keyup()
        if (result == SP.UI.DialogResult.OK) {
            SP.UI.Status.removeAllStatus(true);
            var sId = SP.UI.Status.addStatus("Контрагент добавлен!");
            SP.UI.Status.setStatusPriColor(sId, 'green');
        }
        else if (result == SP.UI.DialogResult.cancel) {
            SP.UI.Status.removeAllStatus(true);
            var sId = SP.UI.Status.addStatus("Вы не стали добавлять контрагента");
            SP.UI.Status.setStatusPriColor(sId, 'yellow');
        }
    }
    
    function closePopupOkContractor() {
        console.log("Начало выполнения функции 'closePopupOkContractor()");
        createListItemContractor();
        SP.UI.ModalDialog.commonModalDialogClose(SP.UI.DialogResult.OK, null);
    }
    
    function closePopupCancelContractor() {
        console.log("Начало выполнения функции 'closePopupCancelContractor()");
        SP.UI.ModalDialog.commonModalDialogClose(SP.UI.DialogResult.cancel, null);
    }
    

    Пример работы:

    понедельник, 28 апреля 2014 г.

    Показать поле "Создан" на форме элемента

    Некоторые поля, например 'Id' или 'Создано' ('Created') невозможно отображать изначально на формах. В SharePoint Designer они в списке столбцов не отображаются. Заказчик просит изобразить на форме примерно следующее:


    Для этого необходимо создать новую форму и добавить в нужное Вам место следующий серверный элемент SharePoint:
    
    

    В итоге у нас получится примерно следующее:
    Не очень красиво, необходимо сменить формат даты. Что бы было еще и на нашем языке.
    Для этого мы с Вами попробует библиотеку momentjs.com

    Для этого ее надо скачать и подключить к странице. Что бы мы могли отобразить время на нашем языке - качать необходимо moment-with-langs.min.js
    Теперь получаем дату в старом формате, меняем и вставляем обратно в страницу.
    Что бы пользователь не видел сам процесс обработки - прячем контейнер со старой датой:

    <tr>
        <td colspan="2">
            <h1>
                Заявка создана:
                <span id="DateCreationLabel" style="color:navy;display:none">
                    <SharePoint:ListItemProperty Property="Created" ID="CreatedDateField" runat="server" />
                </span>
            </h1>
        </td>
    </tr>
    

    Над html выше - выполняем JS ниже:

    var date = $('#DateCreationLabel').text();
        if (moment(date).isValid()) {
            moment.lang('ru');
            date = moment(date).format('LL');
            $('#DateCreationLabel').text(date);
        }
        $('#DateCreationLabel').show();
    
    После выполнения кода видим следующее:
    Вполне приемлемо.