From e74858b9d8595fc0c77ea52868be53f3fed7715e Mon Sep 17 00:00:00 2001 From: Roman Roelofsen Date: Wed, 25 Jan 2017 15:40:07 +0100 Subject: [PATCH] WIP Timeline: create start/due values with drag'n'drop --- .../work-package-cache.service.ts | 4 + .../cell-renderer/timeline-cell-renderer.ts | 30 +++-- .../timeline-milestone-cell-renderer.ts | 4 +- .../wp-timeline-cell-mouse-handler.ts | 113 ++++++++++++++++-- .../wp-table/timeline/wp-timeline-cell.ts | 24 ++-- 5 files changed, 145 insertions(+), 30 deletions(-) diff --git a/frontend/app/components/work-packages/work-package-cache.service.ts b/frontend/app/components/work-packages/work-package-cache.service.ts index 577259f9e2..67218f46f3 100644 --- a/frontend/app/components/work-packages/work-package-cache.service.ts +++ b/frontend/app/components/work-packages/work-package-cache.service.ts @@ -83,10 +83,14 @@ export class WorkPackageCacheService { } saveIfChanged(workPackage) { + console.log("saveIfChangedaaaaaaaaaaa"); + if (!(workPackage.dirty || workPackage.isNew)) { return this.$q.when(workPackage); } + console.log("saveIfChangedbbbbbbbbbbbbb"); + const deferred = this.$q.defer(); workPackage.save() .then(() => { diff --git a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts index d21929f789..d5f5e77651 100644 --- a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts +++ b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-cell-renderer.ts @@ -140,46 +140,50 @@ export class TimelineCellRenderer { * @return true, if the element should still be displayed. * false, if the element must be removed from the timeline. */ - public update(element: HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo: RenderInfo): boolean { - - console.log(wp); - + public update(timelineCell: HTMLElement, bar: HTMLDivElement, renderInfo: RenderInfo): boolean { + const wp = renderInfo.workPackage; // general settings - bar - element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; - element.style.backgroundColor = this.typeColor(renderInfo.workPackage); + bar.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; + 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(); - element.style.backgroundColor = "inherit"; + bar.style.backgroundColor = "inherit"; const color = this.typeColor(renderInfo.workPackage); - element.style.backgroundImage = `linear-gradient(90deg, ${color} 0%, rgba(255,255,255,0) 80%)`; + 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(); - element.style.backgroundColor = "inherit"; + bar.style.backgroundColor = "inherit"; const color = this.typeColor(renderInfo.workPackage); - element.style.backgroundImage = `linear-gradient(90deg, rgba(255,255,255,0) 0%, ${color} 100%)`; + bar.style.backgroundImage = `linear-gradient(90deg, rgba(255,255,255,0) 0%, ${color} 100%)`; } // offset left const offsetStart = start.diff(viewParams.dateDisplayStart, "days"); - element.style.left = calculatePositionValueForDayCount(viewParams, offsetStart); + bar.style.left = calculatePositionValueForDayCount(viewParams, offsetStart); // duration const duration = due.diff(start, "days") + 1; - element.style.width = calculatePositionValueForDayCount(viewParams, duration); + bar.style.width = calculatePositionValueForDayCount(viewParams, duration); // ensure minimum width if (!_.isNaN(start.valueOf()) || !_.isNaN(due.valueOf())) { - element.style.minWidth = "10px"; + bar.style.minWidth = "10px"; } return true; diff --git a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts index 2096514358..a5753cf39d 100644 --- a/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts +++ b/frontend/app/components/wp-table/timeline/cell-renderer/timeline-milestone-cell-renderer.ts @@ -65,7 +65,9 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { return "both"; } - public update(element:HTMLDivElement, wp: WorkPackageResourceInterface, renderInfo:RenderInfo): boolean { + public update(timelineCell: HTMLElement, element: HTMLDivElement, renderInfo: RenderInfo): boolean { + const wp = renderInfo.workPackage; + // abort if no start or due date if (!wp.date) { return false; diff --git a/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts b/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts index d69e8714f2..129cbd1995 100644 --- a/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts +++ b/frontend/app/components/wp-table/timeline/wp-timeline-cell-mouse-handler.ts @@ -29,27 +29,46 @@ import {timelineElementCssClass, RenderInfo} from "./wp-timeline"; import {WorkPackageCacheService} from "../../work-packages/work-package-cache.service"; import {WorkPackageTimelineTableController} from "./wp-timeline-container.directive"; import {TimelineCellRenderer} from "./cell-renderer/timeline-cell-renderer"; +import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-package-resource.service"; import IScope = angular.IScope; import Moment = moment.Moment; +const keyCodeESC = 27; + const classNameBar = "bar"; export const classNameLeftHandle = "leftHandle"; export const classNameRightHandle = "rightHandle"; +function createPlaceholderForEmptyCell() { + 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"; + return placeholder; +} + export function registerWorkPackageMouseHandler(this: void, + getRenderInfo: () => RenderInfo, workPackageTimeline: WorkPackageTimelineTableController, wpCacheService: WorkPackageCacheService, - bar: HTMLElement, + cell: HTMLElement, + bar: HTMLDivElement, renderer: TimelineCellRenderer, renderInfo: RenderInfo) { let startX: number = null; // also flag to signal active drag'n'drop let dateStates:any; - let jBody = jQuery("body"); + const jBody = jQuery("body"); + const placeholderForEmptyCell = createPlaceholderForEmptyCell(); + + // handle mouse move on cell + cell.onmousemove = handleMouseMoveOnEmptyCell; bar.onmousedown = (ev: MouseEvent) => { - mouseDownFn(ev); + workPackageMouseDownFn(ev); }; jBody.on("mouseup", () => { @@ -80,12 +99,12 @@ export function registerWorkPackageMouseHandler(this: void, function keyPressFn(ev: JQueryEventObject) { const kev: KeyboardEvent = ev as any; - if (kev.keyCode === 27) { // ESC + if (kev.keyCode === keyCodeESC) { deactivate(true); } } - function mouseDownFn(ev: MouseEvent) { + function workPackageMouseDownFn(ev: MouseEvent) { ev.preventDefault(); workPackageTimeline.disableViewParamsCalculation = true; @@ -98,6 +117,80 @@ export function registerWorkPackageMouseHandler(this: void, jBody.on("keyup", keyPressFn); } + function handleMouseMoveOnEmptyCell(ev: MouseEvent) { + const renderInfo = getRenderInfo(); + const wp = renderInfo.workPackage; + const start = moment(wp.startDate as any); + const due = moment(wp.dueDate as any); + const noStartDueValues = _.isNaN(start.valueOf()) && _.isNaN(due.valueOf()); + + if (!noStartDueValues) { + return; + } + + // placeholder logic + const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + // const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(days, "days"); + placeholderForEmptyCell.style.left = (days * renderInfo.viewParams.pixelPerDay) + "px"; + cell.appendChild(placeholderForEmptyCell); + cell.onmouseleave = () => { + placeholderForEmptyCell.remove(); + }; + + // create logic + cell.onmousedown = (ev) => { + console.log("create - mouse down"); + placeholderForEmptyCell.remove(); + ev.preventDefault(); + + bar.style.pointerEvents = "none"; + + const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + const clickStart = renderInfo.viewParams.dateDisplayStart.clone().add(days, "days"); + renderInfo.workPackage.startDate = clickStart.format("YYYY-MM-DD"); + renderInfo.workPackage.dueDate = clickStart.format("YYYY-MM-DD"); + renderer.update(cell, bar, renderInfo); + + function cancel(resetStartDueValues: boolean) { + console.log("create - cancel()"); + jBody.off(".create"); + + if (resetStartDueValues) { + renderInfo.workPackage.startDate = null; + renderInfo.workPackage.dueDate = null; + } + bar.style.pointerEvents = "auto"; + renderer.update(cell, bar, renderInfo); + + cell.onmousemove = handleMouseMoveOnEmptyCell; + } + + cell.onmousemove = (ev) => { + console.log("create - mouse move"); + const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + const currentEnd = renderInfo.viewParams.dateDisplayStart.clone().add(days, "days"); + renderInfo.workPackage.dueDate = currentEnd.format("YYYY-MM-DD"); + renderer.update(cell, bar, renderInfo); + }; + + cell.onmouseleave = () => { + cancel(true); + }; + + cell.onmouseup = () => { + cancel(false); + saveWorkPackage(renderInfo.workPackage); + }; + + jBody.on("keyup.create", (ev) => { + const kev: KeyboardEvent = ev as any; + if (kev.keyCode === keyCodeESC) { + cancel(true); + } + }); + }; + } + function deactivate(cancelled: boolean) { workPackageTimeline.disableViewParamsCalculation = false; @@ -122,10 +215,16 @@ export function registerWorkPackageMouseHandler(this: void, } // Persist the changes - wpCacheService.saveIfChanged(renderInfo.workPackage) + saveWorkPackage(renderInfo.workPackage); + } + + function saveWorkPackage(workPackage: WorkPackageResourceInterface) { + console.log("saveWorkPackage()"); + + wpCacheService.saveIfChanged(workPackage) .catch(() => { // Reset the changes on error - renderer.onCancel(renderInfo.workPackage); + renderer.onCancel(workPackage); }) .finally(() => { workPackageTimeline.refreshView(); diff --git a/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts b/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts index 137f100e58..9e7ff7eaa7 100644 --- a/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts +++ b/frontend/app/components/wp-table/timeline/wp-timeline-cell.ts @@ -33,6 +33,7 @@ import {registerWorkPackageMouseHandler} from "./wp-timeline-cell-mouse-handler" import {TimelineMilestoneCellRenderer} from "./cell-renderer/timeline-milestone-cell-renderer"; import {TimelineCellRenderer} from "./cell-renderer/timeline-cell-renderer"; import {Subscription} from "rxjs"; +import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-package-resource.service"; import IScope = angular.IScope; import Moment = moment.Moment; @@ -45,7 +46,10 @@ export class WorkPackageTimelineCell { private subscription: Subscription; - private element: HTMLDivElement = null; + private latestRenderInfo: RenderInfo; + + private wpElement: HTMLDivElement = null; + private elementShape: string = null; constructor(private workPackageTimeline: WorkPackageTimelineTableController, @@ -70,11 +74,11 @@ export class WorkPackageTimelineCell { private clear() { this.timelineCell.innerHTML = ""; - this.element = null; + this.wpElement = null; } private lazyInit(renderer: TimelineCellRenderer, renderInfo: RenderInfo) { - const wasRendered = this.element !== null && this.element.parentNode; + const wasRendered = this.wpElement !== null && this.wpElement.parentNode; // If already rendered with correct shape, ignore if (wasRendered && (this.elementShape === renderer.type)) { @@ -87,15 +91,17 @@ export class WorkPackageTimelineCell { } // Render the given element - this.element = renderer.render(renderInfo); + this.wpElement = renderer.render(renderInfo); this.elementShape = renderer.type; // Register the element - this.timelineCell.appendChild(this.element); + this.timelineCell.appendChild(this.wpElement); registerWorkPackageMouseHandler( + () => this.latestRenderInfo, this.workPackageTimeline, this.wpCacheService, - this.element, + this.timelineCell, + this.wpElement, renderer, renderInfo); @@ -130,14 +136,14 @@ export class WorkPackageTimelineCell { } private updateView(renderInfo: RenderInfo) { - const wp = renderInfo.workPackage; - const renderer = this.cellRenderer(wp); + this.latestRenderInfo = renderInfo; + const renderer = this.cellRenderer(renderInfo.workPackage); // Render initial element if necessary this.lazyInit(renderer, renderInfo); // Render the upgrade from renderInfo - const shouldBeDisplayed = renderer.update(this.element, wp, renderInfo); + const shouldBeDisplayed = renderer.update(this.timelineCell, this.wpElement, renderInfo); if (!shouldBeDisplayed) { this.clear(); }