Часто возникающая задача – это прикрутить к ячейкам 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;
}
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);
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);
Результат выполнения этого кода из консоли:
Позиционирование
Правильное кроссбраузерное позиционирование контекстного меню можно реализовать, используя код, который сам 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; };
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;
}
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);
}
// 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);
}
Дошлифовка
Когда я собрал код и запустил его, всплыли две проблемы:
- Контекстное меню появлялось даже при щелчке на заголовки столбцов и строк.
- При правом клике по ячейке, активная ячейка не менялась
Впрочем, решить эти две проблемы оказалось не очень сложно.
Определить, что пользователь кликнул именно по ячейке, а не по заголовку – получилось совсем просто. У SP.JsGrid.EventArgs.Click есть свойство context:
export enum ClickContext {
SelectAllSquare,
RowHeader,
ColumnHeader,
Cell,
Gantt,
Other
}
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;
SelectCellRangeByKey(recordKey1: string, recordKey2: string, colKey1: string, colKey2: string, bAppend: boolean, optPaneId?): void;
Впрочем, тут оказалось всё не так просто. Событие OnRightClick срабатывает на mousedown, а браузерное нативное контекстное меню (как минимум в Chrome) открывается по mouseup. Из-за того, что грид переводит некоторые элементы в режим редактирования при их выделении (в частности, это касается любых колонок с dropdown’ом – Lookup, Boolean и т.д.), получалось, что выскакивало 2 контекстных меню: браузерное и моё кастомное:
Неприятненько!
Справиться с этой проблемой удалось путем запрета редактирования в гриде на время открытия контекстного меню. Методы, запрещающие и разрешающие редактирование грида, нашлись в JsGridControl:
/** Enables grid editing */
EnableEditing(): void;
/** Disables grid editing: all the records become readonly */
DisableEditing(): void;
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;
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();
})();
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 всё без проблем. Пользуем! :)
Давай уже статьи как свой грид сделать и как контролы кастомизировать.
ОтветитьУдалитьДа что-то интереса не видно.
УдалитьЗдравствуйте Андрей, есть вопрос по SP2010. Можете помочь?
ОтветитьУдалитьКратко о задаче:
Есть 2 листа, где с листа A идет лукап на лист B. На листе В стоят воркфловы настроенные.
Вопрос: Можно ли сделать так, что при обновлении листа А, значение лукапа добавлялось в лист В?
если честно, не очень понял задачу.
Удалитьтак чтоли надо? (код тут)
Решение желательно должно быть через воркфловы или в крайнем случае через SPservices + jQuery
ОтветитьУдалитьАндрей, добрый день! Возможно вопрос не совсем по теме. Я делаю стандартную фильтрацию на JSGrid, столкнулся с проблемой, что когда значений для фильтра много, автоматически они не отдаются в варианты фильтрации. Нужно писать свой код для выдачи, видимо с веб-сервисом... А можно ли использовать стандартные механизмы? Сталкивались с таким? Спасибо:)
ОтветитьУдалить