From 41780ae1e2c35275e3bc643d7ec9f57fa5595e77 Mon Sep 17 00:00:00 2001 From: Roman Roelofsen Date: Thu, 26 Jan 2017 19:12:09 +0100 Subject: [PATCH] WIP Timeline: create start/due values with drag'n'drop --- .../work-package-cache.service.ts | 5 +- .../cell-renderer/timeline-cell-renderer.ts | 42 ++++++++- .../timeline-milestone-cell-renderer.ts | 70 +++++++++++---- .../wp-timeline-cell-mouse-handler.ts | 89 ++++++++----------- 4 files changed, 131 insertions(+), 75 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 67218f46f3..b2e4771e08 100644 --- a/frontend/app/components/work-packages/work-package-cache.service.ts +++ b/frontend/app/components/work-packages/work-package-cache.service.ts @@ -83,13 +83,14 @@ export class WorkPackageCacheService { } saveIfChanged(workPackage) { - console.log("saveIfChangedaaaaaaaaaaa"); + console.log("saveIfChanged"); if (!(workPackage.dirty || workPackage.isNew)) { + console.log(" - aborted"); return this.$q.when(workPackage); } - console.log("saveIfChangedbbbbbbbbbbbbb"); + console.log(" - pass"); const deferred = this.$q.defer(); workPackage.save() 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 d5f5e77651..381825cd6f 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 @@ -22,6 +22,27 @@ export class TimelineCellRenderer { return '#8CD1E8'; } + 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. @@ -47,8 +68,9 @@ export class TimelineCellRenderer { * depending on which initial date was set. */ public onDaysMoved(wp: WorkPackageResourceInterface, + dayUnderCursor: Moment, delta: number, - direction: "left" | "right" | "both"): CellDateMovement { + direction: "left" | "right" | "both" | "create" | "dragright"): CellDateMovement { const initialStartDate = wp.$pristine['startDate']; const initialDueDate = wp.$pristine['dueDate']; @@ -66,6 +88,8 @@ export class TimelineCellRenderer { 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 @@ -80,10 +104,14 @@ export class TimelineCellRenderer { return dates; } - public onMouseDown(ev: MouseEvent, renderInfo: RenderInfo, elem: HTMLElement): "left" | "right" | "both" { + public onMouseDown(ev: MouseEvent, + dateForCreate: string|null, + renderInfo: RenderInfo, + elem: HTMLElement): "left" | "right" | "both" | "dragright" | "create" { + renderInfo.workPackage.storePristine('startDate'); renderInfo.workPackage.storePristine('dueDate'); - let direction: "left" | "right" | "both"; + let direction: "left" | "right" | "both" | "create" | "dragright"; // Update the cursor and maybe set start/due values if (jQuery(ev.target).hasClass(classNameLeftHandle)) { @@ -93,7 +121,7 @@ export class TimelineCellRenderer { if (renderInfo.workPackage.startDate === null) { renderInfo.workPackage.startDate = renderInfo.workPackage.dueDate; } - } else if (jQuery(ev.target).hasClass(classNameRightHandle)) { + } else if (jQuery(ev.target).hasClass(classNameRightHandle) || dateForCreate) { // only right direction = "right"; this.forceCursor('e-resize'); @@ -108,6 +136,12 @@ export class TimelineCellRenderer { 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"); 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 a5753cf39d..7bf9c2ace5 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 @@ -10,20 +10,48 @@ interface CellMilestoneMovement { export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { - public get type():string { + public get type(): string { return 'milestone'; } - public get fallbackColor():string { + public get fallbackColor(): string { return '#C0392B'; } + public isEmpty(wp: WorkPackageResourceInterface) { + const date = moment(wp.date as any); + const noDateValue = _.isNaN(date.valueOf()); + return noDateValue; + } + + public displayPlaceholderUnderCursor(ev: MouseEvent, renderInfo: RenderInfo): HTMLElement { + const days = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + + const placeholder = document.createElement("div"); + placeholder.className = "timeline-element milestone"; + placeholder.style.pointerEvents = "none"; + placeholder.style.position = "absolute"; + placeholder.style.height = "1em"; + placeholder.style.width = "1em"; + placeholder.style.left = (days * renderInfo.viewParams.pixelPerDay) + "px"; + + const diamond = document.createElement("div"); + diamond.className = "diamond"; + diamond.style.backgroundColor = "#DDDDDD"; + diamond.style.left = "0.5em"; + diamond.style.height = "1em"; + diamond.style.width = "1em"; + placeholder.appendChild(diamond); + + return placeholder; + } + /** * Assign changed dates to the work package. * For generic work packages, assigns start and due date. * */ - public assignDateValues(wp: WorkPackageResourceInterface, dates:CellMilestoneMovement) { + public assignDateValues(wp: WorkPackageResourceInterface, dates: CellMilestoneMovement) { this.assignDate(wp, 'date', dates.date); this.updateMilestoneMovedLabel(dates.date); @@ -39,9 +67,13 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { /** * Handle movement by days of milestone. */ - public onDaysMoved(wp:WorkPackageResourceInterface, delta:number, direction: "left" | "right" | "both") { + public onDaysMoved(wp: WorkPackageResourceInterface, + dayUnderCursor: Moment, + delta: number, + direction: "left" | "right" | "both" | "create" | "dragright") { + const initialDate = wp.$pristine['date']; - let dates:CellMilestoneMovement = {}; + let dates: CellMilestoneMovement = {}; if (initialDate) { dates.date = moment(initialDate).add(delta, "days"); @@ -50,9 +82,19 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { return dates; } - public onMouseDown(ev: MouseEvent, renderInfo: RenderInfo, elem: HTMLElement): "left" | "right" | "both" { - this.forceCursor('ew-resize'); + public onMouseDown(ev: MouseEvent, + dateForCreate: string|null, + renderInfo: RenderInfo, + elem: HTMLElement): "left" | "right" | "both" | "create" | "dragright" { + + let direction: "left" | "right" | "both" | "create" | "dragright" = "both"; renderInfo.workPackage.storePristine('date'); + this.forceCursor('ew-resize'); + + if (dateForCreate) { + renderInfo.workPackage.date = dateForCreate; + direction = "create"; + } // create date label const dateInfo = document.createElement("div"); @@ -62,11 +104,13 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { this.updateMilestoneMovedLabel(moment(renderInfo.workPackage.date)); - return "both"; + return direction; } public update(timelineCell: HTMLElement, element: HTMLDivElement, renderInfo: RenderInfo): boolean { const wp = renderInfo.workPackage; + const viewParams = renderInfo.viewParams; + const date = moment(wp.date as any); // abort if no start or due date if (!wp.date) { @@ -75,16 +119,12 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { const diamond = jQuery(".diamond", element)[0]; - element.style.marginLeft = renderInfo.viewParams.scrollOffsetInPx + "px"; + element.style.marginLeft = viewParams.scrollOffsetInPx + "px"; element.style.width = '1em'; element.style.height = '1em'; diamond.style.width = '1em'; diamond.style.height = '1em'; - - diamond.style.backgroundColor = this.typeColor(renderInfo.workPackage); - - const viewParams = renderInfo.viewParams; - const date = moment(wp.date as any); + diamond.style.backgroundColor = this.typeColor(wp); // offset left const offsetStart = date.diff(viewParams.dateDisplayStart, "days"); @@ -97,7 +137,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { * Render a milestone element, a single day event with no resize, but * move functionality. */ - public render(renderInfo: RenderInfo):HTMLDivElement { + public render(renderInfo: RenderInfo): HTMLDivElement { const element = document.createElement("div"); element.className = timelineElementCssClass + " " + this.type; 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 129cbd1995..9fd32091cf 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 @@ -40,16 +40,6 @@ 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, @@ -61,8 +51,8 @@ export function registerWorkPackageMouseHandler(this: void, let startX: number = null; // also flag to signal active drag'n'drop let dateStates:any; + let placeholderForEmptyCell: HTMLElement; const jBody = jQuery("body"); - const placeholderForEmptyCell = createPlaceholderForEmptyCell(); // handle mouse move on cell cell.onmousemove = handleMouseMoveOnEmptyCell; @@ -86,13 +76,16 @@ export function registerWorkPackageMouseHandler(this: void, wpCacheService.updateWorkPackage(wp); } - function createMouseMoveFn(direction: "left" | "right" | "both") { + function createMouseMoveFn(direction: "left" | "right" | "both" | "create" | "dragright") { return (ev: JQueryEventObject) => { const mev: MouseEvent = ev as any; const distance = Math.floor((mev.clientX - startX) / renderInfo.viewParams.pixelPerDay); const days = distance < 0 ? distance + 1 : distance; - dateStates = renderer.onDaysMoved(renderInfo.workPackage, days, direction); + const offsetDayCurrent = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayCurrent, "days"); + + dateStates = renderer.onDaysMoved(renderInfo.workPackage, dayUnderCursor, days, direction); applyDateValues(dateStates); } } @@ -111,7 +104,7 @@ export function registerWorkPackageMouseHandler(this: void, startX = ev.clientX; // Determine what attributes of the work package should be changed - const direction = renderer.onMouseDown(ev, renderInfo, bar); + const direction = renderer.onMouseDown(ev, null, renderInfo, bar); jBody.on("mousemove", createMouseMoveFn(direction)); jBody.on("keyup", keyPressFn); @@ -120,84 +113,74 @@ export function registerWorkPackageMouseHandler(this: void, 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) { + if (!renderer.isEmpty(wp)) { 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"; + placeholderForEmptyCell && placeholderForEmptyCell.remove(); + placeholderForEmptyCell = renderer.displayPlaceholderUnderCursor(ev, renderInfo); cell.appendChild(placeholderForEmptyCell); + + // abort if mouse leaves cell cell.onmouseleave = () => { placeholderForEmptyCell.remove(); }; // create logic cell.onmousedown = (ev) => { - console.log("create - mouse down"); placeholderForEmptyCell.remove(); - ev.preventDefault(); - bar.style.pointerEvents = "none"; + ev.preventDefault(); - 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"); + const offsetDayStart = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + const clickStart = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayStart, "days"); + const dateForCreate = clickStart.format("YYYY-MM-DD"); + const mouseDownType = renderer.onMouseDown(ev, dateForCreate, renderInfo, bar); 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; + if (mouseDownType === "create") { + deactivate(false); + return; } 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"); + const offsetDayCurrent = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay); + const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayCurrent, "days"); + const widthInDays = offsetDayCurrent - offsetDayStart; + const moved = renderer.onDaysMoved(wp, dayUnderCursor, widthInDays, mouseDownType); + renderer.assignDateValues(wp, moved); renderer.update(cell, bar, renderInfo); }; cell.onmouseleave = () => { - cancel(true); + deactivate(true); }; cell.onmouseup = () => { - cancel(false); - saveWorkPackage(renderInfo.workPackage); + deactivate(false); }; - jBody.on("keyup.create", (ev) => { + jBody.on("keyup", (ev) => { const kev: KeyboardEvent = ev as any; if (kev.keyCode === keyCodeESC) { - cancel(true); + deactivate(true); } }); + }; } function deactivate(cancelled: boolean) { workPackageTimeline.disableViewParamsCalculation = false; - if (startX == null) { - return; - } + // if (startX == null) { + // return; + // } + cell.onmousemove = handleMouseMoveOnEmptyCell; + bar.style.pointerEvents = "auto"; jBody.off("mousemove"); jBody.off("keyup"); jQuery(".hascontextmenu").css("cursor", "context-menu"); @@ -219,8 +202,6 @@ export function registerWorkPackageMouseHandler(this: void, } function saveWorkPackage(workPackage: WorkPackageResourceInterface) { - console.log("saveWorkPackage()"); - wpCacheService.saveIfChanged(workPackage) .catch(() => { // Reset the changes on error