среда, 24 июня 2015 г.

SharePoint 2013 CSR с добавлением Сallout меню

Сегодня и, наверное, в будущем мы будем больше говорить о мире JavaScript т.к. это более релевантная тема в нашей текущей ситуации, на данный момент мир SharePoint переживает очень большие изменения, и мы меняемся вместе с ним. Несколько лет назад, перед тем как начать плотно изучать JavaScript, я постоянно был по уши затянут в бэкенд составляющую SharePoint, постоянно пытался сделать красивое решение из километров серверного кода. Но сейчас, все очень кардинально поменялось, для одних шок и паника, для других глоток свежего воздуха. Я не буду давать совет, просто скажу, что лично для меня это стал реально воздух. Теперь я могу нормально разделять бэкенд и фронтенд, без всяких SPService (ничего не имею против, в 2010 версии он правда помогал), но сейчас можно отдать на клиента больше логики чем раньше, а на вход иметь только API свое или штатное. Если вам будут по душе данная тематика по CSR, то можно будет продолжить в рамках серии постов "Путь на клиента".
Очень давно узнал о нашем всеми полюбившимся CSR и пристрелявшись с данным видом рендеринга через JavaScript, могу сказать, что с ним конечно можно очень много бизнес задач решить и сегодня приведу еще небольшой пример использования CSR вместе с callout меню, чтобы добиться более динамического UI.

Сегодня сценарий будет простой, и вы сами в будущем сможете добавить в него все что хотите. Сначала небольшая предыстория, почему же вставлять callout меню? Я для себя отметил, что если вы делаете свой рендеринг и при этом не хотите ограничиваться отображением из одного списка (Если у вас более сложная архитектура списков), и пытаетесь делать на этапе (пост) рендеринга запросы к API в асинхронном режиме то вас ждут разочарования т.к. рендеринг делается синхронно и, если вы хорошо знакомы с JQuery вы можете делать все это в Defered методах, но тогда рендеринг будет медленный т.к. он будет ждать пока все выполниться. Тут есть и вторая сторона медали, использовать пост-рендеринг и в момент, когда идет цикл по элементам наращивать массив с нужными атрибутами для дальнейшей постобработки, тут теряется красота т.к. сначала будут один вид, потом он внезапно меняется.
Вот тут и приходит на помощь наш Callout меню,

Все ниже сценарии мы будем разбирать как нам сделать без Visual Studio, чтобы охватить большую аудиторию. Для того, кого будет интересен проект, может писать мне в личку.

1) Создадим списки

Список Trainings - список тренингов, список Participants - список записавшихся на тренинг. Все детали как и кто записывается опустим, цель у нас другая.

Список Training:
  • Training Name (ex. Title)
  • Description
  • Participants - Lookup на список Participants с Related Count на поле Training, чтобы отображать количество участников.
Список Participants:
  • Title (не используем)
  • Participant - тип Text (для примера будет достаточно)
  • Training - Lookup на список Trainings

2) Создадим наш первый тренинг и подпишем на него нашего первого участника.

Дальше создадим простую страницу веб-частей и добавим наш список тренингов с представлением по-умолчанию.
Дальше, для работы нам понадобиться SharePoint Designer 2013 (Далее SPD), если вы сразу хотите перейти к результату, то ближе к концу поста поста будет исходный JavaScript нашего финального csr.
Откроем наш сайт в SPD и проследуем в папку scripts, если ее нету, то создаем.
Создадим файл JavaScript trinings.csr.js

Начнем мы с переопределения rendering template (SPClientTemplates.TemplateManager) рендеринга, базовая конструкция у него следующая:

* Более подробнее по csr, можно почитать у Андрея Маркеева тут.

В итоге у нас получится вот такая конструкция:
function overrideItems() {
  // создание и инициализация объектов для переопределения контекста 
  var itemsOverride = {};
  itemsOverride.BaseViewID = 1;
  itemsOverride.ListTemplateType = 100;
  // initialize template  
  itemsOverride.Templates = {};
  itemsOverride.Templates.Header = allItemsRenderHeader;
  itemsOverride.Templates.Footer = allItemsRenderFooter;
  itemsOverride.Templates.Item = allItemsRender;

  // регистриация нашего темплейта для переопределения в менеджере 
  SPClientTemplates.TemplateManager.RegisterTemplateOverrides(itemsOverride);
}
(function () {
  overrideItems();
})();

function allItemsRenderHeader(ctx) {


}
function allItemsRenderFooter(ctx) {

}
function allItemsRender(ctx) {
 
}

3) Приаттачим наш пустой csr, к представлению списка





4) Теперь добавим наши методы и функции, вот в таком виде:

var trainingIds = [];
var trainingElementsIds = [];

function overrideItems() {
  // создание и инициализация объектов для переопределения контекста 
  var itemsOverride = {};
  itemsOverride.BaseViewID = 1;
  itemsOverride.ListTemplateType = 100;
  // initialize template  
  itemsOverride.Templates = {};
  itemsOverride.Templates.Header = allItemsRenderHeader;
  itemsOverride.Templates.Footer = allItemsRenderFooter;
  itemsOverride.Templates.Item = allItemsRender;

  // регистриация нашего темплейта для переопределения в менеджере 
  SPClientTemplates.TemplateManager.RegisterTemplateOverrides(itemsOverride);
}
(function () {
  overrideItems();
})();

function allItemsRenderHeader(ctx) {
  var header = String.format('<table class="ms-listviewtable"><thead><tr class="ms-viewheadertr ms-vhltr"><th class="ms-vh-icon ms-minWidthHeader"><img border="0" width="16" height="16" src="/_layouts/15/images/icgen.gif"></th><th class="ms-vh2" style="max-width: 500px;"><span>{0}</span></th><th></th></tr></thead><tbody>', 'Trainings');
  return header;

}
function allItemsRenderFooter(ctx) {
  return "</tbody></table>";
}
function allItemsRender(ctx) {
  // Текущий элемент тренинга
  var currentItem = ctx.CurrentItem;
  // LanchPoint для нашего Callout-а
  var trainingCalloutId = '_training_' + currentItem.ID;
  var trainingElement = String.format('<a class="ms-listlink ms-draggable" href="#" dragid="1" draggable="true">{0}</a>', currentItem.Title);
  // Так как мы будем открывать callout по клику надо будет прописать все lanchpoint-ы в массив
  trainingElementsIds.push(trainingCalloutId);
  trainingIds.push(currentItem.ID);

  var training = String.format('<tr><td class="ms-cellstyle ms-vb-icon"><img border="0" src="/_layouts/15/images/icdocset.gif"></td><td class="ms-cellstyle ms-vb-title" isecb="TRUE" iscallout="TRUE" height="100%">{1}</td><td class="ms-list-itemLink-td ms-cellstyle"><div class="ms-list-itemLink" id={2} onclick="openTrainingCallout(this)" cid="{3}"><a class="ms-lstItmLinkAnchor ms-ellipsis-a"><img class="ms-ellipsis-icon" src="/_layouts/15/images/spcommon.png?rev=23" alt="Open Menu"></a></div></td></tr>', currentItem.Title, trainingElement, trainingCalloutId, currentItem.Title);
  return training;
}

var openTrainingCallout = function (el) {
  var cId = el.attributes.cid.value;
  var calloutId = '_call_' + cId;
  openCallout(el, cId, calloutId, cId);
};

var openCallout = function name(launchPoint, cId, calloutId, calloutTitle) {
  //
  var participants = getParticipants(cId);
  participants.then(function (data) {
    if (data.d.results) {
      console.log(data.d.results);
      var results = data.d.results;
      if (results.length != 0) {
        var calloutHtml = "<div>Participants:</div>";
        var index = 0;
        while (index < results.length) {
          var item = results[index];
          calloutHtml += "<div><span>" + item.Participant + "</span></div>";
          index++
        }
        initCalloutForElement(launchPoint, calloutId, launchPoint.attributes.id.value, calloutTitle, calloutHtml);
      }
    }
  });
}

var getParticipants = function (trainingTitle) {
  return $.ajax({
    url: String.format("{0}/_api/web/lists/getbytitle('Participants')/Items?$filter=Training/Title eq '{1}'", _spPageContextInfo.webAbsoluteUrl, trainingTitle),
    type: "GET",
    headers: { "accept": "application/json;odata=verbose" },
    error: function (xhr) {
      alert(xhr.status + ': ' + xhr.statusText);
    }
  });
}

var initCalloutForElement = function (lanchpoint, calloutContainerId, calloutElementId, calloutTitle, html) {
  if (typeof CalloutManager == 'undefined') {
    SP.SOD.executeFunc("callout.js", "Callout", function () {
      CalloutManager.closeAll();
      createCreateCallout(lanchpoint, html, calloutElementId, calloutTitle);
    });
  }
  else {
    var elmTd = getAncestor(lanchpoint, "TD");
    if (CalloutManager.containsOneCalloutOpen(elmTd)) {
      return;
    }
    if (CalloutManager.containsOneCalloutOpen(lanchpoint)) {
      return;
    }
    createCreateCallout(lanchpoint, html, calloutElementId, calloutTitle);
  }
};

var createCreateCallout = function (launchPoint, html, calId, calTitle) {
  var listCallout = CalloutManager.getFromLaunchPointIfExists(launchPoint);
  if (listCallout == null) {
    var option = new CalloutOptions();
    option.ID = calId;
    option.launchPoint = launchPoint;
    option.beakOrientation = 'leftRight';
    option.title = calTitle;
    option.content = html;
    option.contentWidth = 420;
    listCallout = CalloutManager.createNewIfNecessary(option);
    listCallout.open(true);
  }
  else {
    listCallout.toggle();
  }
}

var getAncestor = function (elem, tag) {
  while (elem != null && elem.tagName != tag) {
    elem = elem.parentNode;
  }
  return elem;
};

5) И если мы все сделали правильно, то конечным результатом у нас будет:

А дальше уже можно добавить в данный callout CalloutActionOptions и добавить много интересной логики, но это уже вы сами. Всем спасибо. Если заметили ошибку или появился вопрос пишите в комментариях или в личку.

3 комментария:

  1. Спасибо за статью, полезно.
    для подобных вещей я использовал бутстрап
    http://imgur.com/74fAvec

    ОтветитьУдалить
    Ответы
    1. Спасибо. Я, раньше по бустрапу в SP прикалывался, многие проекты сделал, но как мы все знаем бустрап без бубна не работает корректно в SP. По итогу решил весь UI максимально приближать к by Design

      Удалить
  2. Очень много кода для "элементарных вещей", которые казалось бы не должны вылезать на платформе с поддержкой HTML5 и CSS3, SharePoint - увы реальность!

    ОтветитьУдалить