kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
811 lines
28 KiB
811 lines
28 KiB
//-- copyright
|
|
// OpenProject is a project management system.
|
|
// Copyright (C) 2012-2013 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.
|
|
//++
|
|
|
|
// ╭───────────────────────────────────────────────────────────────╮
|
|
// │ _____ _ _ _ │
|
|
// │ |_ _(_)_ __ ___ ___| (_)_ __ ___ ___ │
|
|
// │ | | | | '_ ` _ \ / _ \ | | '_ \ / _ \/ __| │
|
|
// │ | | | | | | | | | __/ | | | | | __/\__ \ │
|
|
// │ |_| |_|_| |_| |_|\___|_|_|_| |_|\___||___/ │
|
|
// ├───────────────────────────────────────────────────────────────┤
|
|
// │ Javascript library that fetches and plots timelines for the │
|
|
// │ OpenProject timelines module. │
|
|
// ╰───────────────────────────────────────────────────────────────╯
|
|
|
|
// stricter than default
|
|
/*jshint undef:true,
|
|
eqeqeq:true,
|
|
forin:true,
|
|
immed:true,
|
|
latedef:true,
|
|
trailing: true
|
|
*/
|
|
|
|
// looser than default
|
|
/*jshint eqnull:true */
|
|
|
|
// environment and other global vars
|
|
/*jshint browser:true, devel:true*/
|
|
/*global jQuery:false, Raphael:false, Timeline:true*/
|
|
|
|
if (typeof Timeline === "undefined") {
|
|
Timeline = {};
|
|
}
|
|
|
|
// ╭───────────────────────────────────────────────────────────────────╮
|
|
// │ Timeline.PlanningElement │
|
|
// ╰───────────────────────────────────────────────────────────────────╯
|
|
|
|
Timeline.PlanningElement = {
|
|
is: function(t) {
|
|
return Timeline.PlanningElement.identifier === t.identifier;
|
|
},
|
|
identifier: 'planning_elements',
|
|
hide: function () {
|
|
return false;
|
|
},
|
|
filteredOut: function() {
|
|
var filtered = this.filteredOutForProjectFilter();
|
|
this.filteredOut = function() { return filtered; };
|
|
return filtered;
|
|
},
|
|
inTimeFrame: function () {
|
|
return this.timeline.inTimeFilter(this.start(), this.end());
|
|
},
|
|
filteredOutForProjectFilter: function() {
|
|
return this.project.filteredOut();
|
|
},
|
|
all: function(timeline) {
|
|
// collect all planning elements
|
|
var r = timeline.planning_elements;
|
|
var result = [];
|
|
for (var key in r) {
|
|
if (r.hasOwnProperty(key)) {
|
|
result.push(r[key]);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
getProject: function() {
|
|
return (this.project !== undefined) ? this.project : null;
|
|
},
|
|
getPlanningElementType: function() {
|
|
return (this.planning_element_type !== undefined) ?
|
|
this.planning_element_type : null;
|
|
},
|
|
getResponsible: function() {
|
|
return (this.responsible !== undefined) ? this.responsible : null;
|
|
},
|
|
getResponsibleName: function() {
|
|
if (this.responsible && this.responsible.name) {
|
|
return this.responsible.name;
|
|
}
|
|
},
|
|
getAssignedName: function () {
|
|
if (this.assigned_to && this.assigned_to.name) {
|
|
return this.assigned_to.name;
|
|
}
|
|
},
|
|
getParent: function() {
|
|
return (this.parent !== undefined) ? this.parent : null;
|
|
},
|
|
getChildren: function() {
|
|
if (!this.planning_elements) {
|
|
return [];
|
|
}
|
|
if (!this.sorted) {
|
|
this.sort('planning_elements');
|
|
this.sorted = true;
|
|
}
|
|
return this.planning_elements;
|
|
},
|
|
hasChildren: function() {
|
|
return this.getChildren().length > 0;
|
|
},
|
|
getTypeName: function () {
|
|
var pet = this.getPlanningElementType();
|
|
if (pet) {
|
|
return pet.name;
|
|
}
|
|
},
|
|
getStatusName: function () {
|
|
if (this.status) {
|
|
return this.status.name;
|
|
}
|
|
},
|
|
getProjectName: function () {
|
|
if (this.project) {
|
|
return this.project.name;
|
|
}
|
|
},
|
|
sort: function(field) {
|
|
this[field] = this[field].sort(function(a, b) {
|
|
// order by date, name
|
|
var dc = 0;
|
|
var as = a.start(), bs = b.start();
|
|
if (as) {
|
|
if (bs) {
|
|
dc = as.compareTo(bs);
|
|
} else {
|
|
dc = 1;
|
|
}
|
|
} else if (bs) {
|
|
dc = -1;
|
|
}
|
|
if (dc !== 0) {
|
|
return dc;
|
|
}
|
|
if (a.name < b.name) {
|
|
return -1;
|
|
}
|
|
if (a.name > b.name) {
|
|
return +1;
|
|
}
|
|
return 0;
|
|
});
|
|
return this;
|
|
},
|
|
start: function() {
|
|
var pet = this.getPlanningElementType();
|
|
//if we have got a milestone w/o a start date but with an end date, just set them the same.
|
|
if (this.start_date === undefined && this.due_date !== undefined && pet && pet.is_milestone) {
|
|
this.start_date = this.due_date;
|
|
}
|
|
if (this.start_date_object === undefined && this.start_date !== undefined) {
|
|
this.start_date_object = Date.parse(this.start_date);
|
|
}
|
|
return this.start_date_object;
|
|
},
|
|
end: function() {
|
|
var pet = this.getPlanningElementType();
|
|
//if we have got a milestone w/o a start date but with an end date, just set them the same.
|
|
if (this.due_date === undefined && this.start_date !== undefined && pet && pet.is_milestone) {
|
|
this.due_date = this.start_date;
|
|
}
|
|
if (this.due_date_object=== undefined && this.due_date !== undefined) {
|
|
this.due_date_object = Date.parse(this.due_date);
|
|
}
|
|
return this.due_date_object;
|
|
},
|
|
getAttribute: function (val) {
|
|
if (typeof this[val] === "function") {
|
|
return this[val]();
|
|
}
|
|
|
|
return this[val];
|
|
},
|
|
does_historical_differ: function (val) {
|
|
if (!this.has_historical()) {
|
|
return false;
|
|
}
|
|
|
|
return this.historical().getAttribute(val) !== this.getAttribute(val);
|
|
},
|
|
has_historical: function () {
|
|
return this.historical_element !== undefined;
|
|
},
|
|
historical: function () {
|
|
return this.historical_element || Object.create(Timeline.PlanningElement);
|
|
},
|
|
alternate_start: function() {
|
|
return this.historical().start();
|
|
},
|
|
alternate_end: function() {
|
|
return this.historical().end();
|
|
},
|
|
getSubElements: function() {
|
|
return this.getChildren();
|
|
},
|
|
hasAlternateDates: function() {
|
|
return (this.does_historical_differ("start_date") ||
|
|
this.does_historical_differ("end_date") ||
|
|
this.is_deleted);
|
|
},
|
|
isDeleted: function() {
|
|
return true && this.is_deleted;
|
|
},
|
|
isNewlyAdded: function() {
|
|
return (this.timeline.isComparing() &&
|
|
!this.has_historical());
|
|
},
|
|
getAlternateHorizontalBounds: function(scale, absolute_beginning, milestone) {
|
|
return this.getHorizontalBoundsForDates(
|
|
scale,
|
|
absolute_beginning,
|
|
this.alternate_start(),
|
|
this.alternate_end(),
|
|
milestone
|
|
);
|
|
},
|
|
getHorizontalBounds: function(scale, absolute_beginning, milestone) {
|
|
return this.getHorizontalBoundsForDates(
|
|
scale,
|
|
absolute_beginning,
|
|
this.start(),
|
|
this.end(),
|
|
milestone
|
|
);
|
|
},
|
|
hasStartDate: function () {
|
|
if (this.start()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
hasEndDate: function () {
|
|
if (this.end()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
hasBothDates: function () {
|
|
if (this.start() && this.end()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
hasOneDate: function () {
|
|
if (this.start() || this.end()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
getHorizontalBoundsForDates: function(scale, absolute_beginning, start, end, milestone) {
|
|
var timeline = this.timeline;
|
|
|
|
if (!start && !end) {
|
|
return {
|
|
'x': 0,
|
|
'w': 0,
|
|
'end': function () {
|
|
return this.x + this.w;
|
|
}
|
|
};
|
|
} else if (!end) {
|
|
end = start.clone().addDays(2);
|
|
} else if (!start) {
|
|
start = end.clone().addDays(-2);
|
|
}
|
|
|
|
// calculate graphical representation. the +1 makes sense when
|
|
// considering equal start and end date.
|
|
var x = timeline.getDaysBetween(absolute_beginning, start) * scale.day;
|
|
var w = (timeline.getDaysBetween(start, end) + 1) * scale.day;
|
|
|
|
if (milestone === true) {
|
|
//we are in the middle of the diamond so we have to move half the size to the left
|
|
x -= ((scale.height - 1) / 2);
|
|
//and add the diamonds corners to the width.
|
|
w += scale.height - 1;
|
|
}
|
|
|
|
return {
|
|
'x': x,
|
|
'w': w,
|
|
'end': function () {
|
|
return this.x + this.w;
|
|
}
|
|
};
|
|
},
|
|
getUrl: function() {
|
|
var options = this.timeline.options;
|
|
var url = options.url_prefix;
|
|
|
|
url += "/work_packages/";
|
|
url += this.id;
|
|
|
|
return url;
|
|
},
|
|
getColor: function () {
|
|
// if there is a color for this planning element type, use it.
|
|
// use it also for planning elements w/ children. if there are
|
|
// children but no planning element type, use the default color
|
|
// for planning element parents. if there is no planning element
|
|
// type and there are no children, use a default color.
|
|
var pet = this.getPlanningElementType();
|
|
var color;
|
|
|
|
if (pet && pet.color) {
|
|
color = pet.color.hexcode;
|
|
} else if (this.hasChildren()) {
|
|
color = Timeline.DEFAULT_PARENT_COLOR;
|
|
} else {
|
|
color = Timeline.DEFAULT_COLOR;
|
|
}
|
|
|
|
if (!this.hasBothDates()) {
|
|
if (this.hasStartDate()) {
|
|
color = "180-#ffffff-" + color;
|
|
} else {
|
|
color = "180-" + color + "-#ffffff";
|
|
}
|
|
}
|
|
|
|
return color;
|
|
},
|
|
render: function(node, in_aggregation, label_space) {
|
|
var timeline = this.timeline;
|
|
var paper = timeline.getPaper();
|
|
var scale = timeline.getScale();
|
|
var beginning = timeline.getBeginning();
|
|
var elements = [];
|
|
var pet = this.getPlanningElementType();
|
|
var self = this;
|
|
var color, text, x, y, textColor;
|
|
var bounds = this.getHorizontalBounds(scale, beginning);
|
|
var left = bounds.x;
|
|
var width = bounds.w;
|
|
var alternate_bounds = this.getAlternateHorizontalBounds(scale, beginning);
|
|
var alternate_left = alternate_bounds.x;
|
|
var alternate_width = alternate_bounds.w;
|
|
var hover_left = left;
|
|
var hover_width = width;
|
|
var element = node.getDOMElement();
|
|
var captionElements = [];
|
|
var label;
|
|
var deleted = true && this.is_deleted;
|
|
var comparison_offset = deleted ? 0 : Timeline.DEFAULT_COMPARISON_OFFSET;
|
|
var strokeColor = Timeline.DEFAULT_STROKE_COLOR;
|
|
var historical = this.historical();
|
|
|
|
var has_both_dates = this.hasBothDates();
|
|
var has_one_date = this.hasOneDate();
|
|
var has_start_date = this.hasStartDate();
|
|
|
|
if (in_aggregation && label_space !== undefined) {
|
|
hover_left = label_space.x + Timeline.HOVER_THRESHOLD;
|
|
hover_width = label_space.w - 2 * Timeline.HOVER_THRESHOLD;
|
|
}
|
|
|
|
if (in_aggregation && !has_both_dates) {
|
|
return;
|
|
}
|
|
|
|
var has_alternative = this.hasAlternateDates();
|
|
var could_have_been_milestone = (this.alternate_start === this.alternate_end);
|
|
|
|
var height, top;
|
|
|
|
if (historical.hasOneDate()) {
|
|
// ╭─────────────────────────────────────────────────────────╮
|
|
// │ Rendering of historical data. Use default planning │
|
|
// │ element appearance, only use milestones when the │
|
|
// │ element is currently a milestone and the historical │
|
|
// │ data has equal start and end dates. │
|
|
// ╰─────────────────────────────────────────────────────────╯
|
|
color = this.historical().getColor();
|
|
|
|
if (!historical.hasBothDates()) {
|
|
strokeColor = 'none';
|
|
}
|
|
|
|
//TODO: fix for work units w/o start/end date
|
|
if (!in_aggregation && has_alternative) {
|
|
if (pet && pet.is_milestone && could_have_been_milestone) {
|
|
|
|
height = scale.height - 1; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
paper.path(
|
|
timeline.psub('M#{x} #{y}h#{w}l#{d} #{d}l-#{d} #{d}H#{x}l-#{d} -#{d}l#{d} -#{d}Z', {
|
|
x: alternate_left + scale.day / 2,
|
|
y: top - comparison_offset,
|
|
w: alternate_width - scale.day,
|
|
d: height / 2 // diamond corner width.
|
|
})
|
|
).attr({
|
|
'fill': color, // Timeline.DEFAULT_FILL_COLOR_IN_COMPARISONS,
|
|
'opacity': 0.33,
|
|
'stroke': Timeline.DEFAULT_STROKE_COLOR_IN_COMPARISONS,
|
|
'stroke-dasharray': Timeline.DEFAULT_STROKE_DASHARRAY_IN_COMPARISONS
|
|
});
|
|
|
|
} else {
|
|
|
|
height = scale.height - 6; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
paper.rect(
|
|
alternate_left,
|
|
top - comparison_offset, // 8px margin-top
|
|
alternate_width,
|
|
height, // 8px margin-bottom
|
|
4 // round corners
|
|
).attr({
|
|
'fill': color, // Timeline.DEFAULT_FILL_COLOR_IN_COMPARISONS,
|
|
'opacity': 0.33,
|
|
'stroke': Timeline.DEFAULT_STROKE_COLOR_IN_COMPARISONS,
|
|
'stroke-dasharray': Timeline.DEFAULT_STROKE_DASHARRAY_IN_COMPARISONS
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// only render planning elements that have
|
|
// either a start or an end date.
|
|
if (has_one_date) {
|
|
color = this.getColor();
|
|
|
|
if (!has_both_dates) {
|
|
strokeColor = 'none';
|
|
}
|
|
|
|
// ╭─────────────────────────────────────────────────────────╮
|
|
// │ Rendering of actual elements, as milestones, with teeth │
|
|
// │ and the generic, dafault planning element w/ round │
|
|
// │ edges. │
|
|
// ╰─────────────────────────────────────────────────────────╯
|
|
|
|
// in_aggregation defines whether the planning element should be
|
|
// renderd as a generic planning element regardless of children.
|
|
|
|
if (!deleted && pet && pet.is_milestone) {
|
|
|
|
} else if (!deleted && !in_aggregation && this.hasChildren() && node.isExpanded()) {
|
|
|
|
// with teeth (has children).
|
|
|
|
paper.path(
|
|
timeline.psub('M#{x} #{y}m#{d} #{d}l-#{d} #{d}l-#{d} -#{d}V#{y}H#{x}h#{w}h#{d}v#{d}l-#{d} #{d}l-#{d} -#{d}z' + /* outer path */
|
|
'm0 0v-#{d}m#{w} 0m-#{d} 0m-#{d} 0v#{d}' /* inner vertical lines */, {
|
|
x: left,
|
|
y: timeline.getRelativeVerticalOffset(element) + 8,
|
|
d: scale.height + 2 - 16,
|
|
w: width
|
|
})
|
|
).attr({
|
|
'fill': color,
|
|
'stroke': strokeColor
|
|
});
|
|
} else if (!deleted) {
|
|
|
|
// generic.
|
|
|
|
height = scale.height - 6; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
paper.rect(
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
4 // round corners
|
|
).attr({
|
|
'fill': color,
|
|
'stroke': strokeColor
|
|
});
|
|
}
|
|
}
|
|
},
|
|
renderForeground: function (node, in_aggregation, label_space) {
|
|
var timeline = this.timeline;
|
|
var paper = timeline.getPaper();
|
|
var scale = timeline.getScale();
|
|
var beginning = timeline.getBeginning();
|
|
var elements = [];
|
|
var pet = this.getPlanningElementType();
|
|
var self = this;
|
|
var color, text, x, y, textColor;
|
|
var bounds = this.getHorizontalBounds(scale, beginning);
|
|
var left = bounds.x;
|
|
var width = bounds.w;
|
|
var alternate_bounds = this.getAlternateHorizontalBounds(scale, beginning);
|
|
var alternate_left = alternate_bounds.x;
|
|
var alternate_width = alternate_bounds.w;
|
|
var hover_left = left;
|
|
var hover_width = width;
|
|
var element = node.getDOMElement();
|
|
var captionElements = [];
|
|
var label, textWidth;
|
|
var deleted = true && this.is_deleted;
|
|
var comparison_offset = deleted ? 0 : Timeline.DEFAULT_COMPARISON_OFFSET;
|
|
|
|
var has_both_dates = this.hasBothDates();
|
|
var has_one_date = this.hasOneDate();
|
|
var has_start_date = this.hasStartDate();
|
|
|
|
if (in_aggregation && label_space !== undefined) {
|
|
hover_left = label_space.x + Timeline.HOVER_THRESHOLD;
|
|
hover_width = label_space.w - 2 * Timeline.HOVER_THRESHOLD;
|
|
}
|
|
|
|
var has_alternative = this.hasAlternateDates();
|
|
var could_have_been_milestone = (this.alternate_start === this.alternate_end);
|
|
|
|
var height, top;
|
|
|
|
// if there is a color for this planning element type, use it.
|
|
// use it also for planning elements w/ children. if there are
|
|
// children but no planning element type, use the default color
|
|
// for planning element parents. if there is no planning element
|
|
// type and there are no children, use a default color.
|
|
|
|
if (pet && pet.color) {
|
|
color = pet.color.hexcode;
|
|
} else if (this.hasChildren()) {
|
|
color = Timeline.DEFAULT_PARENT_COLOR;
|
|
} else {
|
|
color = Timeline.DEFAULT_COLOR;
|
|
}
|
|
|
|
if (!deleted && pet && pet.is_milestone) {
|
|
|
|
// milestones.
|
|
height = scale.height - 1; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
paper.path(
|
|
timeline.psub('M#{x} #{y}h#{w}l#{d} #{d}l-#{d} #{d}H#{x}l-#{d} -#{d}l#{d} -#{d}Z', {
|
|
x: left + scale.day / 2,
|
|
y: top,
|
|
w: width - scale.day,
|
|
d: height / 2 // diamond corner width.
|
|
})
|
|
).attr({
|
|
'fill': color,
|
|
'stroke': Timeline.DEFAULT_STROKE_COLOR
|
|
});
|
|
|
|
}
|
|
|
|
// ╭─────────────────────────────────────────────────────────╮
|
|
// │ Labels for rendered elements, either in aggregartion │
|
|
// │ or out of aggregation, inside of elements or outside. │
|
|
// ╰─────────────────────────────────────────────────────────╯
|
|
|
|
height = scale.height - 6; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
y = top + 11;
|
|
|
|
if (has_one_date) {
|
|
if (!in_aggregation) {
|
|
|
|
// text rendering in planning elements outside of aggregations
|
|
label = timeline.paper.text(0, -5, this.subject);
|
|
label.attr({
|
|
'font-size': 12
|
|
});
|
|
|
|
textWidth = label.getBBox().width;
|
|
|
|
// if this is an expanded planning element w/ children, or if
|
|
// the text would not fit:
|
|
if (this.hasChildren() && node.isExpanded() ||
|
|
textWidth > width - Timeline.PE_TEXT_INSIDE_PADDING) {
|
|
|
|
// place a white rect below the label.
|
|
captionElements.push(
|
|
timeline.paper.rect(
|
|
-3,
|
|
-12,
|
|
textWidth + 6,
|
|
15,
|
|
4.5
|
|
).attr({
|
|
'fill': '#ffffff',
|
|
'opacity': 0.5,
|
|
'stroke': 'none'
|
|
}));
|
|
|
|
// text outside planning element
|
|
x = left + width + Timeline.PE_TEXT_OUTSIDE_PADDING;
|
|
textColor = Timeline.PE_DEFAULT_TEXT_COLOR;
|
|
|
|
if (this.hasChildren()) {
|
|
x += Timeline.PE_TEXT_ADDITIONAL_OUTSIDE_PADDING_WHEN_EXPANDED_WITH_CHILDREN;
|
|
}
|
|
|
|
if (pet && pet.is_milestone) {
|
|
x += Timeline.PE_TEXT_ADDITIONAL_OUTSIDE_PADDING_WHEN_MILESTONE;
|
|
}
|
|
|
|
} else if (!has_both_dates) {
|
|
// text inside planning element
|
|
if (has_start_date) {
|
|
x = left + 4; // left of the WU
|
|
} else {
|
|
x = left + width - // right of the WU
|
|
textWidth - // text width
|
|
4; // small border from the right
|
|
}
|
|
|
|
textColor = timeline.getLimunanceFor(color) > Timeline.PE_LUMINANCE_THRESHOLD ?
|
|
Timeline.PE_DARK_TEXT_COLOR : Timeline.PE_LIGHT_TEXT_COLOR;
|
|
} else {
|
|
|
|
// text inside planning element
|
|
x = left + width * 0.5 + // center of the planning element
|
|
textWidth * (-0.5); // half of text width
|
|
|
|
textColor = timeline.getLimunanceFor(color) > Timeline.PE_LUMINANCE_THRESHOLD ?
|
|
Timeline.PE_DARK_TEXT_COLOR : Timeline.PE_LIGHT_TEXT_COLOR;
|
|
}
|
|
|
|
label.attr({
|
|
'fill': textColor,
|
|
'text-anchor': "start",
|
|
'stroke': 'none'
|
|
});
|
|
|
|
if (captionElements[0]) {
|
|
label.insertAfter(captionElements[0]);
|
|
}
|
|
|
|
captionElements.push(label);
|
|
|
|
jQuery.each(captionElements, function(i, e) {
|
|
e.translate(x, y);
|
|
});
|
|
|
|
} else if (label_space.w > Timeline.PE_TEXT_AGGREGATED_LABEL_WIDTH_THRESHOLD) {
|
|
|
|
textColor = timeline.getLimunanceFor(color) > Timeline.PE_LUMINANCE_THRESHOLD ?
|
|
Timeline.PE_DARK_TEXT_COLOR : Timeline.PE_LIGHT_TEXT_COLOR;
|
|
|
|
var text = this.subject;
|
|
label = timeline.paper.text(0, 0, text);
|
|
label.attr({
|
|
'font-size': 12,
|
|
'fill': textColor,
|
|
'stroke': 'none'
|
|
});
|
|
|
|
x = label_space.x + label_space.w/2;
|
|
y -= 4;
|
|
|
|
while (text.length > 0 && label.getBBox().width > label_space.w) {
|
|
text = text.slice(0, -1);
|
|
label.attr({ 'text': text });
|
|
}
|
|
|
|
label.translate(x, y);
|
|
}
|
|
}
|
|
|
|
// ╭─────────────────────────────────────────────────────────╮
|
|
// │ Defining hover areas that will produce tooltips when │
|
|
// │ mouse is over them. This is last to include text drawn │
|
|
// │ over planning elements. │
|
|
// ╰─────────────────────────────────────────────────────────╯
|
|
|
|
height = scale.height - 6; //6px makes the element a little smaller.
|
|
top = (timeline.getRelativeVerticalOffset(element) + timeline.getRelativeVerticalBottomOffset(element)) / 2 - height / 2;
|
|
|
|
elements.push(paper.rect(
|
|
hover_left - Timeline.HOVER_THRESHOLD,
|
|
top - Timeline.HOVER_THRESHOLD, // 8px margin-top
|
|
hover_width + 2 * Timeline.HOVER_THRESHOLD,
|
|
height + 2 * Timeline.HOVER_THRESHOLD, // 8px margin-bottom
|
|
4 // round corners
|
|
).attr({
|
|
'fill': '#ffffff',
|
|
'opacity': 0
|
|
}));
|
|
|
|
jQuery.each(elements, function(i, e) {
|
|
timeline.addHoverHandler(node, e);
|
|
//self.addElement(e);
|
|
});
|
|
},
|
|
renderVertical: function(node) {
|
|
var timeline = this.timeline;
|
|
var paper = timeline.getPaper();
|
|
var scale = timeline.getScale();
|
|
var beginning = timeline.getBeginning();
|
|
|
|
var pet = this.getPlanningElementType();
|
|
var self = this;
|
|
var color;
|
|
var bounds = this.getHorizontalBounds(scale, beginning);
|
|
|
|
var deleted = true && this.is_deleted;
|
|
|
|
var left = bounds.x;
|
|
var width = bounds.w;
|
|
|
|
var element = node.getDOMElement();
|
|
|
|
var has_both_dates = this.hasBothDates();
|
|
var has_one_date = this.hasOneDate();
|
|
var has_start_date = this.hasStartDate();
|
|
|
|
var hoverElement;
|
|
|
|
color = this.getColor();
|
|
|
|
if (has_one_date) {
|
|
if (!deleted && pet && pet.is_milestone) {
|
|
timeline.paper.path(
|
|
timeline.psub("M#{left} #{top}L#{left} #{height}", {
|
|
'left': left + scale.day / 2,
|
|
'top': timeline.decoHeight(),
|
|
'height': timeline.getMeasuredHeight()
|
|
})
|
|
).attr({
|
|
'stroke': color,
|
|
'stroke-width': 2,
|
|
'stroke-dasharray': '- '
|
|
});
|
|
|
|
hoverElement = paper.rect(
|
|
left + scale.day / 2 - 2 * Timeline.HOVER_THRESHOLD,
|
|
timeline.decoHeight(), // 8px margin-top
|
|
4 * Timeline.HOVER_THRESHOLD,
|
|
timeline.getMeasuredHeight() // 8px margin-bottom
|
|
).attr({
|
|
'fill': '#ffffff',
|
|
'opacity': 0
|
|
});
|
|
|
|
timeline.addHoverHandler(node, hoverElement);
|
|
} else if (!deleted) {
|
|
paper.rect(
|
|
left,
|
|
timeline.decoHeight(),
|
|
width,
|
|
timeline.getMeasuredHeight()
|
|
).attr({
|
|
'fill': color,
|
|
'stroke': Timeline.DEFAULT_STROKE_COLOR,
|
|
'opacity': 0.2
|
|
});
|
|
|
|
hoverElement = paper.rect(
|
|
left - Timeline.HOVER_THRESHOLD,
|
|
timeline.decoHeight(), // 8px margin-top
|
|
width + 2 * Timeline.HOVER_THRESHOLD,
|
|
timeline.getMeasuredHeight() // 8px margin-bottom
|
|
).attr({
|
|
'fill': '#ffffff',
|
|
'opacity': 0
|
|
});
|
|
|
|
timeline.addHoverHandler(node, hoverElement);
|
|
//self.addElement(hoverElement);
|
|
}
|
|
}
|
|
},
|
|
addElement: function(e) {
|
|
if (!this.elements) {
|
|
this.elements = [];
|
|
}
|
|
this.elements.push(e);
|
|
return this;
|
|
},
|
|
getElements: function() {
|
|
if (!this.elements) {
|
|
this.elements = [];
|
|
}
|
|
return this.elements;
|
|
}
|
|
};
|
|
|