среда, 22 октября 2014 г.

Своё контекстное меню в JsGrid


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

В прошлом посте я объяснял, как устроена система событий JsGrid, и оказывается, как раз с помощью события SP.JsGrid.EventType.OnRightClick - можно решить задачу с контекстным меню. Помимо события, впрочем, надо еще уметь само контекстное меню создавать. И оказывается, JsGrid имеет API и для этого тоже!

SP.JsGrid.ContextMenu


Класс ContextMenu имеет следующее определение:

/** Create a context menu. Uses the SharePoint OOTB context menus under the hood. */
export class ContextMenu {
    /** Create a context menu object.
        parentNode is the DOM node there the context menu will be attached. */
    constructor(parentNode, id);
    /** Gets the underlying SharePoint context menu object.
        Usually not necessary. */
    GetMenuElement(): any;
    EnableCompactMode(): void;
    /** Determine if the menu is currently shown */
    IsOpen(): boolean;
    /** Determine if any items were added into the menu */
    IsEmpty(): boolean;
    /** Insert a new menu item into the menu */
    InsertMenuItem(text: string, fnItemClicked: { (): void }, imageUri: string, imageAltText: string, bDisabled: boolean, bChecked: boolean): MenuItem;
    /** Insert a separator element into the menu */
    InsertSeparator(): void;
    /** Hide the menu */
    Hide(): void;
    /** Set a callback that will be fired when menu is destroyed */
    SetOnMouseOut(optfnOnMouseOut): void;
    /** Show the menu at a specified position */
    Show(pos: { top: number; left: number; width: number; height: number; }, optfnOnMouseOut): void;
    /** Show the menu next to a specified element */
    ShowAttached(elem, optfnOnMouseOut): void;
    /** Refresh menu */
    Refresh(): void;
    /** Destroy menu */
    Dispose(): void;
}
export class MenuItem {
    /** Changes state of the item. If item is not enabled, it is grayed out and can't be clicked. */
    Enable(bEnable: boolean): void;
    /** Changes checked state of the item. Checked items are displayed with a checkmark to the left. */
    Check(bCheck: boolean): void;
}

Главные методы здесь – это InsertMenuItem, который добавляет элемент в меню; и Show, который собственно меню отображает.

Алгоритм создания простейшего контекстного меню выглядит примерно так:

var m = new SP.JsGrid.ContextMenu(document.getElementById('spgridcontainer_WPQ2'), "my_jsgrid_menu");
m.InsertMenuItem("test1", function () { alert('test 1'); }, null, null, false, false);
m.InsertMenuItem("test2", function () { alert('test 2'); }, null, null, false, false);
m.InsertSeparator();
m.InsertMenuItem("test3", function () { alert('test 2'); }, null, null, false, false);
m.Show({ top: 150, left: 100, width: 0, height: 0 }, null);

Результат выполнения этого кода из консоли:

image


Позиционирование


Правильное кроссбраузерное позиционирование контекстного меню можно реализовать, используя код, который сам SharePoint использует для этого же, и который я предусмотрительно подсмотрел :)

Для этого используются функции SP.Internal.DomElement.GetLocation и SP.Internal.DomElement.GetEventLocation:

static GetLocation(element: HTMLElement): { x: number; y: number; };
static GetEventLocation(eventInfo: any): { x: number; y: number; };

GetEventLocation нужен, чтобы определить абсолютную позицию щелчка мыши. eventInfo берется из EventArgs, которые для события OnRightClick выглядят, кстати, следующим образом:

export class Click implements IEventArgs {
    constructor(eventInfo, context: JsGrid.ClickContext, recordKey: number, fieldKey: string);
    eventInfo: any;
    context: JsGrid.ClickContext;
    recordKey: number;
    fieldKey: string;
}

Обратите внимание, используется класс SP.JsGrid.EventArgs.Click, этот класс общий для типов событий OnRightClick и OnDoubleClick.

Т.е. код для позиционирования будет примерно следующий:

function OnRightClick (args) {

    // create menu here

    var parentLoc = SP.Internal.DomElement.GetLocation(containerElement);
    var eventLoc = SP.Internal.DomElement.GetEventLocation(args.eventInfo);
    myContextMenu.Show({
        left: eventLoc.x - parentLoc.x,
        top: eventLoc.y - parentLoc.y,
        width: 0,
        height: 0
    }, null);

}

Дошлифовка


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

Впрочем, решить эти две проблемы оказалось не очень сложно.

Определить, что пользователь кликнул именно по ячейке, а не по заголовку – получилось совсем просто. У SP.JsGrid.EventArgs.Click есть свойство context:

export enum ClickContext {
    SelectAllSquare,
    RowHeader,
    ColumnHeader,
    Cell,
    Gantt,
    Other
}

После добавления проверки args.context == SP.JsGrid.ClickContext.Cell первая проблема была решена.

С второй проблемой помог справится метод SelectCellRangeByKey объекта JsGridControl. Причем, метода, чтобы выделить только одну ячейку – я не нашел. Есть только выделение диапазона. Впрочем, диапазон может состоять и из одной ячейки.

/** Select cells by recordKeys. Pass bAppend=true if you want to append this selection to the current rather than replace it. */
SelectCellRangeByKey(recordKey1: string, recordKey2: string, colKey1: string, colKey2: string, bAppend: boolean, optPaneId?): void;

Впрочем, тут оказалось всё не так просто. Событие OnRightClick срабатывает на mousedown, а браузерное нативное контекстное меню (как минимум в Chrome) открывается по mouseup. Из-за того, что грид переводит некоторые элементы в режим редактирования при их выделении (в частности, это касается любых колонок с dropdown’ом – Lookup, Boolean и т.д.), получалось, что выскакивало 2 контекстных меню: браузерное и моё кастомное:

image

Неприятненько!

Справиться с этой проблемой удалось путем запрета редактирования в гриде на время открытия контекстного меню. Методы, запрещающие и разрешающие редактирование грида, нашлись в JsGridControl:

/** Enables grid editing */
EnableEditing(): void;
/** Disables grid editing: all the records become readonly */
DisableEditing(): void;

DisableEditing я вызывал сразу перед тем, как выделить ячейку, а EnableEditing – через callback onfnoptMouseOut, который срабатывает именно в момент уничтожения меню:
/** Show the menu at a specified position.
    optfnOnMouseOut callback fires when the menu is destroyed. */
Show(pos: { top: number; left: number; width: number; height: number; }, optfnOnMouseOut?: { (): void; }): void;

Итоговый код



Вот что у меня получилось в итоге:

(function () {

    function init() {

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

    }

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

        var containerElement = document.getElementById('spgridcontainer_' + ctx.wpq);
        var jsGridControl = containerElement.jsgrid;
        jsGridControl.AttachEvent(SP.JsGrid.EventType.OnRightClick, function (args) {

            if (args.context != SP.JsGrid.ClickContext.Cell)
                return;

            var myContextMenu = new SP.JsGrid.ContextMenu(containerElement, "my_jsgrid_menu");
            myContextMenu.InsertMenuItem("test1", function () { alert('test 1'); }, null, null, false, false);
            myContextMenu.InsertMenuItem("test2", function () { alert('test 2'); }, null, null, false, false);
            myContextMenu.InsertSeparator();
            myContextMenu.InsertMenuItem("test3", function () { alert('test 2'); }, null, null, false, false);

            var parentLoc = SP.Internal.DomElement.GetLocation(containerElement);
            var eventLoc = SP.Internal.DomElement.GetEventLocation(args.eventInfo);
            myContextMenu.Show({
                left: eventLoc.x - parentLoc.x,
                top: eventLoc.y - parentLoc.y,
                width: 0,
                height: 0
            }, function () {
                    jsGridControl.EnableEditing();
                });


            jsGridControl.DisableEditing();
            jsGridControl.SelectCellRangeByKey(args.recordKey, args.recordKey, args.fieldKey, args.fieldKey, false);

        });

    }

    RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~site/Style Library/jsgridCustomize.js"), init);
    init();

})();

Заключение


Оооо, я бы очень хотел чтобы все мои статьи по SharePoint были настолько простыми и незамысловатыми, и лишенными всяческих хаков! :)

В общем, с контекстными меню в JsGrid всё без проблем. Пользуем! :)

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

  1. Давай уже статьи как свой грид сделать и как контролы кастомизировать.

    ОтветитьУдалить
  2. Здравствуйте Андрей, есть вопрос по SP2010. Можете помочь?
    Кратко о задаче:
    Есть 2 листа, где с листа A идет лукап на лист B. На листе В стоят воркфловы настроенные.
    Вопрос: Можно ли сделать так, что при обновлении листа А, значение лукапа добавлялось в лист В?

    ОтветитьУдалить
    Ответы
    1. если честно, не очень понял задачу.
      так чтоли надо? (код тут)

      Удалить
  3. Решение желательно должно быть через воркфловы или в крайнем случае через SPservices + jQuery

    ОтветитьУдалить
  4. Андрей, добрый день! Возможно вопрос не совсем по теме. Я делаю стандартную фильтрацию на JSGrid, столкнулся с проблемой, что когда значений для фильтра много, автоматически они не отдаются в варианты фильтрации. Нужно писать свой код для выдачи, видимо с веб-сервисом... А можно ли использовать стандартные механизмы? Сталкивались с таким? Спасибо:)

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

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