Generate powerful shortcuts and interactions with your graphics by creating a context menu. This article proposes a four steps implementation of a context menu system starting from scratch in your ScheduleJS graphics.
Step 1: Define your context menu structure using HTML
The first step in this tutorial is to declare your context menu in the HTML part of the application. The best way to keep your code clean is to create a dedicated component to render the context menu and define your interactions.
<!-- The ngStyle input will handle positionning -->
<div class="demo-planning-board-context-menu"
[class.display]="!gantt.contextMenuOverlay.isHidden"
[ngStyle]="gantt.contextMenuOverlay.position"
(click)="gantt.contextMenuOverlay.onHide()">
<!-- (1) Set started action -->
<div class="demo-planning-board-context-menu-action"
(click)="gantt.contextMenuOverlay.onSetTaskStarted()">
Set started
</div>
<!-- (2) Set completed action -->
<div class="demo-planning-board-context-menu-action"
(click)="gantt.contextMenuOverlay.onSetActivityCompleted()">
Set completed
</div>
<!-- (3) Set late action -->
<div class="demo-planning-board-context-menu-action"
(click)="gantt.contextMenuOverlay.onSetActivityLate()">
Set late
</div>
<!-- (4) Set priority action -->
<div class="demo-planning-board-context-menu-action"
(click)="gantt.contextMenuOverlay.onSetActivityPriority()">
{{ gantt.contextMenuOverlay.activity?.isHighPriority
? "Remove high priority" : "Set high priority" }}
</div>
</div>
The display
child class will be used to hide and show the context menu, while we will update its position using CSS and pass it through the Angular [ngStyle]
input property.
Here, we created a simple contextual layout with four actions:
-
Set started
: Change the activity state and set it as started -
Set completed
: Set the activity as completed -
Set late
: Set the activity sequence as late, starting from a specific activity -
Set priority
: Set the priority as High or remove this setting Now let’s do a little CSS to make it pretty. We recommend using SCSS to create a scope for the style classes like the following:
.demo-planning-board-context-menu {
display: none;
position: absolute;
z-index: 1;
background: #555555dd;
border-radius: 5px;
padding: 3px 0;
&.display {
display: flex;
flex-direction: column;
}
.demo-planning-board-context-menu-action {
color: white;
font: 13px $demo-planning-board-font;
padding: 0 10px;
margin: 3px 0;
&:hover {
color: black;
filter: brightness(0.9);
background: rgba(255, 255, 255, 0.4);
}
&:active {
filter: brightness(0.8);
}
}
}
Once done, we can start playing with Angular to create the logic for this element.
Step 2: Create an overlay abstraction
Using an object-oriented approach, we can define an abstraction that will be the starting point for all our overlays, so we can reuse it to create tooltips, modals, and such.
export abstract class PlanningBoardAbstractOverlay {
// Attributes
isHidden: boolean = true;
activity: PlanningBoardActivity | undefined = undefined;
position: PlanningBoardOverlayPosition = {};
// Constructor
constructor(public gantt: PlanningBoardGanttChart) { }
// Methods
abstract onShow(pointerEvent: PointerEvent, activity: PlanningBoardActivity | undefined): void;
onHide(): void {
this.isHidden = true;
}
setOverlayElementPosition(pointerEvent: PointerEvent): void {
const isRight = pointerEvent.x > window.innerWidth / 2;
const isBottom = pointerEvent.y > window.innerHeight / 2;
const marginPx = 10;
this.position.top = isBottom ? "auto" : pointerEvent.y + marginPx + "px";
this.position.right = isRight ? window.innerWidth - pointerEvent.x + marginPx + "px" : "auto";
this.position.bottom = isBottom ? window.innerHeight - pointerEvent.y + marginPx + "px" : "auto";
this.position.left = isRight ? "auto" : pointerEvent.x + marginPx + "px";
}
}
export interface PlanningBoardOverlayPosition {
top?: string;
right?: string;
bottom?: string;
left?: string;
}
Let’s store a few properties that will hold the state of our current overlay abstraction:
- The
isHidden
property will be used to hide and show the overlay. - The
activity
property links our overlay to a specific activity. - The
position
property will define where the overlay should render using our PointerEvent. Exposing theGanttChart
instance in the overlay will help us to create actions and interact with our graphics.
The overlay will also expose three methods:
- The
onShow
method is used to define the display strategy. - The
onHide
method. - The
setOverlayElementPosition
will update the position property.
Step 3: Build the context menu logic
Using our PlanningBoardAbstractOverlay
abstract class, we can now create a new PlanningBoardContextMenuOverlay
class that will hold the logic for our context menu.
export class PlanningBoardContextMenuOverlay extends PlanningBoardAbstractOverlay {
// Methods
onShow(pointerEvent: PointerEvent, activity: PlanningBoardActivity): void {
if (activity) {
this.isHidden = false;
this.activity = activity;
this.setOverlayElementPosition(pointerEvent);
} else {
this.onHide();
}
}
// Context menu actions
onSetTaskStarted(): void {
this.activity.progressRatio = 0.01;
this.gantt.redraw();
}
onSetActivityCompleted(): void {
this.activity.progressRatio = 1;
this.gantt.redraw();
}
onSetActivityLate(): void {
this.activity.deadline = 0;
this.activity.successorFinishesAfterDeadline = true;
this.gantt.redraw();
}
onSetActivityPriority(): void {
this.activity.isHighPriority = !this.activity.isHighPriority;
this.gantt.redraw();
}
}
Let’s design the onShow
process:
- When opening the menu with an activity, we will store this activity and display our contextual menu. Let’s use the
setOverlayElementPosition
we created in our abstract class and give it ourPointerEvent
. - If the context menu is opened without a contextual activity, we trigger the
onHide
method. Our four actions will update the activity data and trigger aredraw
, letting our underlyingActivityRenderer
update the graphics with this new information.
Step 4: Trigger the context menu
ScheduleJS proposes a large set of event methods that you can register in the main object: the GanttChart. An easy way to organize the code is to create a custom GanttChart
class that extends the default GanttChart
.
// Here we create our custom GanttChart class
export class PlanningBoardGanttChart extends GanttChart<PlanningBoardRow> {
// Instantiate our PlanningBoardContextMenuOverlay class
readonly contextMenuOverlay: PlanningBoardContextMenuOverlay = new PlanningBoardContextMenuOverlay(this);
// The minimal GanttChart implementation requires the Angular injector
constructor(injector: Injector) {
super(undefined, injector);
}
// Event handlers [...]
}
As the GanttChart
object is at the core of ScheduleJS, its class is a great place to register the default renderers, system layers, and events handlers. Note that the ScheduleJS API is accessible through the GanttChart
instance with methods like gantt.getGraphics()
.
The GanttChart
class proposes a set of overridable methods designed to handle user input on the graphics, for example:
onRowDrawingEnginePointerDown
onDatelinePointerMove
onToggleGrid
What we want to do here is to override the onRowDrawingEngineContextMenu
method to trigger logic when opening our context menu. In a desktop environment, this method is called when the user right-clicks anywhere on the graphics.
/**
* Trigger when right-clicking on the canvas
*/
onRowDrawingEngineContextMenu(pointerEvent: PointerEvent, row: PlanningBoardRow, index: number): void {
super.onRowDrawingEngineContextMenu(pointerEvent, row, index);
const hoveredActivity = this._getHoveredActivity();
if (hoveredActivity) {
this.tooltipOverlay.onHide();
this.contextMenuOverlay.onShow(pointerEvent, hoveredActivity);
this._changeDetectorRef.detectChanges();
}
}
Now, when the user right-clicks on the graphics, this code will trigger with the underlying pointer event, current row, and row-index. We can extract the currently hoveredActivity
using the graphics.getHoveredActivity
API and pass it to the overlay onShow
method.
The context menu we are building will add interactions with activities present on the canvas. On user right click, we will check if we are hovering over an activity, and if it’s the case, we hide our tooltip and trigger the contextMenuOverlay.onShow
method.
For performance reasons, this method is run outside of the Angular zone by ScheduleJS. So we have to add a call to ChangeDetectorRef.detectChanges()
to trigger change detection manually and update the DOM.
Conclusion
In conclusion, implementing a context menu in your ScheduleJS graphics involves four key steps. First, define your context menu structure using HTML by creating a dedicated component for rendering the menu and handling interactions. Second, create an overlay abstraction to manage positioning and display logic. Third, build the context menu logic by extending the overlay abstraction, and lastly, trigger the context menu through events in your custom GanttChart class.
To see the final result, please visit our blog. For more detailed information, you can explore the comprehensive documentation on ScheduleJS.
Top comments (0)