понедельник, 20 октября 2014 г.

Выделение цветом важных данных… в JSGrid!

Пока дефинишены пишутся, решил написать про некоторые простые методы работы с JSGrid, на примере подсветки данных в гриде.

Также в статье приводится довольно много сведений общего плана про JsGrid, которые очень важны для понимания, как это все работает.

SP.JsGrid.JsGridControl


Главной "входной точкой" любых кастомизаций JsGrid является контрол SP.JsGrid.JsGridControl. У него множество различных методов – >135 публичных методов, которые позволяют контролировать буквально каждый аспект грида.

Главный вопрос конечно вот какой – как получить объект JsGridControl из уже существующего грида режима Quick Edit? Оказывается, довольно легко!

Перейдите в консоль браузера и напишите $get(‘spgridcontainer_WPQ2’).jsgrid – в большинстве ситуаций (когда у вас только одна веб-часть на странице), этот код вернет нужный объект JsGridControl.

Из CSR кастомизации, то же самое делается следующим кодом:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    OnPostRender: jsGridCustomize
});


function jsGridCustomize(ctx) {
    if (ctx.enteringGridMode || !ctx.inGridMode)
        return;

    var g = $get('spgridcontainer_' + ctx.wpq).jsgrid;

}

Этот же код в отладчике:

image

Ну все, теперь можно с этим объектом что-нибудь сделать…

Колонки JsGrid


Есть несколько способов подсветки JsGrid, но сегодня я буду использовать способ, связанный с кастомизацией колонок. Вот какие у JsGridControl есть методы по работе с колонками:

/** Show a previously hidden column at a specified position.
    If atIdx is not defined, column will be shown at it's previous position. */
ShowColumn(columnKey: string, atIdx?: number): void;
/** Hide the specified column from grid */
HideColumn(columnKey: string): void;
/** Update column descriptions */
UpdateColumns(columnInfoCollection: ColumnInfoCollection): void;
GetColumns(optPaneId?): ColumnInfo[];
/** Get ColumnInfo object by fieldKey
    @fieldKey when working with SharePoint data sources, fieldKey corresponds to field internal name */
GetColumnByFieldKey(fieldKey: string, optPaneId?): ColumnInfo;
/** Adds a column, based on the specified grid field */
AddColumn(columnInfo: ColumnInfo, gridField: GridField): void;

Как видно из кода, тип, представляющий колонку – это SP.JsGrid.ColumnInfo.

Давайте посмотрим, что он из себя представляет:

export class ColumnInfo {
    constructor(name: string, imgSrc: string, key: string, width: number);
    /** Column title */
    name: string;
    /** Column image URL.
        If not null, the column header cell will show the image instead of title text.
        If the title is defined at the same time as the imgSrc, the title will be shown as a tooltip. */
    imgSrc: string;
    /** Custom image HTML.
        If you define this in addition to the imgSrc attribute, then instead of standard img tag
        the custom HTML defined by this field will be used. */
    imgRawSrc: string;
    /** Column identifier */
    columnKey: string;
    /** Field keys of the fields, that are displayed in this column */
    fieldKeys: string[];
    /** Width of the column */
    width: number;
    bOpenMenuOnContentClick: boolean;
    /** always returns 'column' */
    ColumnType(): string;
    /** true by default */
    isVisible: boolean;
    /** true by default */
    isHidable: boolean;
    /** true by default */
    isResizable: boolean;
    /** true by default */
    isSortable: boolean;
    /** true by default */
    isAutoFilterable: boolean;
    /** false by default */
    isFooter: boolean;
    /** determine whether the cells in this column should be clickable */
    fnShouldLinkSingleValue: { (record: IRecord, fieldKey: string, dataValue: any, localizedValue: any): boolean };
    /** if a particular cell is determined as clickable by fnShouldLinkSingleValue, this function will be called when the cell is clicked */
    fnSingleValueClicked: { (record: IRecord, fieldKey: string, dataValue: any, localizedValue: any): void };
    /** this is used when you need to make some of the cells in the column readonly, but at the same time keep others editable */
    fnGetCellEditMode: { (record: IRecord, fieldKey: string): JsGrid.EditMode };
    /** this function should return name of the display control for the given cell in the column
        the name should be previously associated with the display control via SP.JsGrid.PropertyType.Utils.RegisterDisplayControl method */
    fnGetDisplayControlName: { (record: IRecord, fieldKey: string): string };
    /** this function should return name of the edit control for the given cell in the column
        the name should be previously associated with the edit control via SP.JsGrid.PropertyType.Utils.RegisterEditControl method */
    fnGetEditControlName: { (record: IRecord, fieldKey: string): string };
    /** set widget control names for a particular cell
        widgets are basically in-cell buttons with associated popup controls, e.g. date selector or address book button
        standard widget ids are defined in the SP.JsGrid.WidgetControl.Type enumeration
        it is also possible to create your own widgets
        usually this function is not used, and instead, widget control names are determined via PropertyType
     */
    fnGetWidgetControlNames: { (record: IRecord, fieldKey: string): string[] };
    /** this function should return id of the style for the given cell in the column
        styles and their ids are registered for a JsGridControl via jsGridParams.styleManager.RegisterCellStyle method */
    fnGetCellStyleId: { (record: IRecord, fieldKey: string, dataValue: any): string };
    /** set custom tooltip for the given cell in the column. by default, localized value is displayed as the tooltip */
    fnGetSingleValueTooltip: { (record: IRecord, fieldKey: string, dataValue: any, localizedValue: any): string };
}

Как видите, с колонками можно много что делать интересного.

Для подсветки данных, центральный метод – это fnGetCellStyleId:

/** this function should return id of the style for the given cell in the column
    styles and their ids are registered for a JsGridControl via jsGridParams.styleManager.RegisterCellStyle method */
fnGetCellStyleId: { (record: IRecord, fieldKey: string, dataValue: any): string };

Похоже, то что надо: можно определить стиль для конкретной ячейки, на основе данных соответствующего поля (или даже данных других полей).

Для примера, вот код, который определяет fnGetCellStyleId для колонки “Title”:

function jsGridCustomize(ctx) {
    if (ctx.enteringGridMode || !ctx.inGridMode)
        return;

    var g = $get('spgridcontainer_' + ctx.wpq).jsgrid;

    var titleColumn = g.GetColumnByFieldKey("Title");
    titleColumn.fnGetCellStyleId = function (record, fieldKey, dataValue) {
        // do something here
    }

    // refresh rows so that highlights are applied
    g.RefreshAllRows();
}

Ну хорошо, callback мы определили, и простой тест покажет, что он успешно вызывается. Теперь пора разобраться с параметрами и возвращаемым значением. Начнем с параметров!

Двойственные значения полей в JsGrid: LocalizedValue и DataValue


Передаваемый в коллбэк параметр record предоставляет доступ к значениям полей текущей записи. Вот как выглядит определение интерфейса IRecord:

export interface IRecord {
    /** True if this is an entry row */
    bIsNewRow: boolean;

    /** Please use SetProp and GetProp */
    properties: { [fieldKey: string]: IPropertyBase };

    /** returns recordKey */
    key(): number;
    /** returns raw data value for the specified field */
    GetDataValue(fieldKey: string): any;
    /** returns localized text value for the specified field */
    GetLocalizedValue(fieldKey: string): string;
    /** returns true if data value for the specified field is available */
    HasDataValue(fieldKey: string): boolean;
    /** returns true if localized text value for the specified field is available */
    HasLocalizedValue(fieldKey: string): boolean;

    GetProp(fieldKey: string): IPropertyBase;
    SetProp(fieldKey: string, prop: IPropertyBase): void;

    /** Update the specified field with the specified value */
    AddFieldValue(fieldKey: string, value: any): void;
    /** Removes value of the specified field.
        Does not refresh the view. */
    RemoveFieldValue(fieldKey: string): void;
}

fieldKey, как минимум в случае списка в режиме Quick Edit – это InternalName поля (SPField).

Главными методами работы с данными здесь являются HasDataValue, HasLocalizedValue, GetDataValue и GetLocalizedValue. Итак, что же это такое, DataValue и LocalizedValue?

Вспомните про Lookup-поля, и все сразу становится понятным: LookupId – это DataValue, LookupValue – это LocalizedValue. В зависимости от типа поля, LookupValue или даже DataValue может не присутствовать.

Вот таблица, включающая некоторые типы полей в SharePoint и примеры DataValue и LocalizedValue для них:

Тип поля DataValue LocalizedValue
Text (отсутствует) “Example title”
Lookup 1 “Example lookup value”
Boolean “0” или “1” “Yes” или “No”
Date (отсутствует) "10/3/2014"
MultiUser “Andrei Markeev; John Smith” [{
  email: “andrei.markeev@test.test”,
  display: ”Andrei Markeev”
},
{
  email: “john.smith@test.test”,
  display: “John Smith”
}]
Choice “Example choice value” “Example choice value”
Number (formatted as percent) (отсутствует) “100 %”
и т.д.

Обратите внимание, что отображение значений в итоге происходит через специальный контрол, поэтому отображаемое в гриде значение может на самом деле не соответствовать ни DataValue, ни LocalizedValue (но определяться на их основе). Хороший пример этого феномена – поле типа “Date”. Как видно из таблицы, LocalizedValue для поля Date имеет значение “10/3/2014”. Однако в гриде это же самое значение выглядит вот как:
image

Стили JsGrid


В JsGrid своя система стилей (не CSS), что несколько ограничивает возможности по изменению внешнего вида грида.

В частности, например для ячеек JsGrid понимает только такие атрибуты:

export interface ICellStyle {
    /** -> CSS font-family */
    font: string;
    /** -> CSS font-size */
    fontSize: string;
    /** -> CSS font-weight */
    fontWeight: string;
    /** -> CSS font-style */
    fontStyle: string;
    /** -> CSS color */
    textColor: string;
    /** -> CSS background-color */
    backgroundColor: string;
    /** -> CSS text-align */
    textAlign: string;
}

Каждый стиль имеет уникальный идентификатор, и каждый стиль нужно обязательно регистрировать перед использованием. Примерный код для регистрации выглядит так:

gridParams.styleManager.RegisterCellStyle(
    "Important",
    SP.JsGrid.Style.CreateStyle(SP.JsGrid.Style.Type.Cell, { backgroundColor: "#ffcccc" })
);

Регистрация должна производиться в процессе инициализации грида. В дальнейшем обращение к стилю происходит только через его идентификатор.

Таким образом, JsGrid принуждает все стили определять централизованно, в одном месте (при инициализации грида).

Это очень удобно и очень правильно, если грид создается нами самими с нуля, но совершенно никуда не годится, если мы используем грид, который уже создал SharePoint.

Что делать? Хак конечно, а какие еще варианты…

Хак


Эх, как бы хотелось чтобы в SharePoint можно было обходиться без хаков!… Но пока для данного случая способа нормально работать с процессом инициализации JsGridControl без хаков я не нашел :(

Хак состоит в том, чтобы заинжектить коллбэк в метод SP.JsGrid.JsGridControl.Init. Этот метод как раз выполняет инициализацию грида и туда передается объект jsGridParams, содержащий все самые важные внутренние объекты и свойства, включая пресловутый styleManager.

Вот определение метода Init:

export class JsGridControl {
    // ...

    /** Initialize the control */
    Init(parameters: SP.JsGrid.JsGridControl.Parameters): void;

    // ...
}

Нижеследующий код подменяет JsGridControl так, что каждый раз когда вызывается Init, вызывается также мой кастомный метод jsGridCustomInit с теми же параметрами:

var originalControl = SP.JsGrid.JsGridControl;
SP.JsGrid.JsGridControl = function (parentNode, bShowLoadingBanner) {
    var control = new originalControl(parentNode, bShowLoadingBanner);
    for (var m in control) {
        this[m] = (function (mm) { return control[mm] })(m);
    }
    this.Init = function (params) {
        jsGridCustomInit(params);
        return control.Init(params);
    }


};
for (var p in originalControl)
    SP.JsGrid.JsGridControl[p] = originalControl[p];

Собственно остается лишь нормально обернуть этот код и вызвать его сразу после загрузки JsGrid.js.

Я согласен, что данный хак выглядит неприятно, и вообще само по себе действие неприятно, но зато этот прием позволяет работать с Quick Edit режимом списков полноценным образом (т.к. множество действий должны выполняться именно при инициализации грида).

Итоговый код


В итоге у меня получился вот такой код:

function init() {

    SP.SOD.executeFunc("jsgrid.js", "SP.JsGrid.JsGridControl", function () {
        applyJsGridHack();
    });

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
        OnPostRender: jsGridCustomize
    });


}

function jsGridCustomize(ctx) {
    if (ctx.enteringGridMode || !ctx.inGridMode)
        return;

    var g = $get('spgridcontainer_' + ctx.wpq).jsgrid;

    var titleColumn = g.GetColumnByFieldKey("Title");
    titleColumn.fnGetCellStyleId = function (record, fieldKey, dataValue) {
        if (record.GetDataValue("Capital") == "1")
            return "Important";
        else
            return null;
    }
        g.RefreshAllRows();

}


function jsGridCustomInit(params) {
    params.styleManager.RegisterCellStyle("Important", SP.JsGrid.Style.CreateStyle(SP.JsGrid.Style.Type.Cell, { backgroundColor: "#ffcccc" }));
}

function applyJsGridHack() {
    var originalControl = SP.JsGrid.JsGridControl;
    SP.JsGrid.JsGridControl = function (parentNode, bShowLoadingBanner) {
        var control = new originalControl(parentNode, bShowLoadingBanner);
        for (var m in control) {
            this[m] = (function (mm) { return control[mm] })(m);
        }
        this.Init = function (params) {
            jsGridCustomInit(params);
            return control.Init(params);
        }


        };
    for (var p in originalControl)
        SP.JsGrid.JsGridControl[p] = originalControl[p];

}

И вот результат выполнения:

image

Подсветка строк целиком


Для подсветки строк целиком, можно конечно банально определить один и тот же fnGetCellStyleId. Но альтернативно, можно использовать систему так называемых делегатов и метод JsGridControl SetDelegate.

/** Set a delegate. Delegates are way to replace default functionality with custom one. */
SetDelegate(delegateKey: JsGrid.DelegateType, fn): void;
/** Get current delegate. */
GetDelegate(delegateKey: JsGrid.DelegateType): any;

Метод SetDelegate предназначен специально для расширения функционала JsGrid. Всего существует аж 32 типа делегатов, которые позволяют заменять или дополнять стандартный функционал JsGrid. Например, с помощью делегатов можно делать кастомную сортировку, фильтрацию, пейджинг и т.д. Но тема сегодняшнего поста – подсветка строк, и поэтому я расскажу только об одном делегате, под названием GetGridRowStyleId. Вот определение функции делегата:

function GetGridRowStyleId_Delegate(record: SP.JsGrid.IRecord): string

Как видите, все просто: делегату передается текущая запись, и на основе этой записи он должен вернуть идентификатор стиля.

Единственная тонкость состоит в том, что делегаты должны регистрироваться опять же при инициализации грида, то есть тогда же, когда происходит регистрация стилей. Возможно, какие-то делегаты работают даже если их зарегистрировать позже – не знаю, но GetGridRowStyleId точно не работает.

Итак, код для подсветки всей строки (условия такие же как при подсветки колонки выше):

g.SetDelegate(SP.JsGrid.DelegateType.GetGridRowStyleId, function (record) {
    if (record.GetDataValue("Capital") == "1")
        return "Important";
    else
        return null;
});

Повторюсь, этот код нужно выполнить в jsGridCustomInit! И не забудьте протащить туда “g” – т.е. сам объект контрола, это легко сделать т.к. объект control в applyJsGridHack это как раз он же.

И наконец – ожидаемый результат:

image

Заключение


Еще раз повторюсь: с JsGrid работать довольно приятно. API несколько громоздкий и старомодный, но все же очень логичный и продуманный. Меня не покидает мысль, что JsGrid писала какая-то отдельная команда нормальных разработчиков :)

P.S. Если есть какие-то дополнительные вопросы – не стесняйтесь задавать в комментариях!

5 комментариев:

  1. Андрей, я так понимаю все это ради того, чтобы иметь возможность кастомизировать грид в режиме QuickEdit? В режиме просмотра можем использовать обычную CSR кастомизацию представлений?

    ОтветитьУдалить
    Ответы
    1. Да, всё верно. Я подробно описывал, зачем нужен JSGrid, в предыдущем посте Документирую JSGrid. Quick Edit - это самое распространенное, но на самом деле на основе JSGrid можно делать и отдельные решения, мержить данные из разных списков, и много еще что.

      К примеру в DeskWork еще в 2011м, если мне не изменяет память, мы делали очень приятное решение Центр задач, и с тех самых пор мне этот контрол очень полюбился. Excel в браузере, и причем практически лишен характерных для SharePoint багов и глюков.

      На самом деле CSR частично работает и для Quick Edit, но только очень частично :(

      Удалить
  2. Никак не могу победить параметр width у column! Значение присваивается, всё ОК, но грид никак на него не реагирует!
    Например, поле Name отлично редактируется, а что с шириной, никак не могу понять!

    А за статью спасибо, Андрей! Как всегда, очень познавательно!

    ОтветитьУдалить
    Ответы
    1. Привет,
      Спасибо огромное за отзыв, а то что-то с фидбэком по JSGrid настолько кисло, что я уже подумал что зря всё это писал!))
      Но еще не теряю надежду что народ прочухает и оценит этот кусок нормального среди шарепойнта :)

      Насчет width - проверю. Помнится, устанавливал его, все было ок. Проверю, как только смогу.

      Удалить
  3. А как выделить цветом заголовок таблицы?

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

Внимание! Реклама и прочий спам будут беспощадно удаляться.