OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/frontend/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts

372 lines
13 KiB

import * as moment from "moment";
import {$injectNow} from "../../../angular/angular-injector-bridge.functions";
import {WorkPackageResourceInterface} from "../../../api/api-v3/hal-resources/work-package-resource.service";
import {
calculatePositionValueForDayCount,
calculatePositionValueForDayCountingPx,
RenderInfo,
timelineElementCssClass,
timelineMarkerSelectionStartClass
} from "../wp-timeline";
import {classNameLeftHandle, classNameRightHandle} from "./wp-timeline-cell-mouse-handler";
import Moment = moment.Moment;
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
import {hasChildrenInTable} from '../../../wp-fast-table/helpers/wp-table-hierarchy-helpers';
interface CellDateMovement {
// Target values to move work package to
startDate?: moment.Moment;
dueDate?: moment.Moment;
}
export class TimelineCellRenderer {
protected TimezoneService:any;
protected dateDisplaysOnMouseMove: {left?: HTMLElement; right?: HTMLElement} = {};
constructor(public workPackageTimeline:WorkPackageTimelineTableController) {
}
public get type(): string {
return "bar";
}
public get fallbackColor(): string {
return "rgba(50, 50, 50, 0.1)";
}
public isEmpty(wp: WorkPackageResourceInterface) {
const start = moment(wp.startDate as any);
const due = moment(wp.dueDate as any);
const noStartAndDueValues = _.isNaN(start.valueOf()) && _.isNaN(due.valueOf());
return noStartAndDueValues;
}
public displayPlaceholderUnderCursor(ev: MouseEvent, renderInfo: RenderInfo): HTMLElement {
const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay);
const placeholder = document.createElement("div");
placeholder.style.pointerEvents = "none";
placeholder.style.backgroundColor = "#DDDDDD";
placeholder.style.position = "absolute";
placeholder.style.height = "1em";
placeholder.style.width = "30px";
placeholder.style.left = (days * renderInfo.viewParams.pixelPerDay) + "px";
return placeholder;
}
/**
* Assign changed dates to the work package.
* For generic work packages, assigns start and due date.
*
*/
public assignDateValues(wp: WorkPackageResourceInterface, dates: CellDateMovement) {
this.assignDate(wp, "startDate", dates.startDate!);
this.assignDate(wp, "dueDate", dates.dueDate!);
this.updateLeftRightMovedLabel(dates.startDate!, dates.dueDate!);
}
/**
* Restore the original date, if any was set.
*/
public onCancel(wp: WorkPackageResourceInterface) {
wp.restoreFromPristine("startDate");
wp.restoreFromPristine("dueDate");
}
/**
* Handle movement by <delta> days of the work package to either (or both) edge(s)
* depending on which initial date was set.
*/
public onDaysMoved(wp: WorkPackageResourceInterface,
dayUnderCursor: Moment,
delta: number,
direction: "left" | "right" | "both" | "create" | "dragright"): CellDateMovement {
const initialStartDate = wp.$pristine["startDate"];
const initialDueDate = wp.$pristine["dueDate"];
let dates: CellDateMovement = {};
if (direction === "left") {
dates.startDate = moment(initialStartDate || initialDueDate).add(delta, "days");
} else if (direction === "right") {
dates.dueDate = moment(initialDueDate || initialStartDate).add(delta, "days");
} else if (direction === "both") {
if (initialStartDate) {
dates.startDate = moment(initialStartDate).add(delta, "days");
}
if (initialDueDate) {
dates.dueDate = moment(initialDueDate).add(delta, "days");
}
} else if (direction === "dragright") {
dates.dueDate = moment(wp.startDate).clone().add(delta, "days");
}
// avoid negative "overdrag" if only start or due are changed
if (direction !== "both") {
if (dates.startDate != undefined && dates.startDate.isAfter(moment(wp.dueDate))) {
dates.startDate = moment(wp.dueDate);
} else if (dates.dueDate != undefined && dates.dueDate.isBefore(moment(wp.startDate))) {
dates.dueDate = moment(wp.startDate);
}
}
return dates;
}
public onMouseDown(ev: MouseEvent,
dateForCreate: string|null,
renderInfo: RenderInfo,
elem: HTMLElement): "left" | "right" | "both" | "dragright" | "create" {
// check for active selection mode
if (renderInfo.viewParams.activeSelectionMode) {
renderInfo.viewParams.activeSelectionMode(renderInfo.workPackage);
ev.preventDefault();
return "both"; // irrelevant
}
renderInfo.workPackage.storePristine("startDate");
renderInfo.workPackage.storePristine("dueDate");
let direction: "left" | "right" | "both" | "create" | "dragright";
// Update the cursor and maybe set start/due values
if (jQuery(ev.target).hasClass(classNameLeftHandle)) {
// only left
direction = "left";
this.forceCursor("w-resize");
if (renderInfo.workPackage.startDate === null) {
renderInfo.workPackage.startDate = renderInfo.workPackage.dueDate;
}
} else if (jQuery(ev.target).hasClass(classNameRightHandle) || dateForCreate) {
// only right
direction = "right";
this.forceCursor("e-resize");
if (renderInfo.workPackage.dueDate === null) {
renderInfo.workPackage.dueDate = renderInfo.workPackage.startDate;
}
} else {
// both
direction = "both";
this.forceCursor("ew-resize");
}
this.dateDisplaysOnMouseMove = [null, null];
if (dateForCreate) {
renderInfo.workPackage.startDate = dateForCreate;
renderInfo.workPackage.dueDate = dateForCreate;
direction = "dragright";
}
if (!jQuery(ev.target).hasClass(classNameRightHandle) && renderInfo.workPackage.startDate) {
// create left date label
const leftInfo = document.createElement("div");
leftInfo.className = "leftDateDisplay";
this.dateDisplaysOnMouseMove.left = leftInfo;
elem.appendChild(leftInfo);
}
if (!jQuery(ev.target).hasClass(classNameLeftHandle) && renderInfo.workPackage.dueDate) {
// create right date label
const rightInfo = document.createElement("div");
rightInfo.className = "rightDateDisplay";
this.dateDisplaysOnMouseMove.right = rightInfo;
elem.appendChild(rightInfo);
}
this.updateLeftRightMovedLabel(
moment(renderInfo.workPackage.startDate),
moment(renderInfo.workPackage.dueDate));
return direction;
}
public onMouseDownEnd() {
this.dateDisplaysOnMouseMove.left && this.dateDisplaysOnMouseMove.left.remove();
this.dateDisplaysOnMouseMove.right && this.dateDisplaysOnMouseMove.right.remove();
this.dateDisplaysOnMouseMove = {};
}
/**
* @return true, if the element should still be displayed.
* false, if the element must be removed from the timeline.
*/
public update(timelineCell: HTMLElement, bar: HTMLDivElement, renderInfo: RenderInfo): boolean {
const wp = renderInfo.workPackage;
// general settings - bar
bar.style.backgroundColor = this.typeColor(renderInfo.workPackage);
const viewParams = renderInfo.viewParams;
let start = moment(wp.startDate as any);
let due = moment(wp.dueDate as any);
if (_.isNaN(start.valueOf()) && _.isNaN(due.valueOf())) {
bar.style.visibility = "hidden";
} else {
bar.style.visibility = "visible";
}
// only start date, fade out bar to the right
if (_.isNaN(due.valueOf()) && !_.isNaN(start.valueOf())) {
due = start.clone();
bar.style.backgroundColor = "inherit";
const color = this.typeColor(renderInfo.workPackage);
bar.style.backgroundImage = `linear-gradient(90deg, ${color} 0%, rgba(255,255,255,0) 80%)`;
}
// only due date, fade out bar to the left
if (_.isNaN(start.valueOf()) && !_.isNaN(due.valueOf())) {
start = due.clone();
bar.style.backgroundColor = "inherit";
const color = this.typeColor(renderInfo.workPackage);
bar.style.backgroundImage = `linear-gradient(90deg, rgba(255,255,255,0) 0%, ${color} 100%)`;
}
// offset left
const offsetStart = start.diff(viewParams.dateDisplayStart, "days");
bar.style.left = calculatePositionValueForDayCount(viewParams, offsetStart);
// duration
const duration = due.diff(start, "days") + 1;
bar.style.width = calculatePositionValueForDayCount(viewParams, duration);
// ensure minimum width
if (!_.isNaN(start.valueOf()) || !_.isNaN(due.valueOf())) {
const minWidth = _.max([renderInfo.viewParams.pixelPerDay, 2]);
bar.style.minWidth = minWidth + "px";
}
this.checkForActiveSelectionMode(renderInfo, bar);
this.checkForSpecialDisplaySituations(renderInfo, bar);
return true;
}
protected checkForActiveSelectionMode(renderInfo: RenderInfo, element: HTMLElement) {
if (renderInfo.viewParams.activeSelectionMode) {
element.style.backgroundImage = null; // required! unable to disable "fade out bar" with css
if (renderInfo.viewParams.selectionModeStart === "" + renderInfo.workPackage.id) {
jQuery(element).addClass(timelineMarkerSelectionStartClass);
element.style.background = null;
}
}
}
getLeftmostXValue(renderInfo: RenderInfo): number {
const wp = renderInfo.workPackage;
let start = moment(wp.startDate as any);
start = _.isNaN(start.valueOf()) ? moment(wp.dueDate).clone() : start;
const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, "days");
return calculatePositionValueForDayCountingPx(renderInfo.viewParams, offsetStart);
}
getInnerXOffsetForRelationLineDock(renderInfo: RenderInfo): number {
return renderInfo.viewParams.pixelPerDay / 8;
}
getRightmostXValue(renderInfo: RenderInfo): number {
const wp = renderInfo.workPackage;
let start = moment(wp.startDate as any);
start = _.isNaN(start.valueOf()) ? moment(wp.dueDate).clone() : start;
let due = moment(wp.dueDate as any);
due = _.isNaN(due.valueOf()) ? start.clone() : due;
const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, "days");
const duration = due.diff(start, "days") + 1;
return calculatePositionValueForDayCountingPx(renderInfo.viewParams, offsetStart + duration);
}
/**
* Render the generic cell element, a bar spanning from
* start to due date.
*/
public render(renderInfo: RenderInfo): HTMLDivElement {
const bar = document.createElement("div");
const left = document.createElement("div");
const right = document.createElement("div");
bar.className = timelineElementCssClass + " " + this.type;
left.className = classNameLeftHandle;
right.className = classNameRightHandle;
bar.appendChild(left);
bar.appendChild(right);
return bar;
}
protected typeColor(wp: WorkPackageResourceInterface): string {
let type = wp.type && wp.type.state.value;
if (type && type.color) {
return type.color;
}
return this.fallbackColor;
}
protected assignDate(wp: WorkPackageResourceInterface, attributeName: string, value: moment.Moment) {
if (value) {
wp[attributeName] = value.format("YYYY-MM-DD") as any;
}
}
/**
* Force the cursor to the given cursor type.
*/
protected forceCursor(cursor: string) {
jQuery(".hascontextmenu").css("cursor", cursor);
jQuery("." + timelineElementCssClass).css("cursor", cursor);
}
/**
* Changes the presentation of the work package.
*
* Known cases:
* 1. Display a clamp if this work package is a parent element
*/
checkForSpecialDisplaySituations(renderInfo: RenderInfo, bar: HTMLElement) {
const wp = renderInfo.workPackage;
// Cannot eddit the work package if it has children
if (!wp.isLeaf) {
bar.classList.add("-readonly");
}
// Display the parent as clamp-style when it has children in the table
if (this.workPackageTimeline.inHierarchyMode &&
hasChildrenInTable(wp, this.workPackageTimeline.workPackageTable)) {
bar.classList.add('-clamp-style');
bar.style.borderStyle = 'solid';
bar.style.borderWidth = '2px';
bar.style.borderColor = this.typeColor(wp);
bar.style.borderBottom = 'none';
bar.style.background = 'none';
}
}
private updateLeftRightMovedLabel(start: Moment, due: Moment) {
if (!this.TimezoneService) {
this.TimezoneService = $injectNow("TimezoneService");
}
if (this.dateDisplaysOnMouseMove.left && start) {
this.dateDisplaysOnMouseMove.left.innerText = this.TimezoneService.formattedDate(start);
}
if (this.dateDisplaysOnMouseMove.right && due) {
this.dateDisplaysOnMouseMove.right.innerText = this.TimezoneService.formattedDate(due);
}
}
}