commit
205aab63b2
@ -0,0 +1,45 @@ |
||||
.wp-timeline--slider-wrapper |
||||
position: absolute |
||||
z-index: 101 |
||||
|
||||
|
||||
// Slider |
||||
.noUi-horizontal.wp-timeline--slider |
||||
padding-right: 32px |
||||
|
||||
.noUi-handle |
||||
left: -1px |
||||
height: calc(1rem - 2px) |
||||
top: 0 |
||||
box-shadow: none |
||||
background: $gray |
||||
|
||||
&:before, &:after |
||||
height: calc(1rem - 6px) |
||||
top: 1px |
||||
background: $light-gray |
||||
|
||||
&:before |
||||
left: calc(50% - 1px) |
||||
&:after |
||||
left: calc(50% + 1px) |
||||
|
||||
.noUi-origin |
||||
right: -32px |
||||
|
||||
|
||||
// Scroll slider |
||||
.wp-timeline--slider |
||||
position: relative |
||||
height: 1rem |
||||
border-radius: none |
||||
|
||||
.ui-slider-handle |
||||
position: absolute |
||||
top: 0 |
||||
width: 1rem |
||||
height: 100% |
||||
cursor: default |
||||
touch-action: none |
||||
border-radius: none |
||||
background: $primary-color |
@ -0,0 +1,55 @@ |
||||
@import '_slider' |
||||
@import 'elements/_bar' |
||||
@import 'elements/_milestone' |
||||
|
||||
.wp-timeline--th |
||||
min-width: 600px !important |
||||
|
||||
.wp-timeline--dummy-controls |
||||
position: absolute |
||||
right: 20px |
||||
background: white |
||||
top: calc(#{$generic-table--header-height} + 10px) |
||||
border: 1px solid #D7D7D7 |
||||
box-shadow: 0 5px 3px -4px #DDDDDD |
||||
// Should match the thead z-index |
||||
z-index: 200 |
||||
|
||||
// Ensure even padding around buttons |
||||
padding: 5px |
||||
line-height: 1 |
||||
|
||||
button |
||||
margin: 0 |
||||
|
||||
.wp-timeline-header-container.generic-table--sort-header-outer |
||||
padding: 0 |
||||
z-index: 100 |
||||
margin-left: -5px |
||||
height: $generic-table--header-height |
||||
|
||||
&:hover |
||||
// Show scroll buttons on hover |
||||
.wp-timeline--scroll-btn |
||||
display: block |
||||
|
||||
.wp-timeline--scroll-wrapper |
||||
border-left: 5px solid $gray |
||||
|
||||
// The scroll wrapper is resized to span the entire table. |
||||
// Without this, scroll is caught by the header. |
||||
pointer-events: none |
||||
position: relative |
||||
|
||||
// Overflow must be hidden since the width of the column is fixed. |
||||
// Since height is explicitly set, this is effectively |
||||
// overflow-x: hidden / overflow-y: visible |
||||
overflow: hidden |
||||
|
||||
.wp-timeline-cell |
||||
border-bottom: none !important |
||||
position: relative |
||||
z-index: 100 |
||||
|
||||
&.-collapsed |
||||
display: none |
@ -0,0 +1,49 @@ |
||||
.timeline-element.bar |
||||
position: relative |
||||
height: 1em |
||||
border-radius: 2px |
||||
float: left |
||||
z-index: 50 |
||||
cursor: ew-resize |
||||
|
||||
.leftHandle |
||||
position: absolute |
||||
left: 0 |
||||
top: 0 |
||||
width: 20px |
||||
max-width: 20% |
||||
height: 100% |
||||
cursor: w-resize |
||||
//&:hover |
||||
// background-image: linear-gradient(90deg, #6c6c6c 0%, rgba(255,255,255,0) 80%) |
||||
|
||||
.rightHandle |
||||
position: absolute |
||||
right: 0 |
||||
top: 0 |
||||
width: 20px |
||||
max-width: 20% |
||||
height: 100% |
||||
cursor: e-resize |
||||
//&:hover |
||||
// background-image: linear-gradient(90deg, rgba(255,255,255,0) 0%, #6c6c6c 80%) |
||||
|
||||
.leftDateDisplay |
||||
position: absolute |
||||
background-color: #f4f4f4 |
||||
border: 1px solid #d4d4d4 |
||||
border-radius: 5px |
||||
top: -5px |
||||
left: -85px |
||||
padding: 0 5px 0 5px |
||||
font-size: 10px |
||||
|
||||
.rightDateDisplay |
||||
position: absolute |
||||
background-color: #f4f4f4 |
||||
border: 1px solid #d4d4d4 |
||||
border-radius: 5px |
||||
top: -5px |
||||
right: -85px |
||||
padding: 0 5px 0 5px |
||||
font-size: 10px |
@ -0,0 +1,20 @@ |
||||
.timeline-element.milestone |
||||
position: relative |
||||
z-index: 50 |
||||
cursor: ew-resize |
||||
|
||||
.diamond |
||||
position: absolute |
||||
transform: rotate(45deg) |
||||
transform-origin: center center |
||||
|
||||
.rightDateDisplay |
||||
position: absolute |
||||
background-color: #f4f4f4 |
||||
border: 1px solid #d4d4d4 |
||||
border-radius: 5px |
||||
top: -5px |
||||
right: -85px |
||||
padding: 0 5px 0 5px |
||||
font-size: 10px |
||||
|
@ -0,0 +1,75 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {HalResource} from './hal-resource.service'; |
||||
import {WorkPackageResource} from './work-package-resource.service'; |
||||
import {CollectionResource} from './collection-resource.service'; |
||||
import {HalRequestService} from './../hal-request/hal-request.service'; |
||||
import {opApiModule} from '../../../../angular-modules'; |
||||
import {States} from '../../../states.service'; |
||||
import {State} from './../../../../helpers/reactive-fassade'; |
||||
|
||||
var states: States; |
||||
var halRequest: HalRequestService; |
||||
var v3Path:any; |
||||
|
||||
export class TypeResource extends HalResource { |
||||
public color:string; |
||||
|
||||
public static loadAll() { |
||||
const types = states.types; |
||||
const typeUrl = v3Path.types(); |
||||
|
||||
halRequest.get(typeUrl).then((result:CollectionResource) => { |
||||
result.elements.forEach((value:TypeResource) => { |
||||
types.get(value.href as string).put(value); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public get state() { |
||||
return states.types.get(this.href as string); |
||||
} |
||||
} |
||||
|
||||
function typeResource(...args:any[]) { |
||||
[ |
||||
states, |
||||
halRequest, |
||||
v3Path |
||||
] = args; |
||||
return TypeResource; |
||||
} |
||||
|
||||
typeResource.$inject = [ |
||||
'states', |
||||
'halRequest', |
||||
'v3Path' |
||||
]; |
||||
|
||||
opApiModule.factory('TypeResource', typeResource); |
@ -1,235 +0,0 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {opUiComponentsModule} from "../../../angular-modules"; |
||||
|
||||
export class interactiveTableController { |
||||
|
||||
// Width of the table and container last applied by this directive
|
||||
private lastWidthSet:any; |
||||
|
||||
// Width of the scrollbar to account for
|
||||
private scrollBarWidth = 16; |
||||
|
||||
constructor(protected $element:ng.IAugmentedJQuery, |
||||
protected $timeout:ng.ITimeoutService, |
||||
protected $interval:ng.IIntervalService, |
||||
protected $scope:ng.IScope, |
||||
protected $window:ng.IWindowService) { |
||||
'ngInject'; |
||||
|
||||
// Initialize the interactive table
|
||||
$timeout(() => { |
||||
this.cloneSpacer(); |
||||
this.setTableWidths(); |
||||
}); |
||||
|
||||
// Watch for global resize events
|
||||
// (e.g., table should expand to full width of window)
|
||||
angular.element($window).on('resize', _.debounce(() => this.setTableWidths(), 50)); |
||||
|
||||
// Watch for changes in state
|
||||
// (e.g., detail view opening)
|
||||
$scope.$on('$stateChangeSuccess', () => { |
||||
$timeout(() => this.setTableWidths(), 200); |
||||
}); |
||||
|
||||
// Watch for changes in the project navigation menu
|
||||
$scope.$on('openproject.layout.navigationToggled', () => { |
||||
$timeout(() => this.setTableWidths(), 200); |
||||
}); |
||||
|
||||
// Watch for updates not coming through the above methods
|
||||
// e.g., attributes in the table inititally rendering such as the progress bar
|
||||
// will cause the table width to expand
|
||||
var stopInterval = $interval(() => this.refreshWhenNeeded(), 1000, 0, false); |
||||
$scope.$on('$destroy', () => { |
||||
$interval.cancel(stopInterval); |
||||
}); |
||||
|
||||
} |
||||
|
||||
private get table() { |
||||
return this.$element; |
||||
} |
||||
|
||||
private get visible() { |
||||
return this.table.is(':visible'); |
||||
} |
||||
|
||||
private getInnerContainer() { |
||||
return this.table.parent('.generic-table--results-container'); |
||||
} |
||||
|
||||
private getOuterContainer() { |
||||
return this.table.closest('.generic-table--container'); |
||||
} |
||||
|
||||
private isWorkPackagesTable () { |
||||
return this.table.closest('.work-package-table--container').length !== 0; |
||||
} |
||||
|
||||
private getBackgrounds() { |
||||
return this.getInnerContainer() |
||||
.find('.generic-table--header-background,.generic-table--footer-background'); |
||||
} |
||||
|
||||
private getHeadersFooters() { |
||||
return this.table.find( |
||||
'.generic-table--sort-header-outer,' + |
||||
'.generic-table--header-outer,' + |
||||
'.generic-table--footer-outer' |
||||
); |
||||
} |
||||
|
||||
private cloneSpacer() { |
||||
this.getHeadersFooters().each((i, el) => { |
||||
var element = angular.element(el); |
||||
var html = element.text(); |
||||
var hiddenForSighted = element.find('.hidden-for-sighted').text(); |
||||
|
||||
html = html.replace(hiddenForSighted, ''); |
||||
|
||||
var spacerHtml = '<div class="generic-table--column-spacer">' + html + '</div>'; |
||||
|
||||
var newElement = angular.element(spacerHtml); |
||||
|
||||
newElement.appendTo(element.parent()); |
||||
}); |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Return the actual width of the table element (including scrollbar if necessary) |
||||
*/ |
||||
private currentWidth() { |
||||
return this.table.width(); |
||||
} |
||||
|
||||
/** |
||||
* Re-adjust the table's container widths |
||||
*/ |
||||
private setTableContainerWidths() { |
||||
var width = this.currentWidth(); |
||||
|
||||
// account for a possible scrollbar
|
||||
if (width > document.documentElement.clientWidth - this.scrollBarWidth) { |
||||
width += this.scrollBarWidth; |
||||
} |
||||
|
||||
this.lastWidthSet = width; |
||||
if (width > this.getOuterContainer().width()) { |
||||
// force containers to the width of the table
|
||||
this.getInnerContainer().width(width); |
||||
this.getBackgrounds().width(width); |
||||
} else { |
||||
// ensure table stretches to container sizes
|
||||
this.getInnerContainer().css('width', '100%'); |
||||
this.getBackgrounds().css('width', '100%'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Correct header and footer widths after table is updated. |
||||
*/ |
||||
private setHeaderFooterWidths() { |
||||
this.getHeadersFooters().each((i, el) => { |
||||
var element = angular.element(el); |
||||
var spacer = element.parent(); |
||||
var width = spacer.width(); |
||||
|
||||
if (width !== 0) { |
||||
element.css('width', width + 'px') |
||||
.parent().css('width', width + 'px'); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Reset all table element widths to auto. |
||||
*/ |
||||
private invalidateWidths() { |
||||
this.getInnerContainer().css('width', 'auto'); |
||||
this.getBackgrounds().css('width', 'auto'); |
||||
this.getHeadersFooters().each((i, el) => { |
||||
angular.element(el).css('width', 'auto'); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Update the table itself and its containers after an external event. |
||||
* (Resize, column content changes) |
||||
*/ |
||||
private setTableWidths() { |
||||
this.invalidateWidths(); |
||||
this.setTableContainerWidths(); |
||||
this.setHeaderFooterWidths(); |
||||
}; |
||||
|
||||
private refreshWhenNeeded() { |
||||
if(!this.visible) { |
||||
return; |
||||
} |
||||
|
||||
// If the inner width of the table changed due to some outer event,
|
||||
// adjust accordingly.
|
||||
var actualWidth = this.currentWidth(); |
||||
if (Math.abs(this.lastWidthSet - actualWidth) >= 10) { |
||||
return this.setTableWidths(); |
||||
} |
||||
|
||||
// If any of the outer header widths changed,
|
||||
// adjust the fixed headers.
|
||||
this.getHeadersFooters().each((i, el) => { |
||||
var element = angular.element(el); |
||||
var width = element.parent().width(); |
||||
|
||||
if (width !== 0 && element.outerWidth() !== width) { |
||||
return this.setTableWidths(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
} |
||||
|
||||
function interactiveTable() { |
||||
return { |
||||
restrict: 'A', |
||||
controller: interactiveTableController, |
||||
bindToController: true, |
||||
|
||||
link: function(scope:ng.IScope, element:ng.IAugmentedJQuery) { |
||||
if (element.filter('table').length === 0) { |
||||
throw 'needs to be defined on a \'table\' tag'; |
||||
} |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
opUiComponentsModule.directive('interactiveTable', interactiveTable); |
||||
|
@ -0,0 +1,78 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {opUiComponentsModule} from '../../../angular-modules'; |
||||
import {opApiModule} from './../../../angular-modules'; |
||||
import {debugLog} from '../../../helpers/debug_output'; |
||||
|
||||
function requestInterval(fn:Function, delay:number) { |
||||
let start:number = new Date().getTime(); |
||||
let handle:any = {}; |
||||
function loop() { |
||||
handle.value = window.requestAnimationFrame(loop); |
||||
var current = new Date().getTime(), |
||||
delta = current - start; |
||||
if (delta >= delay) { |
||||
fn(); |
||||
start = new Date().getTime(); |
||||
} |
||||
} |
||||
handle.value = window.requestAnimationFrame(loop); |
||||
return handle; |
||||
}; |
||||
|
||||
export const opDimensionEventName = 'op:resize'; |
||||
|
||||
function detectDimensionChanges($window:ng.IWindowService) { |
||||
return { |
||||
restrict: 'A', |
||||
link: function(scope:ng.IScope, element:ng.IAugmentedJQuery, attr:ng.IAttributes) { |
||||
const el = element[0]; |
||||
|
||||
let height = 0, width = 0; |
||||
requestInterval(() => { |
||||
|
||||
let newHeight = el.offsetHeight; |
||||
let newWidth = el.offsetWidth; |
||||
|
||||
if (newHeight !== height || |
||||
newWidth !== width) { |
||||
|
||||
debugLog('Dimension change detected on ', element); |
||||
element.trigger(opDimensionEventName); |
||||
|
||||
height = newHeight; |
||||
width = newWidth; |
||||
} |
||||
}, 1000); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
opUiComponentsModule.directive('detectDimensionChanges', detectDimensionChanges); |
||||
|
@ -0,0 +1,86 @@ |
||||
import {opApiModule} from './../../../angular-modules'; |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {opUiComponentsModule} from '../../../angular-modules'; |
||||
|
||||
interface DragScrollData { |
||||
delta: { |
||||
x: number, |
||||
y: number |
||||
}; |
||||
|
||||
|
||||
} |
||||
|
||||
function opDragScroll() { |
||||
return { |
||||
restrict: 'A', |
||||
link: function(scope:ng.IScope, element:ng.IAugmentedJQuery, attr:ng.IAttributes) { |
||||
const eventName = 'op:dragscroll'; |
||||
|
||||
// Is mouse down?
|
||||
var mousedown = false; |
||||
|
||||
// Position of last mousedown
|
||||
var mousedownX:number, mousedownY:number; |
||||
|
||||
// Mousedown: Potential drag start
|
||||
element.on('mousedown', (evt:JQueryEventObject) => { |
||||
setTimeout(() => { |
||||
mousedown = true; |
||||
mousedownX = evt.clientX; |
||||
mousedownY = evt.clientY; |
||||
}, 50, false); |
||||
}); |
||||
|
||||
// Mouseup: Potential drag stop
|
||||
element.on('mouseup', () => { mousedown = false; }); |
||||
|
||||
// Mousemove: Report movement if mousedown
|
||||
element.on('mousemove', (evt:JQueryEventObject) => { |
||||
if (!mousedown) { |
||||
return; |
||||
} |
||||
|
||||
// Trigger drag scroll event
|
||||
element.trigger(eventName, { |
||||
x: evt.clientX - mousedownX, |
||||
y: evt.clientY - mousedownY |
||||
}); |
||||
|
||||
// Update last mouse position
|
||||
mousedownX = evt.clientX; |
||||
mousedownY = evt.clientY; |
||||
}); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
opUiComponentsModule.directive('opDragScroll', opDragScroll); |
||||
|
@ -1,3 +1,3 @@ |
||||
<div id="work-packages-index"> |
||||
<div ui-view class="work-packages--page-container"></div> |
||||
<div ui-view wp-timeline-container class="work-packages--page-container"></div> |
||||
</div> |
||||
|
@ -0,0 +1,12 @@ |
||||
<button id="{{ ::vm.buttonId }}" |
||||
class="button" |
||||
title="{{ vm.label }}" |
||||
ng-click="vm.performAction()" |
||||
ng-attr-accesskey="{{ ::vm.accessKey }}" |
||||
ng-disabled="vm.disabled" |
||||
ng-class="{ '-active': vm.isActive() }" |
||||
aria-label="{{ vm.label }}"> |
||||
<i class="{{ ::vm.iconClass }} button--icon"></i> |
||||
<span class="button--text" ng-bind="::vm.buttonText"></span> |
||||
<span class="badge -secondary">{{ vm.filterCount }}</span> |
||||
</button> |
@ -0,0 +1,75 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {wpButtonsModule} from '../../../angular-modules'; |
||||
import {WorkPackageButtonController, wpButtonDirective} from '../wp-buttons.module'; |
||||
import {WorkPackageTimelineTableController} from './../../wp-table/timeline/wp-timeline-container.directive'; |
||||
|
||||
export class WorkPackageTimelineButtonController extends WorkPackageButtonController { |
||||
public wpTimelineContainer:WorkPackageTimelineTableController ; |
||||
|
||||
public buttonId:string = 'work-packages-timeline-toggle-button'; |
||||
public iconClass:string = 'icon-view-timeline'; |
||||
|
||||
constructor(public I18n:op.I18n) { |
||||
'ngInject'; |
||||
|
||||
super(I18n); |
||||
} |
||||
|
||||
public get labelKey():string { |
||||
return 'js.button_timeline'; |
||||
} |
||||
|
||||
public isToggle():boolean { |
||||
return true; |
||||
} |
||||
|
||||
public isActive():boolean { |
||||
return this.wpTimelineContainer && this.wpTimelineContainer.visible; |
||||
} |
||||
|
||||
public performAction() { |
||||
this.wpTimelineContainer.toggle(); |
||||
} |
||||
} |
||||
|
||||
function wpTimelineToggleButton():ng.IDirective { |
||||
return wpButtonDirective({ |
||||
require: '^wpTimelineContainer', |
||||
controller: WorkPackageTimelineButtonController, |
||||
link: (scope:any, |
||||
attr:ng.IAttributes, |
||||
element:ng.IAugmentedJQuery, |
||||
wpTimelineContainer:WorkPackageTimelineTableController) => { |
||||
scope.vm.wpTimelineContainer = wpTimelineContainer; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
wpButtonsModule.directive('wpTimelineToggleButton', wpTimelineToggleButton); |
@ -1,36 +0,0 @@ |
||||
import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; |
||||
import {locateRow} from '../../helpers/wp-table-row-helpers'; |
||||
import {WorkPackageTable} from '../../wp-fast-table'; |
||||
import {WorkPackageTableRow} from '../../wp-table.interfaces'; |
||||
import {SingleRowBuilder} from './single-row-builder'; |
||||
|
||||
export class EditingRowBuilder extends SingleRowBuilder { |
||||
|
||||
/** |
||||
* Refresh a row that is currently being edited, that is, some edit fields may be open |
||||
*/ |
||||
public refreshEditing(row:WorkPackageTableRow, editForm:WorkPackageEditForm):HTMLElement|null { |
||||
// Get the row for the WP if refreshing existing
|
||||
const rowElement:HTMLElement|null = row.element || locateRow(row.workPackageId); |
||||
if (!rowElement) { |
||||
return null; |
||||
} |
||||
|
||||
// Detach all existing columns
|
||||
let tds = jQuery(rowElement).find('td').detach(); |
||||
|
||||
// Iterate all columns, reattaching or rendering new columns
|
||||
this.columns.forEach((column:string) => { |
||||
let oldTd = tds.filter(`td.${column}`); |
||||
|
||||
// Reattach the column if its currently being edited
|
||||
if (editForm.activeFields[column] && oldTd.length) { |
||||
rowElement.appendChild(oldTd[0]); |
||||
} else { |
||||
this.buildCell(row.object, column, rowElement); |
||||
} |
||||
}); |
||||
|
||||
return rowElement; |
||||
} |
||||
} |
@ -0,0 +1,53 @@ |
||||
import {wpCellTdClassName} from '../cell-builder'; |
||||
import {timelineCellClassName} from '../timeline-cell-builder'; |
||||
import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; |
||||
import {locateRow} from '../../helpers/wp-table-row-helpers'; |
||||
import {WorkPackageTable} from '../../wp-fast-table'; |
||||
import {WorkPackageTableRow} from '../../wp-table.interfaces'; |
||||
import {SingleRowBuilder} from './single-row-builder'; |
||||
|
||||
import {detailsLinkClassName} from '../details-link-builder'; |
||||
|
||||
export class RowRefreshBuilder extends SingleRowBuilder { |
||||
|
||||
/** |
||||
* Refresh a row that is currently being edited, that is, some edit fields may be open |
||||
*/ |
||||
public refreshRow(row:WorkPackageTableRow, editForm:WorkPackageEditForm|null):HTMLElement|null { |
||||
// Get the row for the WP if refreshing existing
|
||||
const rowElement = row.element || locateRow(row.workPackageId); |
||||
if (!rowElement) { |
||||
return null; |
||||
} |
||||
|
||||
// Iterate all columns, reattaching or rendering new columns
|
||||
const jRow = jQuery(rowElement); |
||||
|
||||
// Detach all current edit cells
|
||||
const cells = jRow.find(`.${wpCellTdClassName}`).detach(); |
||||
|
||||
// Remember the order of all new edit cells
|
||||
const newCells:HTMLElement[] = []; |
||||
|
||||
this.columns.forEach((column:string) => { |
||||
const oldTd = jRow.find(`td.${column}`); |
||||
|
||||
// Skip the replacement of the column if this is being edited.
|
||||
if (this.isColumnBeingEdited(editForm, column)) { |
||||
newCells.push(oldTd[0]); |
||||
return; |
||||
} |
||||
|
||||
// Otherwise, refresh that cell and append it
|
||||
const cell = this.buildCell(row.object, column); |
||||
newCells.push(cell); |
||||
}); |
||||
|
||||
jRow.prepend(newCells); |
||||
return rowElement; |
||||
} |
||||
|
||||
private isColumnBeingEdited(editForm:WorkPackageEditForm|null, column:string) { |
||||
return editForm && editForm.activeFields[column]; |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; |
||||
import {WorkPackageTimelineCell} from '../../wp-table/timeline/wp-timeline-cell'; |
||||
import {State} from '../../../helpers/reactive-fassade'; |
||||
import {UiStateLinkBuilder} from './ui-state-link-builder'; |
||||
import {WorkPackageTimelineTableController} from '../../wp-table/timeline/wp-timeline-container.directive'; |
||||
import {States} from '../../states.service'; |
||||
import {WorkPackageResource} from './../../api/api-v3/hal-resources/work-package-resource.service'; |
||||
import {DisplayField} from './../../wp-display/wp-display-field/wp-display-field.module'; |
||||
import {injectorBridge} from '../../angular/angular-injector-bridge.functions'; |
||||
export const timelineCellClassName = 'wp-timeline-cell'; |
||||
export const timelineCollapsedClassName = '-collapsed'; |
||||
|
||||
export class TimelineCellBuilder { |
||||
|
||||
public states:States; |
||||
public wpCacheService:WorkPackageCacheService; |
||||
|
||||
constructor() { |
||||
injectorBridge(this); |
||||
} |
||||
|
||||
public get isVisible():boolean { |
||||
return this.states.table.timelineVisible.getCurrentValue() || false; |
||||
} |
||||
|
||||
public get timelineInstance():WorkPackageTimelineTableController { |
||||
return this.states.timeline.getCurrentValue()!; |
||||
} |
||||
|
||||
public build(workPackage:WorkPackageResource):HTMLElement { |
||||
const td = document.createElement('td'); |
||||
td.classList.add(timelineCellClassName, '-max'); |
||||
|
||||
if (!this.isVisible) { |
||||
td.classList.add(timelineCollapsedClassName); |
||||
} |
||||
|
||||
this.buildTimelineCell(td, workPackage); |
||||
|
||||
return td; |
||||
} |
||||
|
||||
public buildTimelineCell(cell:HTMLElement, workPackage:WorkPackageResource):void { |
||||
// required data for timeline cell
|
||||
const timelineCell = new WorkPackageTimelineCell( |
||||
this.timelineInstance, |
||||
this.wpCacheService, |
||||
this.states, |
||||
workPackage.id, |
||||
cell |
||||
); |
||||
|
||||
// show timeline cell
|
||||
timelineCell.activate(); |
||||
} |
||||
} |
||||
|
||||
TimelineCellBuilder.$inject = ['states', 'wpCacheService']; |
@ -0,0 +1,33 @@ |
||||
import {States} from '../../../states.service'; |
||||
import { |
||||
TimelineCellBuilder, |
||||
timelineCellClassName, |
||||
timelineCollapsedClassName |
||||
} from '../../builders/timeline-cell-builder'; |
||||
import {WorkPackageTableSelection} from '../../state/wp-table-selection.service'; |
||||
import {injectorBridge} from '../../../angular/angular-injector-bridge.functions'; |
||||
import {WPTableRowSelectionState} from '../../wp-table.interfaces'; |
||||
import {WorkPackageTable} from '../../wp-fast-table'; |
||||
|
||||
export class TimelineTransformer { |
||||
public states:States; |
||||
public timelineCellBuilder = new TimelineCellBuilder(); |
||||
|
||||
constructor(table:WorkPackageTable) { |
||||
injectorBridge(this); |
||||
|
||||
this.states.table.timelineVisible |
||||
.observeUntil(this.states.table.stopAllSubscriptions).subscribe((visible:boolean) => { |
||||
this.renderVisibility(visible); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Update all currently visible rows to match the selection state. |
||||
*/ |
||||
private renderVisibility(visible:boolean) { |
||||
jQuery(`.${timelineCellClassName}`).toggleClass(timelineCollapsedClassName, !visible); |
||||
} |
||||
} |
||||
|
||||
TimelineTransformer.$inject = ['states']; |
@ -0,0 +1,306 @@ |
||||
import {WorkPackageResourceInterface} from "../../../api/api-v3/hal-resources/work-package-resource.service"; |
||||
import { |
||||
RenderInfo, calculatePositionValueForDayCount, timelineElementCssClass, |
||||
calculatePositionValueForDayCountinPx |
||||
} from "../wp-timeline"; |
||||
import {classNameLeftHandle, classNameRightHandle} from "../wp-timeline-cell-mouse-handler"; |
||||
import * as moment from 'moment'; |
||||
import Moment = moment.Moment; |
||||
|
||||
interface CellDateMovement { |
||||
// Target values to move work package to
|
||||
startDate?: moment.Moment; |
||||
dueDate?: moment.Moment; |
||||
} |
||||
|
||||
export class TimelineCellRenderer { |
||||
|
||||
protected dateDisplaysOnMouseMove: {left?: HTMLElement; right?: HTMLElement} = {}; |
||||
|
||||
public get type(): string { |
||||
return 'bar'; |
||||
} |
||||
|
||||
public get fallbackColor(): string { |
||||
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. |
||||
* |
||||
*/ |
||||
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" { |
||||
|
||||
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.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(); |
||||
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())) { |
||||
bar.style.minWidth = "30px"; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
getLeftmostPosition(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 calculatePositionValueForDayCountinPx(renderInfo.viewParams, offsetStart); |
||||
} |
||||
|
||||
getRightmostPosition(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 calculatePositionValueForDayCountinPx(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.getCurrentValue(); |
||||
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); |
||||
} |
||||
|
||||
private updateLeftRightMovedLabel(start: Moment, due: Moment) { |
||||
if (this.dateDisplaysOnMouseMove.left && start) { |
||||
this.dateDisplaysOnMouseMove.left.innerText = start.format("L"); |
||||
} |
||||
|
||||
if (this.dateDisplaysOnMouseMove.right && due) { |
||||
this.dateDisplaysOnMouseMove.right.innerText = due.format("L"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,172 @@ |
||||
import {WorkPackageResourceInterface} from "../../../api/api-v3/hal-resources/work-package-resource.service"; |
||||
import {TimelineCellRenderer} from "./timeline-cell-renderer"; |
||||
import { |
||||
RenderInfo, |
||||
calculatePositionValueForDayCount, |
||||
timelineElementCssClass, |
||||
calculatePositionValueForDayCountinPx |
||||
} from "../wp-timeline"; |
||||
import * as moment from "moment"; |
||||
import Moment = moment.Moment; |
||||
|
||||
interface CellMilestoneMovement { |
||||
// Target value to move milestone to
|
||||
date?: moment.Moment; |
||||
} |
||||
|
||||
export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { |
||||
|
||||
public get type(): string { |
||||
return 'milestone'; |
||||
} |
||||
|
||||
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.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) { |
||||
this.assignDate(wp, 'date', dates.date!); |
||||
|
||||
this.updateMilestoneMovedLabel(dates.date!); |
||||
} |
||||
|
||||
/** |
||||
* Restore the original date, if any was set. |
||||
*/ |
||||
public onCancel(wp: WorkPackageResourceInterface) { |
||||
wp.restoreFromPristine('date'); |
||||
} |
||||
|
||||
/** |
||||
* Handle movement by <delta> days of milestone. |
||||
*/ |
||||
public onDaysMoved(wp: WorkPackageResourceInterface, |
||||
dayUnderCursor: Moment, |
||||
delta: number, |
||||
direction: "left" | "right" | "both" | "create" | "dragright") { |
||||
|
||||
const initialDate = wp.$pristine['date']; |
||||
let dates: CellMilestoneMovement = {}; |
||||
|
||||
if (initialDate) { |
||||
dates.date = moment(initialDate).add(delta, "days"); |
||||
} |
||||
|
||||
return dates; |
||||
} |
||||
|
||||
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"; |
||||
return direction; |
||||
} |
||||
|
||||
// create date label
|
||||
const dateInfo = document.createElement("div"); |
||||
dateInfo.className = "rightDateDisplay"; |
||||
this.dateDisplaysOnMouseMove.right = dateInfo; |
||||
elem.appendChild(dateInfo); |
||||
|
||||
this.updateMilestoneMovedLabel(moment(renderInfo.workPackage.date)); |
||||
|
||||
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) { |
||||
return false; |
||||
} |
||||
|
||||
const diamond = jQuery(".diamond", element)[0]; |
||||
|
||||
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(wp); |
||||
|
||||
// offset left
|
||||
const offsetStart = date.diff(viewParams.dateDisplayStart, "days"); |
||||
element.style.left = 'calc(0.5em + ' + calculatePositionValueForDayCount(viewParams, offsetStart) + ')'; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
getLeftmostPosition(renderInfo: RenderInfo): number { |
||||
const wp = renderInfo.workPackage; |
||||
let start = moment(wp.date as any); |
||||
const offsetStart = start.diff(renderInfo.viewParams.dateDisplayStart, "days"); |
||||
return calculatePositionValueForDayCountinPx(renderInfo.viewParams, offsetStart) + (renderInfo.viewParams.pixelPerDay / 4); |
||||
} |
||||
|
||||
getRightmostPosition(renderInfo: RenderInfo): number { |
||||
return this.getLeftmostPosition(renderInfo); |
||||
} |
||||
|
||||
/** |
||||
* Render a milestone element, a single day event with no resize, but |
||||
* move functionality. |
||||
*/ |
||||
public render(renderInfo: RenderInfo): HTMLDivElement { |
||||
const element = document.createElement("div"); |
||||
element.className = timelineElementCssClass + " " + this.type; |
||||
|
||||
const diamond = document.createElement("div"); |
||||
diamond.className = "diamond"; |
||||
element.appendChild(diamond); |
||||
|
||||
return element; |
||||
} |
||||
|
||||
private updateMilestoneMovedLabel(date: Moment) { |
||||
this.dateDisplaysOnMouseMove.right!.innerText = date.format("L"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,83 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {openprojectModule} from "../../../../angular-modules"; |
||||
import {WorkPackageTimelineTableController} from '../wp-timeline-container.directive'; |
||||
import {ZoomLevel} from '../wp-timeline'; |
||||
import IDirective = angular.IDirective; |
||||
import IScope = angular.IScope; |
||||
|
||||
class WorkPackageTimelineControlController { |
||||
|
||||
private wpTimeline: WorkPackageTimelineTableController; |
||||
|
||||
hscroll: number; |
||||
currentZoom: number; |
||||
|
||||
minZoomLevel = ZoomLevel.DAYS; |
||||
maxZoomLevel = ZoomLevel.YEARS; |
||||
|
||||
text:any; |
||||
|
||||
static $inject = ['I18n']; |
||||
|
||||
constructor(private I18n:op.I18n) { |
||||
this.text = { |
||||
zoomIn: I18n.t('js.timelines.zoom.in'), |
||||
zoomOut: I18n.t('js.timelines.zoom.out'), |
||||
} |
||||
} |
||||
|
||||
$onInit() { |
||||
this.hscroll = this.wpTimeline.viewParameterSettings.scrollOffsetInDays; |
||||
this.currentZoom = ZoomLevel.DAYS; |
||||
} |
||||
|
||||
updateScroll() { |
||||
this.wpTimeline.viewParameterSettings.scrollOffsetInDays = this.hscroll; |
||||
this.wpTimeline.refreshScrollOnly(); |
||||
} |
||||
|
||||
updateZoom(delta:number) { |
||||
this.currentZoom += delta; |
||||
|
||||
this.wpTimeline.viewParameterSettings.zoomLevel = this.currentZoom; |
||||
this.wpTimeline.refreshView(); |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
openprojectModule.component("timelineControl", { |
||||
templateUrl: '/components/wp-table/timeline/controls/wp-timeline.dummy-controls.directive.html', |
||||
controller: WorkPackageTimelineControlController, |
||||
require: { |
||||
wpTimeline: '^wpTimelineContainer' |
||||
} |
||||
}); |
||||
|
@ -0,0 +1,14 @@ |
||||
<div class="wp-timeline--dummy-controls hide-when-print" ng-if="$ctrl.wpTimeline.visible"> |
||||
<div class="timeline-controls--zoom-buttons"> |
||||
<button class="button -transparent" |
||||
ng-disabled="$ctrl.currentZoom == $ctrl.maxZoomLevel" |
||||
ng-click="$ctrl.updateZoom(1)"> |
||||
<i class="button--icon icon-zoom-out"></i> |
||||
</button> |
||||
<button class="button -transparent" |
||||
ng-disabled="$ctrl.currentZoom == $ctrl.minZoomLevel" |
||||
ng-click="$ctrl.updateZoom(-1)"> |
||||
<i class="button--icon icon-zoom-in"></i> |
||||
</button> |
||||
</div> |
||||
</div |
@ -0,0 +1,226 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
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 {keyCodes} from "../../common/keyCodes.enum"; |
||||
import IScope = angular.IScope; |
||||
import * as moment from 'moment'; |
||||
import Moment = moment.Moment; |
||||
|
||||
const classNameBar = "bar"; |
||||
export const classNameLeftHandle = "leftHandle"; |
||||
export const classNameRightHandle = "rightHandle"; |
||||
|
||||
|
||||
function getCursorOffsetInDaysFromLeft(renderInfo: RenderInfo, ev: MouseEvent) { |
||||
const header = renderInfo.viewParams.timelineHeader; |
||||
const headerLeft = header.getAbsoluteLeftCoordinates(); |
||||
const cursorOffsetLeftInPx = ev.clientX - headerLeft; |
||||
const cursorOffsetLeftInDays = Math.floor(cursorOffsetLeftInPx / renderInfo.viewParams.pixelPerDay); |
||||
return cursorOffsetLeftInDays; |
||||
} |
||||
|
||||
export function registerWorkPackageMouseHandler(this: void, |
||||
getRenderInfo: () => RenderInfo, |
||||
workPackageTimeline: WorkPackageTimelineTableController, |
||||
wpCacheService: WorkPackageCacheService, |
||||
cell: HTMLElement, |
||||
bar: HTMLDivElement, |
||||
renderer: TimelineCellRenderer, |
||||
renderInfo: RenderInfo) { |
||||
|
||||
let mouseDownStartDay: number|null = null;// also flag to signal active drag'n'drop
|
||||
|
||||
let dateStates:any; |
||||
let placeholderForEmptyCell: HTMLElement; |
||||
const jBody = jQuery("body"); |
||||
|
||||
// handles change to existing work packages
|
||||
bar.onmousedown = (ev: MouseEvent) => { |
||||
workPackageMouseDownFn(ev); |
||||
}; |
||||
|
||||
// handles initial creation of start/due values
|
||||
cell.onmousemove = handleMouseMoveOnEmptyCell; |
||||
|
||||
function applyDateValues(dates:{[name:string]: Moment}) { |
||||
const wp = renderInfo.workPackage; |
||||
|
||||
// Let the renderer decide which fields we change
|
||||
renderer.assignDateValues(wp, dates); |
||||
|
||||
// Update the work package to refresh dates columns
|
||||
wpCacheService.updateWorkPackage(wp); |
||||
} |
||||
|
||||
function workPackageMouseDownFn(ev: MouseEvent) { |
||||
ev.preventDefault(); |
||||
|
||||
workPackageTimeline.disableViewParamsCalculation = true; |
||||
mouseDownStartDay = getCursorOffsetInDaysFromLeft(renderInfo, ev); |
||||
|
||||
// Determine what attributes of the work package should be changed
|
||||
const direction = renderer.onMouseDown(ev, null, renderInfo, bar); |
||||
|
||||
jBody.on("mousemove", createMouseMoveFn(direction)); |
||||
jBody.on("keyup", keyPressFn); |
||||
jBody.on("mouseup", () => deactivate(false)); |
||||
} |
||||
|
||||
function createMouseMoveFn(direction: "left" | "right" | "both" | "create" | "dragright") { |
||||
return (ev: JQueryEventObject) => { |
||||
const mev: MouseEvent = ev as any; |
||||
|
||||
const days = getCursorOffsetInDaysFromLeft(renderInfo, mev) - mouseDownStartDay; |
||||
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); |
||||
} |
||||
} |
||||
|
||||
function keyPressFn(ev: JQueryEventObject) { |
||||
const kev: KeyboardEvent = ev as any; |
||||
if (kev.keyCode === keyCodes.ESCAPE) { |
||||
deactivate(true); |
||||
} |
||||
} |
||||
|
||||
function handleMouseMoveOnEmptyCell(ev: MouseEvent) { |
||||
// const renderInfo = getRenderInfo();
|
||||
const wp = renderInfo.workPackage; |
||||
|
||||
|
||||
if (!renderer.isEmpty(wp)) { |
||||
return; |
||||
} |
||||
|
||||
// placeholder logic
|
||||
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) => { |
||||
placeholderForEmptyCell.remove(); |
||||
bar.style.pointerEvents = "none"; |
||||
ev.preventDefault(); |
||||
|
||||
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); |
||||
|
||||
if (mouseDownType === "create") { |
||||
deactivate(false); |
||||
ev.preventDefault(); |
||||
return; |
||||
} |
||||
|
||||
cell.onmousemove = (ev) => { |
||||
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); |
||||
wpCacheService.updateWorkPackage(wp); |
||||
|
||||
}; |
||||
|
||||
cell.onmouseleave = () => { |
||||
deactivate(true); |
||||
}; |
||||
|
||||
cell.onmouseup = () => { |
||||
deactivate(false); |
||||
}; |
||||
|
||||
jBody.on("keyup", keyPressFn); |
||||
}; |
||||
} |
||||
|
||||
function deactivate(cancelled: boolean) { |
||||
workPackageTimeline.disableViewParamsCalculation = false; |
||||
|
||||
cell.onmousemove = handleMouseMoveOnEmptyCell; |
||||
cell.onmousedown = _.noop; |
||||
cell.onmouseleave = _.noop; |
||||
cell.onmouseup = _.noop; |
||||
|
||||
bar.style.pointerEvents = "auto"; |
||||
jBody.off("mouseup"); |
||||
jBody.off("mousemove"); |
||||
jBody.off("keyup"); |
||||
jQuery(".hascontextmenu").css("cursor", "context-menu"); |
||||
jQuery("." + timelineElementCssClass).css("cursor", ''); |
||||
jQuery("." + classNameLeftHandle).css("cursor", "w-resize"); |
||||
jQuery("." + classNameBar).css("cursor", "ew-resize"); |
||||
jQuery("." + classNameRightHandle).css("cursor", "e-resize"); |
||||
mouseDownStartDay = null; |
||||
dateStates = {}; |
||||
|
||||
renderer.onMouseDownEnd(); |
||||
|
||||
// const renderInfo = getRenderInfo();
|
||||
const wp = renderInfo.workPackage; |
||||
if (cancelled) { |
||||
// cancelled
|
||||
renderer.onCancel(wp); |
||||
wpCacheService.updateWorkPackage(wp); |
||||
workPackageTimeline.refreshView(); |
||||
} else { |
||||
// Persist the changes
|
||||
saveWorkPackage(wp); |
||||
} |
||||
} |
||||
|
||||
function saveWorkPackage(workPackage: WorkPackageResourceInterface) { |
||||
wpCacheService.saveIfChanged(workPackage) |
||||
.catch(() => { |
||||
|
||||
if (!workPackage.isNew) { |
||||
// Reset the changes on error
|
||||
renderer.onCancel(workPackage); |
||||
} |
||||
}) |
||||
.finally(() => { |
||||
workPackageTimeline.refreshView(); |
||||
}); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,171 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import {States} from "../../states.service"; |
||||
import {RenderInfo} from "./wp-timeline"; |
||||
import {WorkPackageTimelineTableController} from "./wp-timeline-container.directive"; |
||||
import {WorkPackageCacheService} from "../../work-packages/work-package-cache.service"; |
||||
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 * as moment from 'moment'; |
||||
import Moment = moment.Moment; |
||||
import {Observable} from 'rxjs'; |
||||
|
||||
const renderers = { |
||||
milestone: new TimelineMilestoneCellRenderer(), |
||||
generic: new TimelineCellRenderer() |
||||
}; |
||||
|
||||
export class WorkPackageTimelineCell { |
||||
|
||||
private subscription: Subscription; |
||||
|
||||
public latestRenderInfo: RenderInfo; |
||||
|
||||
private wpElement: HTMLDivElement|null = null; |
||||
|
||||
private elementShape: string; |
||||
|
||||
constructor(private workPackageTimeline: WorkPackageTimelineTableController, |
||||
private wpCacheService: WorkPackageCacheService, |
||||
private states: States, |
||||
private workPackageId: string, |
||||
public timelineCell: HTMLElement) { |
||||
} |
||||
|
||||
activate() { |
||||
|
||||
Observable.combineLatest( |
||||
this.workPackageTimeline.addWorkPackage(this.workPackageId), |
||||
this.states.table.timelineVisible.observeUntil(this.states.table.stopAllSubscriptions) |
||||
).subscribe((state:[any, boolean]) => { |
||||
const renderInfo = state[0]; |
||||
if (state[1]) { |
||||
this.updateView(renderInfo); |
||||
this.workPackageTimeline.globalService.updateWorkPackageInfo(this); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
deactivate() { |
||||
this.clear(); |
||||
this.workPackageTimeline.globalService.removeWorkPackageInfo(this.workPackageId); |
||||
this.subscription && this.subscription.unsubscribe(); |
||||
} |
||||
|
||||
getLeftmostPosition(): number { |
||||
const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); |
||||
return renderer.getLeftmostPosition(this.latestRenderInfo); |
||||
} |
||||
|
||||
getRightmostPosition(): number { |
||||
const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); |
||||
return renderer.getRightmostPosition(this.latestRenderInfo); |
||||
} |
||||
|
||||
private clear() { |
||||
this.timelineCell.innerHTML = ""; |
||||
this.wpElement = null; |
||||
} |
||||
|
||||
private lazyInit(renderer: TimelineCellRenderer, renderInfo: RenderInfo) { |
||||
const wasRendered = this.wpElement !== null && this.wpElement.parentNode; |
||||
|
||||
// If already rendered with correct shape, ignore
|
||||
if (wasRendered && (this.elementShape === renderer.type)) { |
||||
return; |
||||
} |
||||
|
||||
// Remove the element first if we're redrawing
|
||||
if (wasRendered) { |
||||
this.clear(); |
||||
} |
||||
|
||||
// Render the given element
|
||||
this.wpElement = renderer.render(renderInfo); |
||||
this.elementShape = renderer.type; |
||||
|
||||
// Register the element
|
||||
this.timelineCell.appendChild(this.wpElement); |
||||
registerWorkPackageMouseHandler( |
||||
() => this.latestRenderInfo, |
||||
this.workPackageTimeline, |
||||
this.wpCacheService, |
||||
this.timelineCell, |
||||
this.wpElement, |
||||
renderer, |
||||
renderInfo); |
||||
|
||||
//-------------------------------------------------
|
||||
// TODO Naive horizontal scroll logic, for testing purpose only
|
||||
jQuery(this.timelineCell).on("wheel", ev => { |
||||
const mwe = ev.originalEvent as MouseWheelEvent; |
||||
|
||||
// horizontal scroll
|
||||
// if (Math.abs(mwe.deltaY) < 20) {
|
||||
mwe.preventDefault(); |
||||
const scrollInDays = -Math.round(mwe.deltaX / 15); |
||||
this.workPackageTimeline.wpTimelineHeader.addScrollDelta(scrollInDays); |
||||
// }
|
||||
|
||||
// forward vertical scroll
|
||||
const s = jQuery(".generic-table--results-container"); |
||||
window.requestAnimationFrame(() => { |
||||
s.scrollTop(s.scrollTop() + mwe.deltaY); |
||||
// s.stop().animate({scrollTop: s.scrollTop() + mwe.deltaY}, 200);
|
||||
}); |
||||
}); |
||||
//-------------------------------------------------
|
||||
} |
||||
|
||||
private cellRenderer(workPackage:WorkPackageResourceInterface): TimelineCellRenderer { |
||||
if (workPackage.isMilestone) { |
||||
return renderers.milestone; |
||||
} |
||||
|
||||
return renderers.generic; |
||||
} |
||||
|
||||
private updateView(renderInfo: RenderInfo) { |
||||
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.timelineCell, this.wpElement as HTMLDivElement, renderInfo); |
||||
if (!shouldBeDisplayed) { |
||||
this.clear(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,215 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import {openprojectModule} from "../../../angular-modules"; |
||||
import {TimelineViewParameters, RenderInfo, timelineElementCssClass} from "./wp-timeline"; |
||||
import {WorkPackageResourceInterface} from "./../../api/api-v3/hal-resources/work-package-resource.service"; |
||||
import {HalRequestService} from '../../api/api-v3/hal-request/hal-request.service'; |
||||
import {WpTimelineHeader} from "./wp-timeline.header"; |
||||
import {States} from "./../../states.service"; |
||||
import {BehaviorSubject, Observable} from "rxjs"; |
||||
import * as moment from 'moment'; |
||||
import Moment = moment.Moment; |
||||
import IDirective = angular.IDirective; |
||||
import IScope = angular.IScope; |
||||
import { WpTimelineGlobalService } from "./wp-timeline-global.directive"; |
||||
import { opDimensionEventName } from "../../common/ui/detect-dimension-changes.directive"; |
||||
|
||||
export class WorkPackageTimelineTableController { |
||||
|
||||
private _viewParameters: TimelineViewParameters = new TimelineViewParameters(); |
||||
|
||||
private workPackagesInView: {[id: string]: WorkPackageResourceInterface} = {}; |
||||
|
||||
public wpTimelineHeader: WpTimelineHeader; |
||||
|
||||
public readonly globalService = new WpTimelineGlobalService(this.$scope, this.states, this.halRequest); |
||||
|
||||
private updateAllWorkPackagesSubject = new BehaviorSubject<boolean>(true); |
||||
|
||||
private refreshViewRequested = false; |
||||
|
||||
public visible = false; |
||||
|
||||
public disableViewParamsCalculation = false; |
||||
|
||||
constructor(private $scope: IScope, |
||||
private $element: ng.IAugmentedJQuery, |
||||
private TypeResource:any, |
||||
private states: States, |
||||
private halRequest: HalRequestService) { |
||||
|
||||
"ngInject"; |
||||
|
||||
this.wpTimelineHeader = new WpTimelineHeader(this); |
||||
$element.on(opDimensionEventName, () => { |
||||
this.refreshView(); |
||||
}); |
||||
|
||||
// TODO: Load only necessary types from API
|
||||
TypeResource.loadAll(); |
||||
} |
||||
|
||||
/** |
||||
* Toggle whether this instance is currently showing. |
||||
*/ |
||||
public toggle() { |
||||
this.visible = !this.visible; |
||||
this.states.table.timelineVisible.put(this.visible); |
||||
|
||||
// If hiding view, resize table afterwards
|
||||
if (!this.visible) { |
||||
jQuery(window).trigger('resize'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a defensive copy of the currently used view parameters. |
||||
*/ |
||||
getViewParametersCopy(): TimelineViewParameters { |
||||
return _.cloneDeep(this._viewParameters); |
||||
} |
||||
|
||||
get viewParameterSettings() { |
||||
return this._viewParameters.settings; |
||||
} |
||||
|
||||
refreshView() { |
||||
if (!this.refreshViewRequested) { |
||||
setTimeout(() => { |
||||
this.calculateViewParams(this._viewParameters); |
||||
this.updateAllWorkPackagesSubject.next(true); |
||||
this.wpTimelineHeader.refreshView(this._viewParameters); |
||||
this.refreshScrollOnly(); |
||||
this.refreshViewRequested = false; |
||||
}, 30); |
||||
} |
||||
this.refreshViewRequested = true; |
||||
} |
||||
|
||||
refreshScrollOnly() { |
||||
jQuery("." + timelineElementCssClass).css("margin-left", this._viewParameters.scrollOffsetInPx + "px"); |
||||
} |
||||
|
||||
addWorkPackage(wpId: string): Observable<RenderInfo> { |
||||
|
||||
const wpObs = this.states.workPackages.get(wpId).observeOnScope(this.$scope) |
||||
.map((wp: any) => { |
||||
this.workPackagesInView[wp.id] = wp; |
||||
const viewParamsChanged = this.calculateViewParams(this._viewParameters); |
||||
if (viewParamsChanged) { |
||||
// view params have changed, notify all cells
|
||||
this.globalService.updateViewParameter(this._viewParameters); |
||||
this.refreshView(); |
||||
} |
||||
|
||||
return { |
||||
viewParams: this._viewParameters, |
||||
workPackage: wp |
||||
}; |
||||
}); |
||||
|
||||
return Observable.combineLatest( |
||||
wpObs, |
||||
this.updateAllWorkPackagesSubject, |
||||
(renderInfo: RenderInfo, forceUpdate: boolean) => { |
||||
return renderInfo; |
||||
} |
||||
); |
||||
} |
||||
|
||||
private calculateViewParams(currentParams: TimelineViewParameters): boolean { |
||||
if (this.disableViewParamsCalculation) { |
||||
return false; |
||||
} |
||||
|
||||
const newParams = new TimelineViewParameters(); |
||||
let changed = false; |
||||
|
||||
// Calculate view parameters
|
||||
for (const wpId in this.workPackagesInView) { |
||||
const workPackage = this.workPackagesInView[wpId]; |
||||
|
||||
const startDate = workPackage.startDate ? moment(workPackage.startDate) : currentParams.now; |
||||
const dueDate = workPackage.dueDate ? moment(workPackage.dueDate) : currentParams.now; |
||||
const date = workPackage.date ? moment(workPackage.date) : currentParams.now; |
||||
|
||||
// start date
|
||||
newParams.dateDisplayStart = moment.min( |
||||
newParams.dateDisplayStart, |
||||
currentParams.now, |
||||
startDate, |
||||
date); |
||||
|
||||
// due date
|
||||
newParams.dateDisplayEnd = moment.max( |
||||
newParams.dateDisplayEnd, |
||||
currentParams.now, |
||||
dueDate, |
||||
date); |
||||
} |
||||
|
||||
// left spacing
|
||||
newParams.dateDisplayStart.subtract(3, "days"); |
||||
|
||||
// right spacing
|
||||
const headerWidth = this.wpTimelineHeader.getHeaderWidth(); |
||||
const pixelPerDay = currentParams.pixelPerDay; |
||||
const visibleDays = Math.ceil((headerWidth / pixelPerDay) * 1.5); |
||||
newParams.dateDisplayEnd.add(visibleDays, "days"); |
||||
|
||||
// Check if view params changed:
|
||||
|
||||
// start date
|
||||
if (!newParams.dateDisplayStart.isSame(this._viewParameters.dateDisplayStart)) { |
||||
changed = true; |
||||
this._viewParameters.dateDisplayStart = newParams.dateDisplayStart; |
||||
} |
||||
|
||||
// end date
|
||||
if (!newParams.dateDisplayEnd.isSame(this._viewParameters.dateDisplayEnd)) { |
||||
changed = true; |
||||
this._viewParameters.dateDisplayEnd = newParams.dateDisplayEnd; |
||||
} |
||||
|
||||
|
||||
this._viewParameters.timelineHeader = this.wpTimelineHeader; |
||||
|
||||
return changed; |
||||
} |
||||
} |
||||
|
||||
|
||||
function wpTimelineContainer() { |
||||
return { |
||||
restrict: 'A', |
||||
controller: WorkPackageTimelineTableController, |
||||
bindToController: true |
||||
}; |
||||
} |
||||
|
||||
openprojectModule.directive('wpTimelineContainer', wpTimelineContainer); |
@ -0,0 +1,214 @@ |
||||
|
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import IDirective = angular.IDirective; |
||||
import IComponentOptions = angular.IComponentOptions; |
||||
import {timelineElementCssClass, TimelineViewParameters} from './wp-timeline'; |
||||
import {WorkPackageTimelineCell} from './wp-timeline-cell'; |
||||
import {States} from '../../states.service'; |
||||
import {HalRequestService} from '../../api/api-v3/hal-request/hal-request.service'; |
||||
import {RelationResource} from '../../api/api-v3/hal-resources/relation-resource.service'; |
||||
import {CollectionResource} from '../../api/api-v3/hal-resources/collection-resource.service'; |
||||
import {debugLog} from '../../../helpers/debug_output'; |
||||
import {WorkPackageResource} from '../../api/api-v3/hal-resources/work-package-resource.service'; |
||||
import IScope = angular.IScope; |
||||
|
||||
|
||||
export const timelineGlobalElementCssClassname = 'timeline-global-element'; |
||||
|
||||
function newSegment(vp: TimelineViewParameters, |
||||
classId: string, |
||||
color: string, |
||||
top: number, |
||||
left: number, |
||||
width: number, |
||||
height: number): HTMLElement { |
||||
|
||||
const segment = document.createElement('div'); |
||||
segment.classList.add(timelineElementCssClass, timelineGlobalElementCssClassname, classId); |
||||
segment.style.position = 'absolute'; |
||||
segment.style.cssFloat = 'left'; |
||||
segment.style.backgroundColor = 'blue'; |
||||
// segment.style.backgroundColor = color;
|
||||
segment.style.marginLeft = vp.scrollOffsetInPx + 'px'; |
||||
segment.style.top = top + 'px'; |
||||
segment.style.left = left + 'px'; |
||||
segment.style.width = width + 'px'; |
||||
segment.style.height = height + 'px'; |
||||
return segment; |
||||
} |
||||
|
||||
export class TimelineGlobalElement { |
||||
private static nextId = 0; |
||||
classId = 'timeline-global-element-id-' + TimelineGlobalElement.nextId++; |
||||
from: string; |
||||
to: string; |
||||
} |
||||
|
||||
export class WpTimelineGlobalService { |
||||
|
||||
private workPackageIdOrder: string[] = []; |
||||
|
||||
private viewParameters: TimelineViewParameters; |
||||
|
||||
private cells: {[id: string]: WorkPackageTimelineCell} = {}; |
||||
|
||||
private elements: TimelineGlobalElement[] = []; |
||||
|
||||
constructor(scope: IScope, states: States, halRequest: HalRequestService) { |
||||
states.table.rows.observeOnScope(scope) |
||||
.subscribe(rows => { |
||||
this.workPackageIdOrder = rows.map(wp => wp.id.toString()); |
||||
|
||||
halRequest.get( |
||||
'/api/v3/relations', |
||||
{ |
||||
filter: [{ involved: {operator: '=', values: this.workPackageIdOrder } }] |
||||
}).then((collection: CollectionResource) => { |
||||
this.elements = []; |
||||
this.removeAllElements(); |
||||
collection.elements.forEach((relation: RelationResource) => { |
||||
const fromId = WorkPackageResource.idFromLink(relation.from.href!); |
||||
const toId = WorkPackageResource.idFromLink(relation.to.href!); |
||||
this.displayRelation(fromId, toId); |
||||
}); |
||||
this.renderElements(); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
updateViewParameter(viewParams: TimelineViewParameters) { |
||||
this.viewParameters = viewParams; |
||||
this.update(); |
||||
} |
||||
|
||||
updateWorkPackageInfo(cell: WorkPackageTimelineCell) { |
||||
this.cells[cell.latestRenderInfo.workPackage.id] = cell; |
||||
this.update(); |
||||
} |
||||
|
||||
removeWorkPackageInfo(id: string) { |
||||
delete this.cells[id]; |
||||
this.update(); |
||||
} |
||||
|
||||
displayRelation(from: string, to: string) { |
||||
const elem = new TimelineGlobalElement(); |
||||
elem.from = from; |
||||
elem.to = to; |
||||
this.elements.push(elem); |
||||
this.update(); |
||||
} |
||||
|
||||
private update() { |
||||
this.removeAllElements(); |
||||
this.renderElements(); |
||||
} |
||||
|
||||
private removeAllElements() { |
||||
jQuery('.' + timelineGlobalElementCssClassname).remove(); |
||||
} |
||||
|
||||
private renderElements() { |
||||
if (this.viewParameters === undefined) { |
||||
debugLog('renderElements() aborted - no viewParameters'); |
||||
return; |
||||
} |
||||
|
||||
const vp = this.viewParameters; |
||||
|
||||
for (let e of this.elements) { |
||||
jQuery('.' + e.classId).remove(); |
||||
|
||||
const idxFrom = this.workPackageIdOrder.indexOf(e.from); |
||||
const idxTo = this.workPackageIdOrder.indexOf(e.to); |
||||
|
||||
const startCell = this.cells[e.from]; |
||||
const endCell = this.cells[e.to]; |
||||
|
||||
if (idxFrom === -1 || idxTo === -1 || _.isNil(startCell) || _.isNil(endCell)) { |
||||
continue; |
||||
} |
||||
|
||||
const directionY = idxFrom < idxTo ? 1 : -1; |
||||
let lastX = startCell.getRightmostPosition(); |
||||
let targetX = endCell.getLeftmostPosition(); |
||||
const directionX = targetX > lastX ? 1 : -1; |
||||
|
||||
// start
|
||||
if (!startCell) { |
||||
continue; |
||||
} |
||||
|
||||
startCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 19, lastX, 10, 1)); |
||||
lastX += 10; |
||||
|
||||
if (directionY === 1) { |
||||
startCell.timelineCell.appendChild(newSegment(vp, e.classId, 'red', 19, lastX, 1, 22)); |
||||
} else { |
||||
startCell.timelineCell.appendChild(newSegment(vp, e.classId, 'red', -1, lastX, 1, 22)); |
||||
} |
||||
|
||||
// vert segment
|
||||
for (let index = idxFrom + directionY; index !== idxTo; index += directionY) { |
||||
const id = this.workPackageIdOrder[index]; |
||||
const cell = this.cells[id]; |
||||
if (_.isNil(cell)) { |
||||
continue; |
||||
} |
||||
cell.timelineCell.appendChild(newSegment(vp, e.classId, 'blue', 0, lastX, 1, 42)); |
||||
} |
||||
|
||||
// end
|
||||
if (directionX === 1) { |
||||
if (directionY === 1) { |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 0, lastX, 1, 19)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'blue', 19, lastX, targetX - lastX, 1)); |
||||
} else { |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 19, lastX, 1, 22)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'blue', 19, lastX, targetX - lastX, 1)); |
||||
} |
||||
} else { |
||||
if (directionY === 1) { |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 0, lastX, 1, 8)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'blue', 8, targetX - 10, lastX - targetX + 11, 1)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 8, targetX - 10, 1, 11)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'red', 19, targetX - 10, 10, 1)); |
||||
} else { |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 32, lastX, 1, 8)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'blue', 32, targetX - 10, lastX - targetX + 11, 1)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'green', 19, targetX - 10, 1, 13)); |
||||
endCell.timelineCell.appendChild(newSegment(vp, e.classId, 'red', 19, targetX - 10, 10, 1)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,446 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import { |
||||
TimelineViewParameters, |
||||
timelineElementCssClass, |
||||
ZoomLevel, |
||||
calculatePositionValueForDayCount |
||||
} from "./wp-timeline"; |
||||
import {todayLine} from "./wp-timeline.today-line"; |
||||
import {WorkPackageTimelineTableController} from "./wp-timeline-container.directive"; |
||||
import * as noUiSlider from "nouislider"; |
||||
import * as moment from 'moment'; |
||||
import Moment = moment.Moment; |
||||
|
||||
const cssClassTableBody = ".work-package-table"; |
||||
const cssClassTableContainer = ".generic-table--results-container"; |
||||
const cssClassHeader = ".wp-timeline-header"; |
||||
const cssHeaderContainer = ".wp-timeline-header-container"; |
||||
const scrollBarHeight = 16; |
||||
|
||||
const colorGrey1 = "#AAAAAA"; |
||||
const colorGrey2 = "#DDDDDD"; |
||||
|
||||
export type GlobalElement = (viewParams: TimelineViewParameters, elem: HTMLElement) => any; |
||||
type GlobalElementsRegistry = {[name: string]: GlobalElement}; |
||||
|
||||
export class WpTimelineHeader { |
||||
|
||||
private globalElementsRegistry: GlobalElementsRegistry = {}; |
||||
|
||||
private globalElements: {[type: string]: HTMLElement} = {}; |
||||
|
||||
private headerCell: HTMLElement; |
||||
private outerHeader: JQuery; |
||||
|
||||
/** UI Scrollbar */ |
||||
private scrollBar: HTMLElement; |
||||
private scrollWrapper: JQuery; |
||||
private scrollBarHandle: JQuery; |
||||
private scrollBarOrigin: JQuery; |
||||
|
||||
private marginTop: number; |
||||
|
||||
/** Height of the header elements */ |
||||
private headerHeight = 45; |
||||
|
||||
/** Height of the table body + table header */ |
||||
private globalHeight: number; |
||||
|
||||
/** The total outer height available to the wrapping container */ |
||||
private containerHeight: number; |
||||
|
||||
private activeZoomLevel: ZoomLevel; |
||||
|
||||
constructor(protected wpTimeline: WorkPackageTimelineTableController) { |
||||
this.addElement("todayline", todayLine); |
||||
} |
||||
|
||||
refreshView(vp: TimelineViewParameters) { |
||||
this.lazyInit(); |
||||
this.renderLabels(vp); |
||||
this.renderGlobalElements(vp); |
||||
this.updateScrollbar(vp); |
||||
} |
||||
|
||||
getHeaderWidth() { |
||||
return this.outerHeader ? this.outerHeader.width() : 1; |
||||
} |
||||
|
||||
getAbsoluteLeftCoordinates(): number { |
||||
return jQuery(this.headerCell).offset().left; |
||||
} |
||||
|
||||
addElement(name: string, renderer: GlobalElement) { |
||||
this.globalElementsRegistry[name] = renderer; |
||||
} |
||||
|
||||
removeElement(name: string) { |
||||
this.globalElements[name].remove(); |
||||
delete this.globalElementsRegistry[name]; |
||||
} |
||||
|
||||
setupScrollbar() { |
||||
this.scrollWrapper = jQuery('.wp-timeline--slider-wrapper'); |
||||
this.scrollBar = this.scrollWrapper.find('.wp-timeline--slider')[0]; |
||||
noUiSlider.create(this.scrollBar, { |
||||
start: [0], |
||||
range: { |
||||
min: [0], |
||||
max: [100] |
||||
}, |
||||
orientation: 'horizontal', |
||||
tooltips: false, |
||||
}); |
||||
|
||||
this.sliderInstance.on('update', (values: any[]) => { |
||||
let value = values[0]; |
||||
this.wpTimeline.viewParameterSettings.scrollOffsetInDays = -value; |
||||
this.wpTimeline.refreshScrollOnly(); |
||||
}); |
||||
|
||||
this.scrollBarHandle = this.scrollWrapper.find('.noUi-handle'); |
||||
this.scrollBarOrigin = this.scrollWrapper.find('.noUi-origin'); |
||||
} |
||||
|
||||
public addScrollDelta(delta:number) { |
||||
const value = (this.wpTimeline.viewParameterSettings.scrollOffsetInDays += delta); |
||||
this.sliderInstance.set(-value); |
||||
this.wpTimeline.refreshScrollOnly(); |
||||
} |
||||
|
||||
// noUiSlider doesn't extend the HTMLElement interface
|
||||
// and thus requires casting for now.
|
||||
private get sliderInstance(): noUiSlider.noUiSlider { |
||||
return (this.scrollBar as noUiSlider.Instance).noUiSlider; |
||||
} |
||||
|
||||
private updateScrollbar(vp: TimelineViewParameters) { |
||||
const headerWidth = this.getHeaderWidth(); |
||||
|
||||
// Update the scrollbar to match the current width
|
||||
this.scrollWrapper.css('width', headerWidth + 'px'); |
||||
|
||||
// Re-position the scrollbar depending on the global height
|
||||
// It should not be any larger than the container height
|
||||
if (this.containerHeight > (this.globalHeight + scrollBarHeight)) { |
||||
this.scrollWrapper.css('top', this.globalHeight + 'px'); |
||||
} else { |
||||
this.scrollWrapper.css('top', (this.containerHeight - scrollBarHeight) + 'px'); |
||||
} |
||||
|
||||
let maxWidth = headerWidth, |
||||
daysDisplayed = Math.min(vp.maxSteps, Math.floor(maxWidth / vp.pixelPerDay)), |
||||
newMax = Math.max(vp.maxSteps - daysDisplayed, 1), |
||||
currentValue = <number> this.sliderInstance.get(), |
||||
newValue = Math.min(newMax, currentValue), |
||||
desiredWidth, newWidth, cssWidth; |
||||
|
||||
// Compute the actual width of the handle depending on the scrollable content
|
||||
// The width should be no smaller than 30px
|
||||
desiredWidth = Math.max(vp.maxSteps / vp.pixelPerDay, (40 - vp.pixelPerDay)) * 2; |
||||
|
||||
// The actual width should be no larger than the actual width of the scrollbar
|
||||
// If the entirety of the timeline is already visible, hide the scrollbar
|
||||
if (newMax === 1) { |
||||
newWidth = maxWidth; |
||||
this.scrollWrapper.hide(); |
||||
} else { |
||||
this.scrollWrapper.show(); |
||||
newWidth = Math.min(maxWidth, desiredWidth); |
||||
} |
||||
|
||||
let newCssWidth = newWidth + 'px'; |
||||
|
||||
// With changed widths, we need to correct the
|
||||
// - right padding of the scrollbar (avoid right boundary traversal)
|
||||
// - width of the actual handle
|
||||
// - offset for origin of the slider.
|
||||
jQuery(this.scrollBar).css('padding-right', (newWidth - 1) + 'px'); |
||||
this.scrollBarHandle.css('width', newCssWidth); |
||||
this.scrollBarOrigin.css('right', '-' + newCssWidth); |
||||
|
||||
(this.sliderInstance as any).updateOptions({ |
||||
start: newValue, |
||||
range: { |
||||
min: 0, |
||||
max: newMax |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private lazyInit() { |
||||
if (this.headerCell === undefined) { |
||||
this.headerCell = jQuery(cssClassHeader)[0]; |
||||
this.outerHeader = jQuery(cssHeaderContainer); |
||||
this.setupScrollbar(); |
||||
} |
||||
|
||||
this.containerHeight = jQuery(cssClassTableContainer).outerHeight() + this.headerHeight; |
||||
this.globalHeight = jQuery(cssClassTableBody).outerHeight() + this.headerHeight; |
||||
this.marginTop = this.headerHeight; |
||||
|
||||
this.headerCell.style.height = this.globalHeight + 'px'; |
||||
} |
||||
|
||||
private renderLabels(vp: TimelineViewParameters) { |
||||
if (this.activeZoomLevel === vp.settings.zoomLevel) { |
||||
return; |
||||
} |
||||
|
||||
jQuery(this.headerCell).empty(); |
||||
this.globalElements = {}; |
||||
this.lazyInit(); |
||||
this.renderGlobalElements(vp); |
||||
|
||||
switch (vp.settings.zoomLevel) { |
||||
case ZoomLevel.DAYS: |
||||
return this.renderLabelsDays(vp); |
||||
case ZoomLevel.WEEKS: |
||||
return this.renderLabelsWeeks(vp); |
||||
case ZoomLevel.MONTHS: |
||||
return this.renderLabelsMonths(vp); |
||||
case ZoomLevel.QUARTERS: |
||||
return this.renderLabelsQuarters(vp); |
||||
case ZoomLevel.YEARS: |
||||
return this.renderLabelsYears(vp); |
||||
} |
||||
|
||||
this.activeZoomLevel = vp.settings.zoomLevel; |
||||
} |
||||
|
||||
private renderLabelsDays(vp: TimelineViewParameters) { |
||||
this.renderTimeSlices(vp, "month", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("MMM YYYY"); |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.fontWeight = "bold"; |
||||
cell.style.fontSize = "10px"; |
||||
cell.style.height = "13px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "week", 13, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("ww"); |
||||
cell.style.borderColor = `${colorGrey1}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = (this.globalHeight - 10) + "px"; |
||||
cell.style.zIndex = "2"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "day", 23, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("D"); |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.zIndex = "1"; |
||||
cell.style.height = (this.globalHeight - 20) + "px"; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "day", 33, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("dd"); |
||||
cell.style.height = "15px"; |
||||
cell.style.paddingTop = "1px"; |
||||
}); |
||||
} |
||||
|
||||
private renderLabelsWeeks(vp: TimelineViewParameters) { |
||||
this.renderTimeSlices(vp, "month", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.innerHTML = start.format("MMM YYYY"); |
||||
cell.style.fontWeight = "bold"; |
||||
cell.style.fontSize = "12px"; |
||||
cell.style.height = "15px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "week", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("ww"); |
||||
cell.style.borderColor = `${colorGrey1}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = (this.globalHeight - 10) + "px"; |
||||
cell.style.zIndex = "2"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "day", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("D"); |
||||
cell.style.borderColor = `${colorGrey1}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.paddingTop = "5px"; |
||||
cell.style.height = "20px"; |
||||
}); |
||||
} |
||||
|
||||
private renderLabelsMonths(vp: TimelineViewParameters) { |
||||
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.innerHTML = start.format("YYYY"); |
||||
cell.style.fontWeight = "bold"; |
||||
cell.style.fontSize = "12px"; |
||||
cell.style.height = "15px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "month", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("MMM"); |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = (this.globalHeight - 10) + "px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "week", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("ww"); |
||||
cell.style.borderColor = `${colorGrey1}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = "25px"; |
||||
cell.style.backgroundColor = "white"; |
||||
cell.style.paddingTop = "5px"; |
||||
cell.style.height = "20px"; |
||||
}); |
||||
} |
||||
|
||||
private renderLabelsQuarters(vp: TimelineViewParameters) { |
||||
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.innerHTML = start.format("YYYY"); |
||||
cell.style.fontWeight = "bold"; |
||||
cell.style.fontSize = "12px"; |
||||
cell.style.height = "15px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "quarter", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = "Q" + start.format("Q"); |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = (this.globalHeight - 10) + "px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "month", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("MMM"); |
||||
cell.style.height = "25px"; |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.backgroundColor = "white"; |
||||
cell.style.paddingTop = "5px"; |
||||
cell.style.height = "20px"; |
||||
}); |
||||
} |
||||
|
||||
private renderLabelsYears(vp: TimelineViewParameters) { |
||||
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("YYYY"); |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.borderColor = `${colorGrey1}`; |
||||
cell.style.backgroundColor = "white"; |
||||
cell.style.fontWeight = "bold"; |
||||
cell.style.fontSize = "12px"; |
||||
cell.style.height = "15px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "quarter", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = "Q" + start.format("Q"); |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = (this.globalHeight - 10) + "px"; |
||||
}); |
||||
|
||||
this.renderTimeSlices(vp, "month", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => { |
||||
cell.innerHTML = start.format("M"); |
||||
cell.style.borderColor = `${colorGrey2}`; |
||||
cell.style.borderTop = `1px solid ${colorGrey1}`; |
||||
cell.style.height = "25px"; |
||||
cell.style.backgroundColor = "white"; |
||||
cell.style.paddingTop = "5px"; |
||||
cell.style.height = "20px"; |
||||
}); |
||||
} |
||||
|
||||
renderTimeSlices(vp: TimelineViewParameters, |
||||
unit: moment.unitOfTime.DurationConstructor, |
||||
marginTop: number, |
||||
startView: Moment, |
||||
endView: Moment, |
||||
cellCallback: (start: Moment, cell: HTMLElement) => void) { |
||||
|
||||
const slices: [Moment, Moment][] = []; |
||||
|
||||
const time = startView.clone().startOf(unit); |
||||
const end = endView.clone().endOf(unit); |
||||
|
||||
while (time.isBefore(end)) { |
||||
const sliceStart = moment.max(time, startView).clone(); |
||||
const sliceEnd = moment.min(time.clone().endOf(unit), endView).clone(); |
||||
time.add(1, unit); |
||||
slices.push([sliceStart, sliceEnd]); |
||||
} |
||||
|
||||
for (let [start, end] of slices) { |
||||
const cell = this.addLabelCell(); |
||||
cell.style.borderRight = "1px solid black"; |
||||
cell.style.top = marginTop + "px"; |
||||
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, "days")); |
||||
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, "days") + 1); |
||||
cell.style.textAlign = "center"; |
||||
cell.style.fontSize = "8px"; |
||||
cellCallback(start, cell); |
||||
} |
||||
} |
||||
|
||||
private addLabelCell(): HTMLElement { |
||||
const label = document.createElement("div"); |
||||
label.className = timelineElementCssClass; |
||||
label.style.position = "absolute"; |
||||
label.style.height = "10px"; |
||||
label.style.width = "10px"; |
||||
label.style.top = "0px"; |
||||
label.style.left = "0px"; |
||||
label.style.lineHeight = "normal"; |
||||
this.headerCell.appendChild(label); |
||||
return label; |
||||
} |
||||
|
||||
private renderGlobalElements(vp: TimelineViewParameters) { |
||||
const enabledGlobalElements = _.keys(this.globalElementsRegistry); |
||||
const createdGlobalElements = _.keys(this.globalElements); |
||||
const newGlobalElements = _.difference(enabledGlobalElements, createdGlobalElements); |
||||
|
||||
// new elements
|
||||
for (const newElem of newGlobalElements) { |
||||
const elem = document.createElement("div"); |
||||
elem.className = timelineElementCssClass + " wp-timeline-global-element-" + newElem; |
||||
elem.style.position = "absolute"; |
||||
elem.style.top = this.marginTop + "px"; |
||||
elem.style.zIndex = "100"; |
||||
this.headerCell.appendChild(elem); |
||||
this.globalElements[newElem] = elem; |
||||
} |
||||
|
||||
// update elements
|
||||
for (const elemType of _.keys(this.globalElements)) { |
||||
const elem = this.globalElements[elemType]; |
||||
elem.style.height = this.globalHeight + "px"; |
||||
this.globalElementsRegistry[elemType](vp, elem); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,42 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {calculatePositionValueForDayCount, TimelineViewParameters} from "./wp-timeline"; |
||||
import * as moment from 'moment'; |
||||
|
||||
|
||||
// Today Line
|
||||
export function todayLine(viewParams: TimelineViewParameters, elem: HTMLElement) { |
||||
elem.style.width = "2px"; |
||||
elem.style.borderLeft = "2px dotted red"; |
||||
const offsetToday = viewParams.now.diff(viewParams.dateDisplayStart, "days"); |
||||
const dayProgress = moment().hour() / 24; |
||||
elem.style.left = calculatePositionValueForDayCount(viewParams, offsetToday + dayProgress); |
||||
elem.style.marginLeft = viewParams.scrollOffsetInPx + "px"; |
||||
} |
||||
|
@ -0,0 +1,125 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import * as moment from "moment"; |
||||
import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-package-resource.service"; |
||||
import {WpTimelineHeader} from "./wp-timeline.header"; |
||||
import Moment = moment.Moment; |
||||
|
||||
export const timelineElementCssClass = "timeline-element"; |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export class TimelineViewParametersSettings { |
||||
|
||||
// showDurationInPx = true;
|
||||
|
||||
scrollOffsetInDays = 0; |
||||
|
||||
zoomLevel: ZoomLevel = ZoomLevel.DAYS; |
||||
|
||||
} |
||||
|
||||
export enum ZoomLevel { |
||||
DAYS, WEEKS, MONTHS, QUARTERS, YEARS |
||||
} |
||||
|
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export class TimelineViewParameters { |
||||
|
||||
readonly now: Moment = moment({hour: 0, minute: 0, seconds: 0}); |
||||
|
||||
dateDisplayStart: Moment = moment({hour: 0, minute: 0, seconds: 0}); |
||||
|
||||
dateDisplayEnd: Moment = this.dateDisplayStart.clone().add(1, "day"); |
||||
|
||||
settings: TimelineViewParametersSettings = new TimelineViewParametersSettings(); |
||||
|
||||
timelineHeader: WpTimelineHeader; |
||||
|
||||
get pixelPerDay() { |
||||
switch (this.settings.zoomLevel) { |
||||
case ZoomLevel.DAYS: |
||||
return 30; |
||||
case ZoomLevel.WEEKS: |
||||
return 15; |
||||
case ZoomLevel.MONTHS: |
||||
return 6; |
||||
case ZoomLevel.QUARTERS: |
||||
return 2; |
||||
case ZoomLevel.YEARS: |
||||
return 0.5; |
||||
} |
||||
} |
||||
|
||||
get maxWidthInPx() { |
||||
return this.maxSteps * this.pixelPerDay; |
||||
} |
||||
|
||||
get maxSteps():number { |
||||
return this.dateDisplayEnd.diff(this.dateDisplayStart, "days"); |
||||
} |
||||
|
||||
get scrollOffsetInPx() { |
||||
return this.settings.scrollOffsetInDays * this.pixelPerDay; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export interface RenderInfo { |
||||
viewParams: TimelineViewParameters; |
||||
workPackage: WorkPackageResourceInterface; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export function calculatePositionValueForDayCountinPx(viewParams: TimelineViewParameters, days: number): number { |
||||
const daysInPx = days * viewParams.pixelPerDay; |
||||
return daysInPx; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export function calculatePositionValueForDayCount(viewParams: TimelineViewParameters, days: number): string { |
||||
const value = calculatePositionValueForDayCountinPx(viewParams, days); |
||||
// if (viewParams.settings.showDurationInPx) {
|
||||
return value + "px"; |
||||
// } else {
|
||||
// return (value / viewParams.maxWidthInPx * 100) + "%";
|
||||
// }
|
||||
} |
||||
|
||||
|
@ -1 +1,12 @@ |
||||
/// <reference path="../tests/typings/tests.d.ts" />
|
||||
|
||||
|
||||
/* |
||||
monkey patch noUiSlider |
||||
*/ |
||||
declare namespace noUiSlider { |
||||
//noinspection JSUnusedGlobalSymbols
|
||||
interface Options { |
||||
tooltips: boolean; |
||||
} |
||||
} |
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue