Merge pull request #474 from frappe/view-modes

View Modes
This commit is contained in:
Safwan Samsudeen 2024-12-03 15:41:45 +05:30 committed by GitHub
commit 02ea61d7c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 518 additions and 687 deletions

4
.gitignore vendored
View File

@ -21,6 +21,7 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html) # Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release build/Release
dist/*
# Dependency directory # Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
@ -29,4 +30,5 @@ node_modules
.DS_Store .DS_Store
gh-pages gh-pages
feedback.md

View File

@ -37,7 +37,7 @@
let tasks = [ let tasks = [
{ {
start: '2024-04-01', start: '2024-04-01',
end: '2024-04-01', end: '2024-04-04',
name: 'Redesign website', name: 'Redesign website',
id: 'Task 0', id: 'Task 0',
progress: 30, progress: 30,
@ -68,8 +68,8 @@
dependencies: 'Task 2', dependencies: 'Task 2',
}, },
{ {
start: '2024-04-08', start: '2024-03-08',
end: '2024-04-10', end: '2024-05-10',
name: 'Deploy', name: 'Deploy',
id: 'Task 4', id: 'Task 4',
progress: 0, progress: 0,
@ -77,7 +77,7 @@
}, },
{ {
start: '2024-04-21', start: '2024-04-21',
end: '2024-04-29', end: '2024-05-29',
name: 'Go Live!', name: 'Go Live!',
id: 'Task 5', id: 'Task 5',
progress: 0, progress: 0,
@ -85,22 +85,22 @@
custom_class: 'bar-milestone', custom_class: 'bar-milestone',
}, },
// { // {
// start: '2014-01-05', // start: '2014-01-05',
// end: '2019-10-12', // end: '2019-10-12',
// name: 'Long term task', // name: 'Long term task',
// id: "Task 6", // id: 'Task 6',
// progress: 0 // progress: 0,
// } // },
]; ];
// Uncomment to test fixed header // Uncomment to test fixed header
tasks = [ // tasks = [
...tasks, // ...tasks,
...Array.from({ length: tasks.length * 3 }, (_, i) => ({ // ...Array.from({ length: tasks.length * 3 }, (_, i) => ({
...tasks[i % 3], // ...tasks[i % 3],
id: i, // id: i,
})), // })),
]; // ];
let gantt_chart = new Gantt('.gantt-target', tasks, { let gantt_chart = new Gantt('.gantt-target', tasks, {
on_click(task) { on_click(task) {
console.log('Click', task); console.log('Click', task);
@ -108,16 +108,14 @@
// on_hover (task, x, y) { // on_hover (task, x, y) {
// console.log("Hover", x, y); // console.log("Hover", x, y);
// } // }
view_mode: 'Day', view_mode: 'Month',
view_mode_padding: { DAY: '3d' }, // view_modes: [
custom_view_modes: [ // {
{ // name: 'Custom Day',
name: 'Custom Day', // padding: '1m',
padding: '1m', // step: '1d',
step: 3, // },
unit: 'day', // ],
},
],
// popup_on: 'click', // popup_on: 'click',
// move_dependencies: false, // move_dependencies: false,
// scroll_to: 'today', // scroll_to: 'today',

View File

@ -28,9 +28,9 @@ export default class Bar {
this.compute_y(); this.compute_y();
this.compute_duration(); this.compute_duration();
this.corner_radius = this.gantt.options.bar_corner_radius; this.corner_radius = this.gantt.options.bar_corner_radius;
this.width = this.gantt.options.column_width * this.duration; this.width = this.gantt.config.column_width * this.duration;
this.progress_width = this.progress_width =
this.gantt.options.column_width * this.gantt.config.column_width *
this.duration * this.duration *
(this.task.progress / 100) || 0; (this.task.progress / 100) || 0;
this.group = createSVG('g', { this.group = createSVG('g', {
@ -107,7 +107,6 @@ export default class Bar {
: ''), : ''),
append_to: this.bar_group, append_to: this.bar_group,
}); });
animateSVG(this.$bar, 'width', 0, this.width); animateSVG(this.$bar, 'width', 0, this.width);
if (this.invalid) { if (this.invalid) {
@ -138,6 +137,7 @@ export default class Bar {
draw_progress_bar() { draw_progress_bar() {
if (this.invalid) return; if (this.invalid) return;
this.$bar_progress = createSVG('rect', { this.$bar_progress = createSVG('rect', {
x: this.x, x: this.x,
y: this.y, y: this.y,
@ -150,8 +150,8 @@ export default class Bar {
}); });
const x = const x =
(date_utils.diff(this.task._start, this.gantt.gantt_start, 'hour') / (date_utils.diff(this.task._start, this.gantt.gantt_start, 'hour') /
this.gantt.options.step) * this.gantt.config.step) *
this.gantt.options.column_width; this.gantt.config.column_width;
let $date_highlight = document.createElement('div'); let $date_highlight = document.createElement('div');
$date_highlight.id = `highlight-${this.task.id}`; $date_highlight.id = `highlight-${this.task.id}`;
@ -285,7 +285,6 @@ export default class Bar {
if (this.gantt.options.popup_on === 'click') { if (this.gantt.options.popup_on === 'click') {
let opened = false; let opened = false;
$.on(this.group, 'click', (e) => { $.on(this.group, 'click', (e) => {
console.log(opened);
if (!opened) { if (!opened) {
this.show_popup(e.offsetX || e.layerX); this.show_popup(e.offsetX || e.layerX);
document.getElementById( document.getElementById(
@ -313,6 +312,7 @@ export default class Bar {
$.on(this.group, 'mouseleave', () => { $.on(this.group, 'mouseleave', () => {
clearTimeout(timeout); clearTimeout(timeout);
this.gantt.popup?.hide?.(); this.gantt.popup?.hide?.();
document.getElementById(`highlight-${task_id}`).style.display = document.getElementById(`highlight-${task_id}`).style.display =
'none'; 'none';
}); });
@ -428,7 +428,6 @@ export default class Bar {
date_changed() { date_changed() {
let changed = false; let changed = false;
const { new_start_date, new_end_date } = this.compute_start_end_date(); const { new_start_date, new_end_date } = this.compute_start_end_date();
if (Number(this.task._start) !== Number(new_start_date)) { if (Number(this.task._start) !== Number(new_start_date)) {
changed = true; changed = true;
this.task._start = new_start_date; this.task._start = new_start_date;
@ -461,15 +460,16 @@ export default class Bar {
compute_start_end_date() { compute_start_end_date() {
const bar = this.$bar; const bar = this.$bar;
const x_in_units = bar.getX() / this.gantt.options.column_width; const x_in_units = bar.getX() / this.gantt.config.column_width;
let new_start_date = date_utils.add( let new_start_date = date_utils.add(
this.gantt.gantt_start, this.gantt.gantt_start,
x_in_units * this.gantt.options.step, x_in_units * this.gantt.config.step,
'hour', this.gantt.config.unit,
); );
const start_offset = const start_offset =
this.gantt.gantt_start.getTimezoneOffset() - this.gantt.gantt_start.getTimezoneOffset() -
new_start_date.getTimezoneOffset(); new_start_date.getTimezoneOffset();
if (start_offset) { if (start_offset) {
new_start_date = date_utils.add( new_start_date = date_utils.add(
new_start_date, new_start_date,
@ -478,11 +478,11 @@ export default class Bar {
); );
} }
const width_in_units = bar.getWidth() / this.gantt.options.column_width; const width_in_units = bar.getWidth() / this.gantt.config.column_width;
const new_end_date = date_utils.add( const new_end_date = date_utils.add(
new_start_date, new_start_date,
width_in_units * this.gantt.options.step, width_in_units * this.gantt.config.step,
'hour', this.gantt.config.unit,
); );
return { new_start_date, new_end_date }; return { new_start_date, new_end_date };
@ -497,7 +497,7 @@ export default class Bar {
compute_expected_progress() { compute_expected_progress() {
this.expected_progress = this.expected_progress =
date_utils.diff(date_utils.today(), this.task._start, 'hour') / date_utils.diff(date_utils.today(), this.task._start, 'hour') /
this.gantt.options.step; this.gantt.config.step;
this.expected_progress = this.expected_progress =
((this.expected_progress < this.duration ((this.expected_progress < this.duration
? this.expected_progress ? this.expected_progress
@ -507,28 +507,36 @@ export default class Bar {
} }
compute_x() { compute_x() {
const { step, column_width } = this.gantt.options; const { step, column_width } = this.gantt.config;
const task_start = this.task._start; const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start; const gantt_start = this.gantt.gantt_start;
const diff = date_utils.diff(task_start, gantt_start, 'hour'); const diff =
let x = (diff / step) * column_width; date_utils.diff(task_start, gantt_start, this.gantt.config.unit) /
this.gantt.config.step;
let x = diff * column_width;
/* Since the column width is based on 30, /* Since the column width is based on 30,
we count the month-difference, multiply it by 30 for a "pseudo-month" we count the month-difference, multiply it by 30 for a "pseudo-month"
and then add the days in the month, making sure the number does not exceed 29 and then add the days in the month, making sure the number does not exceed 29
so it is within the column */ so it is within the column */
if (this.gantt.view_is('Month')) { if (this.gantt.view_is('Month')) {
const diffDaysBasedOn30DayMonths = const diffDaysBasedOn30DayMonths =
date_utils.diff(task_start, gantt_start, 'month') * 30; date_utils.diff(task_start, gantt_start, 'month') * 30;
const dayInMonth = Math.min( const dayInMonth = Math.min(
29, 29,
date_utils.format(task_start, 'DD'), date_utils.format(
task_start,
'DD',
this.gantt.options.language,
),
); );
const diff = diffDaysBasedOn30DayMonths + dayInMonth; const diff = diffDaysBasedOn30DayMonths + dayInMonth;
x = (diff * column_width) / 30; x = (diff * column_width) / 30;
} }
this.x = x; this.x = x;
} }
@ -541,8 +549,11 @@ export default class Bar {
compute_duration() { compute_duration() {
this.duration = this.duration =
date_utils.diff(this.task._end, this.task._start, 'hour') / date_utils.diff(
this.gantt.options.step; this.task._end,
this.task._start,
this.gantt.config.unit,
) / this.gantt.config.step;
} }
get_snap_position(dx) { get_snap_position(dx) {
@ -550,31 +561,31 @@ export default class Bar {
rem, rem,
position; position;
if (this.gantt.view_is('Week')) { // if (this.gantt.view_is('Week')) {
rem = dx % (this.gantt.options.column_width / 7); // rem = dx % (this.gantt.config.column_width / 7);
position = // position =
odx - // odx -
rem + // rem +
(rem < this.gantt.options.column_width / 14 // (rem < this.gantt.config.column_width / 14
? 0 // ? 0
: this.gantt.options.column_width / 7); // : this.gantt.config.column_width / 7);
} else if (this.gantt.view_is('Month')) { // } else if (this.gantt.view_is('Month')) {
rem = dx % (this.gantt.options.column_width / 30); // rem = dx % (this.gantt.config.column_width / 30);
position = // position =
odx - // odx -
rem + // rem +
(rem < this.gantt.options.column_width / 60 // (rem < this.gantt.config.column_width / 60
? 0 // ? 0
: this.gantt.options.column_width / 30); // : this.gantt.config.column_width / 30);
} else { // } else {
rem = dx % this.gantt.options.column_width; rem = dx % this.gantt.config.column_width;
position = position =
odx - odx -
rem + rem +
(rem < this.gantt.options.column_width / 2 (rem < this.gantt.config.column_width / 2
? 0 ? 0
: this.gantt.options.column_width); : this.gantt.config.column_width);
} // }
return position; return position;
} }
@ -592,7 +603,7 @@ export default class Bar {
this.compute_expected_progress(); this.compute_expected_progress();
this.$expected_bar_progress.setAttribute( this.$expected_bar_progress.setAttribute(
'width', 'width',
this.gantt.options.column_width * this.gantt.config.column_width *
this.duration * this.duration *
(this.expected_progress / 100) || 0, (this.expected_progress / 100) || 0,
); );

View File

@ -122,7 +122,7 @@ export default {
return str; return str;
}, },
diff(date_a, date_b, scale = DAY) { diff(date_a, date_b, scale = 'day') {
let milliseconds, seconds, hours, minutes, days, months, years; let milliseconds, seconds, hours, minutes, days, months, years;
milliseconds = date_a - date_b; milliseconds = date_a - date_b;
@ -131,13 +131,14 @@ export default {
hours = minutes / 60; hours = minutes / 60;
days = hours / 24; days = hours / 24;
// Calculate months across years // Calculate months across years
const yearDiff = date_a.getFullYear() - date_b.getFullYear(); let yearDiff = date_a.getFullYear() - date_b.getFullYear();
const monthDiff = date_a.getMonth() - date_b.getMonth(); let monthDiff = date_a.getMonth() - date_b.getMonth();
// calculate extra
monthDiff += (days % 30) / 30;
/* If monthDiff is negative, date_b is in an earlier month than /* If monthDiff is negative, date_b is in an earlier month than
date_a and thus subtracted from the year difference in months */ date_a and thus subtracted from the year difference in months */
months = yearDiff * 12 + monthDiff; months = yearDiff * 12 + monthDiff;
/* If date_a's (e.g. march 1st) day of the month is smaller than date_b (e.g. february 28th), /* If date_a's (e.g. march 1st) day of the month is smaller than date_b (e.g. february 28th),
adjust the month difference */ adjust the month difference */
if (date_a.getDate() < date_b.getDate()) { if (date_a.getDate() < date_b.getDate()) {
@ -151,16 +152,18 @@ export default {
scale += 's'; scale += 's';
} }
return Math.floor( return (
{ Math.round(
milliseconds, {
seconds, milliseconds,
minutes, seconds,
hours, minutes,
days, hours,
months, days,
years, months,
}[scale], years,
}[scale] * 100,
) / 100
); );
}, },
@ -232,6 +235,21 @@ export default {
]; ];
}, },
convert_scales(period, to_scale) {
const TO_DAYS = {
millisecond: 1 / 60 / 60 / 24 / 1000,
second: 1 / 60 / 60 / 24,
minute: 1 / 60 / 24,
hour: 1 / 24,
day: 1,
month: 30,
year: 365,
};
const { duration, scale } = this.parse_duration(period);
let in_days = duration * TO_DAYS[scale];
return in_days / TO_DAYS[to_scale];
},
get_days_in_month(date) { get_days_in_month(date) {
const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@ -248,6 +266,10 @@ export default {
} }
return 28; return 28;
}, },
get_days_in_year(date) {
return date.getFullYear() % 4 ? 365 : 366;
},
}; };
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart

122
src/defaults.js Normal file
View File

@ -0,0 +1,122 @@
import date_utils from './date_utils';
const DEFAULT_VIEW_MODES = [
{
name: 'Hour',
padding: '7d',
step: '1h',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMMM', lang)
: '',
upper_text_frequency: 24,
},
{
name: 'Quarter Day',
padding: '7d',
step: '6h',
format_string: 'YYYY-MM-DD HH',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMM', lang)
: '',
upper_text_frequency: 4,
},
{
name: 'Half Day',
padding: '7d',
step: '12h',
format_string: 'YYYY-MM-DD HH',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
? d.getMonth() !== d.getMonth()
? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang)
: '',
upper_text_frequency: 2,
},
{
name: 'Day',
padding: '14d',
format_string: 'YYYY-MM-DD',
step: '1d',
lower_text: (d, ld, lang) =>
d.getDate() !== ld.getDate() ? date_utils.format(d, 'D', lang) : '',
upper_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang)
: '',
thick_line: (d) => d.getDay() === 1,
},
{
name: 'Week',
padding: '1m',
step: '7d',
column_width: 140,
lower_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang),
upper_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang)
: '',
thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7,
upper_text_frequency: 4,
},
{
name: 'Month',
padding: '2m',
step: '1m',
column_width: 120,
format_string: 'YYYY-MM',
lower_text: 'MMMM',
upper_text: (d, ld, lang) =>
!ld || d.getFullYear() !== ld.getFullYear()
? date_utils.format(d, 'YYYY', lang)
: '',
thick_line: (d) => d.getMonth() % 3 === 0,
default_snap: '7d',
},
{
name: 'Year',
padding: '2y',
step: '1y',
column_width: 120,
format_string: 'YYYY',
upper_text: 'YYYY',
default_snap: '30d',
},
];
const DEFAULT_OPTIONS = {
header_height: 65,
column_width: 30,
view_modes: DEFAULT_VIEW_MODES,
bar_height: 30,
bar_corner_radius: 3,
arrow_curve: 5,
padding: 18,
view_mode: 'Day',
date_format: 'YYYY-MM-DD',
move_dependencies: true,
show_expected_progress: false,
popup: null,
popup_on: 'hover',
language: 'en',
readonly: false,
progress_readonly: false,
dates_readonly: false,
highlight_weekend: true,
scroll_to: 'start',
lines: 'both',
auto_move_label: true,
today_button: true,
view_mode_select: false,
default_snap: '1d',
};
export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES };

View File

@ -10,10 +10,10 @@
--light-border-color: #ebeff2; --light-border-color: #ebeff2;
--light-yellow: #f6e796; --light-yellow: #f6e796;
--holiday-color: #f9fafa; --holiday-color: #f9fafa;
--text-muted: #666; --text-muted: #7c7c7c;
--text-grey: #98a1a9; --text-grey: #98a1a9;
--text-light: #fff; --text-light: #fff;
--text-dark: #111; --text-dark: #171717;
--progress: #ebeef0; --progress: #ebeef0;
--handle-color: #dcdce4; --handle-color: #dcdce4;
--handle-color-important: #94c4f4; --handle-color-important: #94c4f4;
@ -74,7 +74,6 @@
& .lower-text, & .lower-text,
& .upper-text { & .upper-text {
text-anchor: middle; text-anchor: middle;
color: var(--text-dark);
} }
& .upper-header { & .upper-header {
@ -90,6 +89,7 @@
position: absolute; position: absolute;
width: fit-content; width: fit-content;
transform: translateX(-50%); transform: translateX(-50%);
color: var(--text-muted);
} }
& .upper-text { & .upper-text {
@ -97,6 +97,7 @@
width: fit-content; width: fit-content;
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
color: var(--text-dark);
} }
& .current-upper { & .current-upper {
@ -245,6 +246,10 @@
& .bar-label { & .bar-label {
fill: var(--text-light); fill: var(--text-light);
&.big {
fill: var(--text-dark);
}
} }
& .handle { & .handle {

File diff suppressed because it is too large Load Diff