From 0f5d8d702a19bd0ae0c047eaaa788fa13747a2ce Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 12 Jan 2025 19:37:12 -0500 Subject: [PATCH 01/35] support custom index for placing tasks in the same row --- src/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.js b/src/index.js index 93524b63f..76aac3af5 100644 --- a/src/index.js +++ b/src/index.js @@ -165,6 +165,11 @@ export default class Gantt { // cache index task._index = i; + // To support tasks for same row + if (typeof task.custom_index === 'number') { + task._index = task.custom_index; + } + // if hours is not set, assume the last day is full day // e.g: 2018-09-09 becomes 2018-09-09 23:59:59 const task_end_values = date_utils.get_date_values(task._end); From 43677ca22a32ff2a15337ba9fae0983ebc844545 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 12 Jan 2025 19:47:39 -0500 Subject: [PATCH 02/35] improve comments and names --- src/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 76aac3af5..84ec61dd7 100644 --- a/src/index.js +++ b/src/index.js @@ -165,9 +165,9 @@ export default class Gantt { // cache index task._index = i; - // To support tasks for same row - if (typeof task.custom_index === 'number') { - task._index = task.custom_index; + // override the default row index with a custom value for custom row positioning. + if (typeof task.row_override === 'number') { + task._index = task.row_override; } // if hours is not set, assume the last day is full day From 188ffac1833d739f38a8b5e5d018b748ea418182 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Fri, 17 Jan 2025 10:16:25 -0500 Subject: [PATCH 03/35] enable side task list --- src/defaults.js | 12 +++++--- src/index.js | 69 ++++++++++++++++++++++++++++++++++++++++++-- src/styles/gantt.css | 32 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/defaults.js b/src/defaults.js index e63e8ef5c..021cf1802 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -115,14 +115,13 @@ const DEFAULT_OPTIONS = { container_height: 'auto', column_width: null, date_format: 'YYYY-MM-DD HH:mm', - upper_header_height: 45, - lower_header_height: 30, - snap_at: null, - infinite_padding: true, + enable_side_task_list: false, holidays: { 'var(--g-weekend-highlight-color)': 'weekend' }, ignore: [], + infinite_padding: true, language: 'en', lines: 'both', + lower_header_height: 30, move_dependencies: true, padding: 18, popup: (ctx) => { @@ -151,7 +150,12 @@ const DEFAULT_OPTIONS = { readonly: false, scroll_to: 'today', show_expected_progress: false, + side_task_list: { + width: 200, + }, + snap_at: null, today_button: true, + upper_header_height: 45, view_mode: 'Day', view_mode_select: false, view_modes: DEFAULT_VIEW_MODES, diff --git a/src/index.js b/src/index.js index 84ec61dd7..0a29c0dab 100644 --- a/src/index.js +++ b/src/index.js @@ -45,11 +45,16 @@ export default class Gantt { ); } + this.$main_wrapper = this.create_el({ + classes: 'main-wrapper', + append_to: wrapper_element, + }); + // svg element if (!svg_element) { // create it this.$svg = createSVG('svg', { - append_to: wrapper_element, + append_to: this.$main_wrapper, class: 'gantt', }); } else { @@ -363,6 +368,7 @@ export default class Gantt { this.make_bars(); this.make_arrows(); this.map_arrows_on_bars(); + this.fill_side_task_list(); this.set_dimensions(); this.set_scroll_position(this.options.scroll_to); } @@ -393,6 +399,7 @@ export default class Gantt { this.make_grid_background(); this.make_grid_rows(); this.make_grid_header(); + this.make_side_task_list(); this.make_side_header(); } @@ -472,6 +479,27 @@ export default class Gantt { }); } + make_side_task_list() { + if (!this.options.enable_side_task_list) { + return; + } + + this.$side_task_list_fixer_container = this.create_el({ + width: this.options.side_task_list.width, + classes: 'side-task-list-fixer', + prepend_to: this.$main_wrapper, + }); + + this.$side_task_list_container = this.create_el({ + width: this.options.side_task_list.width, + classes: 'side-task-list', + append_to: this.$main_wrapper, + }); + + this.$side_task_list_fixer_container.style.flexBasis = + this.options.side_task_list.width + 'px'; + } + make_side_header() { this.$side_header = this.create_el({ classes: 'side-header' }); this.$upper_header.prepend(this.$side_header); @@ -758,7 +786,17 @@ export default class Gantt { if (!highlightDimensions) return; } - create_el({ left, top, width, height, id, classes, append_to, type }) { + create_el({ + left, + top, + width, + height, + id, + classes, + append_to, + prepend_to, + type, + }) { let $el = document.createElement(type || 'div'); for (let cls of classes.split(' ')) $el.classList.add(cls); $el.style.top = top + 'px'; @@ -767,6 +805,7 @@ export default class Gantt { if (width) $el.style.width = width + 'px'; if (height) $el.style.height = height + 'px'; if (append_to) append_to.appendChild($el); + if (prepend_to) prepend_to.prepend($el); return $el; } @@ -859,6 +898,32 @@ export default class Gantt { }; } + fill_side_task_list() { + if (!this.options.enable_side_task_list) { + return; + } + + this.tasks.forEach((task, index) => { + const taskRow = this.create_el({ + classes: 'side-task-list-row', + append_to: this.$side_task_list_container, + }); + taskRow.textContent = task.name; + + taskRow.style.height = + this.options.bar_height + this.options.padding + 'px'; + }); + + // add empty row for cover little empty row from grid + const emptyTaskRow = this.create_el({ + classes: 'side-task-list-row', + append_to: this.$side_task_list_container, + }); + + // adding -1 to remove unnecessary scroll + emptyTaskRow.style.height = -1 + this.options.padding / 2 + 'px'; + } + make_bars() { this.bars = this.tasks.map((task) => { const bar = new Bar(this, task); diff --git a/src/styles/gantt.css b/src/styles/gantt.css index bddfc4764..59ea7e57c 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -1,10 +1,16 @@ @import './light.css'; +.main-wrapper { + display: flex; + position: relative; +} + .gantt-container { line-height: 14.5px; position: relative; overflow: auto; font-size: 12px; + flex: 1; height: var(--gv-grid-height); width: 100%; border-radius: 8px; @@ -343,3 +349,29 @@ } } } + +.side-task-list { + border-right: var(--g-border-color); + bottom: 0; + box-shadow: 5px 0px 5px -5px rgba(0, 0, 0, 0.3); + overflow-y: auto; + position: absolute; + z-index: 1001; + + & .side-task-list-row { + align-items: center; + background: var(--g-row-color); + display: flex; + dominant-baseline: central; + font-family: Helvetica; + font-size: 13px; + font-weight: 400; + justify-content: center; + text-align: left; + } + + & .side-task-list-row:not(:last-child) { + border-bottom: 1px solid var(--g-border-color); + box-sizing: border-box; + } +} \ No newline at end of file From 33e10244527b0b1a36f93774f07a62d3073a43fb Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Fri, 17 Jan 2025 10:20:33 -0500 Subject: [PATCH 04/35] add end of file to css --- src/styles/gantt.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/gantt.css b/src/styles/gantt.css index 505a32cc6..58f76c416 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -370,4 +370,4 @@ border-bottom: 1px solid var(--g-border-color); box-sizing: border-box; } -} \ No newline at end of file +} From be8bcb96e8409b6e47f14b650dc09d904133a915 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 15:12:19 +0000 Subject: [PATCH 05/35] control label display of bars --- src/bar.js | 4 +++- src/defaults.js | 1 + src/index.js | 13 ++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/bar.js b/src/bar.js index 7f68f4a35..f1ac3e964 100644 --- a/src/bar.js +++ b/src/bar.js @@ -220,10 +220,12 @@ export default class Bar { x_coord = this.x + this.image_size + 5; } + const displayBarLabel = !this.gantt.options.enable_side_task_list || this.gantt.options.enable_side_task_list && this.gantt.options.side_task_list.display_bar_labels; + createSVG('text', { x: x_coord, y: this.y + this.height / 2, - innerHTML: this.task.name, + innerHTML: displayBarLabel ? this.task.name : '', class: 'bar-label', append_to: this.bar_group, }); diff --git a/src/defaults.js b/src/defaults.js index 021cf1802..0eae7a664 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -151,6 +151,7 @@ const DEFAULT_OPTIONS = { scroll_to: 'today', show_expected_progress: false, side_task_list: { + display_bar_labels: false, width: 200, }, snap_at: null, diff --git a/src/index.js b/src/index.js index ae17e556f..63a2699a5 100644 --- a/src/index.js +++ b/src/index.js @@ -77,7 +77,7 @@ export default class Gantt { setup_options(options) { this.original_options = options; - this.options = { ...DEFAULT_OPTIONS, ...options }; + this.options = deepMerge(DEFAULT_OPTIONS, options); const CSS_VARIABLES = { 'grid-height': 'container_height', 'bar-height': 'bar_height', @@ -1649,3 +1649,14 @@ function generate_id(task) { function sanitize(s) { return s.replaceAll(' ', '_').replaceAll(':', '_').replaceAll('.', '_'); } + +function deepMerge(target, source) { + for (const key in source) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + target[key] = deepMerge(target[key] || {}, source[key]); + } else { + target[key] = source[key]; + } + } + return target; +} From f1c4408f8d1cc3f17687f1444c84fadafbd253fe Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 15:12:50 +0000 Subject: [PATCH 06/35] prettier --- src/bar.js | 5 ++++- src/index.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/bar.js b/src/bar.js index f1ac3e964..1ee038e58 100644 --- a/src/bar.js +++ b/src/bar.js @@ -220,7 +220,10 @@ export default class Bar { x_coord = this.x + this.image_size + 5; } - const displayBarLabel = !this.gantt.options.enable_side_task_list || this.gantt.options.enable_side_task_list && this.gantt.options.side_task_list.display_bar_labels; + const displayBarLabel = + !this.gantt.options.enable_side_task_list || + (this.gantt.options.enable_side_task_list && + this.gantt.options.side_task_list.display_bar_labels); createSVG('text', { x: x_coord, diff --git a/src/index.js b/src/index.js index 63a2699a5..c6bf2afeb 100644 --- a/src/index.js +++ b/src/index.js @@ -1652,7 +1652,11 @@ function sanitize(s) { function deepMerge(target, source) { for (const key in source) { - if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + if ( + source[key] && + typeof source[key] === 'object' && + !Array.isArray(source[key]) + ) { target[key] = deepMerge(target[key] || {}, source[key]); } else { target[key] = source[key]; From 6235322246c25533bd2757de46135e199febfa98 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 15:23:42 +0000 Subject: [PATCH 07/35] control scroll for side task list --- src/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.js b/src/index.js index c6bf2afeb..14d116d3a 100644 --- a/src/index.js +++ b/src/index.js @@ -117,6 +117,10 @@ export default class Gantt { } else { this.config.ignored_function = this.options.ignore; } + + if (this.options.enable_side_task_list) { + this.options.scroll_to = 'start'; + } } update_options(options) { From 25bf8a8080bea75066d65f1c128f9c5be0fb4aa4 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:08:09 +0000 Subject: [PATCH 08/35] add task group concept --- src/defaults.js | 2 ++ src/index.js | 42 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/defaults.js b/src/defaults.js index 0eae7a664..7bc4fc520 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -155,6 +155,8 @@ const DEFAULT_OPTIONS = { width: 200, }, snap_at: null, + task_groups: [], + task_groups_enabled: false, today_button: true, upper_header_height: 45, view_mode: 'Day', diff --git a/src/index.js b/src/index.js index 14d116d3a..99ee98e5b 100644 --- a/src/index.js +++ b/src/index.js @@ -121,6 +121,10 @@ export default class Gantt { if (this.options.enable_side_task_list) { this.options.scroll_to = 'start'; } + + this.options.task_groups_enabled = + Array.isArray(this.options.task_groups) && + this.options.task_groups.length > 0; } update_options(options) { @@ -171,12 +175,10 @@ export default class Gantt { return false; } - // cache index - task._index = i; - - // override the default row index with a custom value for custom row positioning. - if (typeof task.row_override === 'number') { - task._index = task.row_override; + const { is_valid, message } = this.cache_index(task, i); + if (!is_valid) { + console.error(message); + return false; } // if hours is not set, assume the last day is full day @@ -216,6 +218,34 @@ export default class Gantt { this.setup_dependencies(); } + cache_index(task, index) { + if (!this.options.task_groups_enabled) { + // set to default behavior + task._index = index; + return { is_valid: true }; + } + + if (!task.task_group_id) { + return { + is_valid: false, + message: `missing "task_group_id" property on task "${task.id}" since "task_groups" are defined`, + }; + } + + const task_group_index = this.options.task_groups.findIndex( + (task_group) => task_group.id === task.task_group_id, + ); + if (task_group_index < 0) { + return { + is_valid: false, + message: `"task_group_id" not found in "task_groups" for task "${task.id}"`, + }; + } + + task._index = task_group_index; + return { is_valid: true }; + } + setup_dependencies() { this.dependency_map = {}; for (let t of this.tasks) { From 42ebcf201cd3ebb0bf659ade49d54d72f7ecd36d Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:16:02 +0000 Subject: [PATCH 09/35] control title on task groups --- src/defaults.js | 7 ++++++- src/popup.js | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/defaults.js b/src/defaults.js index 7bc4fc520..8c3eec0c5 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -125,7 +125,12 @@ const DEFAULT_OPTIONS = { move_dependencies: true, padding: 18, popup: (ctx) => { - ctx.set_title(ctx.task.name); + let title = ctx.task.name; + if (ctx.task_group) { + title = `${ctx.task.name} (${ctx.task_group.name})`; + } + ctx.set_title(title); + if (ctx.task.description) ctx.set_subtitle(ctx.task.description); else ctx.set_subtitle(''); diff --git a/src/popup.js b/src/popup.js index fe30a5be5..c1bb2ec4a 100644 --- a/src/popup.js +++ b/src/popup.js @@ -24,8 +24,13 @@ export default class Popup { show({ x, y, task, target }) { this.actions.innerHTML = ''; + // TODO: if task_group is not found, should this fail? + const task_group = this.gantt.options.task_groups.find( + (task_group) => task_group.id === task.task_group_id, + ); let html = this.popup_func({ task, + task_group, chart: this.gantt, get_title: () => this.title, set_title: (title) => (this.title.innerHTML = title), From 99dac5b1dd7ba680ee3edda0f465e5474470218a Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:35:37 +0000 Subject: [PATCH 10/35] align side list with task groups --- src/index.js | 27 ++++++++++++++++++++------- src/popup.js | 4 +--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 99ee98e5b..3b4994f87 100644 --- a/src/index.js +++ b/src/index.js @@ -441,11 +441,14 @@ export default class Gantt { make_grid_background() { const grid_width = this.dates.length * this.config.column_width; + const itemList = this.options.task_groups_enabled + ? this.options.task_groups + : this.tasks; const grid_height = Math.max( this.config.header_height + this.options.padding + (this.options.bar_height + this.options.padding) * - this.tasks.length - + itemList.length - 10, this.options.container_height !== 'auto' ? this.options.container_height @@ -934,25 +937,29 @@ export default class Gantt { return; } - this.tasks.forEach((task, index) => { - const taskRow = this.create_el({ + const itemList = this.options.task_groups_enabled + ? this.options.task_groups + : this.tasks; + + itemList.forEach((item, index) => { + const row = this.create_el({ classes: 'side-task-list-row', append_to: this.$side_task_list_container, }); - taskRow.textContent = task.name; + row.textContent = item.name; - taskRow.style.height = + row.style.height = this.options.bar_height + this.options.padding + 'px'; }); // add empty row for cover little empty row from grid - const emptyTaskRow = this.create_el({ + const emptyRow = this.create_el({ classes: 'side-task-list-row', append_to: this.$side_task_list_container, }); // adding -1 to remove unnecessary scroll - emptyTaskRow.style.height = -1 + this.options.padding / 2 + 'px'; + emptyRow.style.height = -1 + this.options.padding / 2 + 'px'; } make_bars() { @@ -1651,6 +1658,12 @@ export default class Gantt { ); } + get_task_group_for_task(task) { + return this.options.task_groups.find( + (task_group) => task_group.id === task.task_group_id, + ); + } + /** * Clear all elements from the parent svg element * diff --git a/src/popup.js b/src/popup.js index c1bb2ec4a..d6dd63edf 100644 --- a/src/popup.js +++ b/src/popup.js @@ -25,9 +25,7 @@ export default class Popup { show({ x, y, task, target }) { this.actions.innerHTML = ''; // TODO: if task_group is not found, should this fail? - const task_group = this.gantt.options.task_groups.find( - (task_group) => task_group.id === task.task_group_id, - ); + const task_group = this.gantt.get_task_group_for_task(task); let html = this.popup_func({ task, task_group, From 1532aa3da01654faa1100170d623f3b4dc790029 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:40:53 +0000 Subject: [PATCH 11/35] rename to left sidebar list --- src/bar.js | 6 +++--- src/defaults.js | 4 ++-- src/index.js | 38 +++++++++++++++++++------------------- src/styles/gantt.css | 6 +++--- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/bar.js b/src/bar.js index 1ee038e58..4db25bbfe 100644 --- a/src/bar.js +++ b/src/bar.js @@ -221,9 +221,9 @@ export default class Bar { } const displayBarLabel = - !this.gantt.options.enable_side_task_list || - (this.gantt.options.enable_side_task_list && - this.gantt.options.side_task_list.display_bar_labels); + !this.gantt.options.enable_left_sidebar_list || + (this.gantt.options.enable_left_sidebar_list && + this.gantt.options.left_sidebar_list.display_bar_labels); createSVG('text', { x: x_coord, diff --git a/src/defaults.js b/src/defaults.js index 8c3eec0c5..e79ba88b3 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -115,7 +115,7 @@ const DEFAULT_OPTIONS = { container_height: 'auto', column_width: null, date_format: 'YYYY-MM-DD HH:mm', - enable_side_task_list: false, + enable_left_sidebar_list: false, holidays: { 'var(--g-weekend-highlight-color)': 'weekend' }, ignore: [], infinite_padding: true, @@ -155,7 +155,7 @@ const DEFAULT_OPTIONS = { readonly: false, scroll_to: 'today', show_expected_progress: false, - side_task_list: { + left_sidebar_list: { display_bar_labels: false, width: 200, }, diff --git a/src/index.js b/src/index.js index 3b4994f87..751390ef1 100644 --- a/src/index.js +++ b/src/index.js @@ -118,7 +118,7 @@ export default class Gantt { this.config.ignored_function = this.options.ignore; } - if (this.options.enable_side_task_list) { + if (this.options.enable_left_sidebar_list) { this.options.scroll_to = 'start'; } @@ -399,7 +399,7 @@ export default class Gantt { this.make_bars(); this.make_arrows(); this.map_arrows_on_bars(); - this.fill_side_task_list(); + this.fill_left_sidebar_list(); this.set_dimensions(); this.set_scroll_position(this.options.scroll_to); } @@ -430,7 +430,7 @@ export default class Gantt { this.make_grid_background(); this.make_grid_rows(); this.make_grid_header(); - this.make_side_task_list(); + this.make_left_sidebar_list(); this.make_side_header(); } @@ -513,25 +513,25 @@ export default class Gantt { }); } - make_side_task_list() { - if (!this.options.enable_side_task_list) { + make_left_sidebar_list() { + if (!this.options.enable_left_sidebar_list) { return; } - this.$side_task_list_fixer_container = this.create_el({ - width: this.options.side_task_list.width, - classes: 'side-task-list-fixer', + this.$left_sidebar_list_fixer_container = this.create_el({ + width: this.options.left_sidebar_list.width, + classes: 'left-sidebar-list-fixer', prepend_to: this.$main_wrapper, }); - this.$side_task_list_container = this.create_el({ - width: this.options.side_task_list.width, - classes: 'side-task-list', + this.$left_sidebar_list_container = this.create_el({ + width: this.options.left_sidebar_list.width, + classes: 'left-sidebar-list', append_to: this.$main_wrapper, }); - this.$side_task_list_fixer_container.style.flexBasis = - this.options.side_task_list.width + 'px'; + this.$left_sidebar_list_fixer_container.style.flexBasis = + this.options.left_sidebar_list.width + 'px'; } make_side_header() { @@ -932,8 +932,8 @@ export default class Gantt { }; } - fill_side_task_list() { - if (!this.options.enable_side_task_list) { + fill_left_sidebar_list() { + if (!this.options.enable_left_sidebar_list) { return; } @@ -943,8 +943,8 @@ export default class Gantt { itemList.forEach((item, index) => { const row = this.create_el({ - classes: 'side-task-list-row', - append_to: this.$side_task_list_container, + classes: 'left-sidebar-list-row', + append_to: this.$left_sidebar_list_container, }); row.textContent = item.name; @@ -954,8 +954,8 @@ export default class Gantt { // add empty row for cover little empty row from grid const emptyRow = this.create_el({ - classes: 'side-task-list-row', - append_to: this.$side_task_list_container, + classes: 'left-sidebar-list-row', + append_to: this.$left_sidebar_list_container, }); // adding -1 to remove unnecessary scroll diff --git a/src/styles/gantt.css b/src/styles/gantt.css index 58f76c416..aacb29c4d 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -346,7 +346,7 @@ } } -.side-task-list { +.left-sidebar-list { border-right: var(--g-border-color); bottom: 0; box-shadow: 5px 0px 5px -5px rgba(0, 0, 0, 0.3); @@ -354,7 +354,7 @@ position: absolute; z-index: 1001; - & .side-task-list-row { + & .left-sidebar-list-row { align-items: center; background: var(--g-row-color); display: flex; @@ -366,7 +366,7 @@ text-align: left; } - & .side-task-list-row:not(:last-child) { + & .left-sidebar-list-row:not(:last-child) { border-bottom: 1px solid var(--g-border-color); box-sizing: border-box; } From 6a23101f6d19133d90458c8fd963445f8a9dfcb9 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:42:10 +0000 Subject: [PATCH 12/35] rename variable --- src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 751390ef1..2c1a6bf1f 100644 --- a/src/index.js +++ b/src/index.js @@ -441,14 +441,14 @@ export default class Gantt { make_grid_background() { const grid_width = this.dates.length * this.config.column_width; - const itemList = this.options.task_groups_enabled + const sidebar_list_items = this.options.task_groups_enabled ? this.options.task_groups : this.tasks; const grid_height = Math.max( this.config.header_height + this.options.padding + (this.options.bar_height + this.options.padding) * - itemList.length - + sidebar_list_items.length - 10, this.options.container_height !== 'auto' ? this.options.container_height @@ -937,11 +937,11 @@ export default class Gantt { return; } - const itemList = this.options.task_groups_enabled + const sidebar_list_items = this.options.task_groups_enabled ? this.options.task_groups : this.tasks; - itemList.forEach((item, index) => { + sidebar_list_items.forEach((item, index) => { const row = this.create_el({ classes: 'left-sidebar-list-row', append_to: this.$left_sidebar_list_container, From c2a5cb761945afc33b55cd78dcd77b01be7aa4f5 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 16:46:37 +0000 Subject: [PATCH 13/35] rename sidebar list config --- src/bar.js | 2 +- src/defaults.js | 2 +- src/index.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bar.js b/src/bar.js index 4db25bbfe..20868636c 100644 --- a/src/bar.js +++ b/src/bar.js @@ -223,7 +223,7 @@ export default class Bar { const displayBarLabel = !this.gantt.options.enable_left_sidebar_list || (this.gantt.options.enable_left_sidebar_list && - this.gantt.options.left_sidebar_list.display_bar_labels); + this.gantt.options.left_sidebar_list_config.display_bar_labels); createSVG('text', { x: x_coord, diff --git a/src/defaults.js b/src/defaults.js index e79ba88b3..35507416c 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -155,7 +155,7 @@ const DEFAULT_OPTIONS = { readonly: false, scroll_to: 'today', show_expected_progress: false, - left_sidebar_list: { + left_sidebar_list_config: { display_bar_labels: false, width: 200, }, diff --git a/src/index.js b/src/index.js index 2c1a6bf1f..15f4a408a 100644 --- a/src/index.js +++ b/src/index.js @@ -519,19 +519,19 @@ export default class Gantt { } this.$left_sidebar_list_fixer_container = this.create_el({ - width: this.options.left_sidebar_list.width, + width: this.options.left_sidebar_list_config.width, classes: 'left-sidebar-list-fixer', prepend_to: this.$main_wrapper, }); this.$left_sidebar_list_container = this.create_el({ - width: this.options.left_sidebar_list.width, + width: this.options.left_sidebar_list_config.width, classes: 'left-sidebar-list', append_to: this.$main_wrapper, }); this.$left_sidebar_list_fixer_container.style.flexBasis = - this.options.left_sidebar_list.width + 'px'; + this.options.left_sidebar_list_config.width + 'px'; } make_side_header() { From 910cf932af16ba5c22be2b0388fcc3d6d30ca921 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 17:25:02 +0000 Subject: [PATCH 14/35] add readme docs about sidebar list and task groups --- README.md | 76 ++++++++++++++++++++++++++++++------------------- src/defaults.js | 10 +++---- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 5195f34d4..3295d2906 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ let tasks = [ name: 'Redesign website', start: '2016-12-28', end: '2016-12-31', - progress: 20 + progress: 20, + task_group_id: 'task-group-1', }, ... ] @@ -65,35 +66,36 @@ Frappe Gantt offers a wide range of options to customize your chart. | **Option** | **Description** | **Possible Values** | **Default** | -|---------------------------|---------------------------------------------------------------------------------|----------------------------------------------------|------------------------------------| -| `arrow_curve` | Curve radius of arrows connecting dependencies. | Any positive integer. | `5` | -| `auto_move_label` | Move task labels when user scrolls horizontally. | `true`, `false` | `false` | -| `bar_corner_radius` | Radius of the task bar corners (in pixels). | Any positive integer. | `3` | -| `bar_height` | Height of task bars (in pixels). | Any positive integer. | `30` | -| `container_height` | Height of the container. | `auto` - dynamic container height to fit all tasks - _or_ any positive integer (for pixels). | `auto` | -| `column_width` | Width of each column in the timeline. | Any positive integer. | 45 | -| `date_format` | Format for displaying dates. | Any valid JS date format string. | `YYYY-MM-DD` | -| `upper_header_height` | Height of the upper header in the timeline (in pixels). | Any positive integer. | `45` | -| `lower_header_height` | Height of the lower header in the timeline (in pixels). | Any positive integer. | `30` | -| `snap_at` | Snap tasks at particular intervel while resizing or dragging. | Any _interval_ (see below) | `1d` | -| `infinite_padding` | Whether to extend timeline infinitely when user scrolls. | `true`, `false` | `true` | -| `holidays` | Highlighted holidays on the timeline. | Object mapping CSS colors to holiday types. Types can either be a) 'weekend', or b) array of _strings_ or _date objects_ or _objects_ in the format `{date: ..., label: ...}` | `{ 'var(--g-weekend-highlight-color)': 'weekend' }` | -| `ignore` | Ignored areas in the rendering | `weekend` _or_ Array of strings or date objects (`weekend` can be present to the array also). | `[]` | -| `language` | Language for localization. | ISO 639-1 codes like `en`, `fr`, `es`. | `en` | -| `lines` | Determines which grid lines to display. | `none` for no lines, `vertical` for only vertical lines, `horizontal` for only horizontal lines, `both` for complete grid. | `both` | -| `move_dependencies` | Whether moving a task automatically moves its dependencies. | `true`, `false` | `true` | -| `padding` | Padding around task bars (in pixels). | Any positive integer. | `18` | -| `popup_on` | Event to trigger the popup display. | `click` _or_ `hover` | `click` | -| `readonly_progress` | Disables editing task progress. | `true`, `false` | `false` | -| `readonly_dates` | Disables editing task dates. | `true`, `false` | `false` | -| `readonly` | Disables all editing features. | `true`, `false` | `false` | -| `scroll_to` | Determines the starting point when chart is rendered. | `today`, `start`, `end`, or a date string. | `today` | -| `show_expected_progress` | Shows expected progress for tasks. | `true`, `false` | `false` | -| `today_button` | Adds a button to navigate to today’s date. | `true`, `false` | `true` | -| `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` | -| `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` | - -Apart from these ones, two options - `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. +|----------------------------|---------------------------------------------------------------------------------|----------------------------------------------------|------------------------------------| +| `arrow_curve` | Curve radius of arrows connecting dependencies. | Any positive integer. | `5` | +| `auto_move_label` | Move task labels when user scrolls horizontally. | `true`, `false` | `false` | +| `bar_corner_radius` | Radius of the task bar corners (in pixels). | Any positive integer. | `3` | +| `bar_height` | Height of task bars (in pixels). | Any positive integer. | `30` | +| `container_height` | Height of the container. | `auto` - dynamic container height to fit all tasks - _or_ any positive integer (for pixels). | `auto` | +| `column_width` | Width of each column in the timeline. | Any positive integer. | 45 | +| `date_format` | Format for displaying dates. | Any valid JS date format string. | `YYYY-MM-DD` | +| `enable_left_sidebar_list` | Enable clasical left sidebar with tasks. | `true`, `false` | `false` | +| `holidays` | Highlighted holidays on the timeline. | Object mapping CSS colors to holiday types. Types can either be a) 'weekend', or b) array of _strings_ or _date objects_ or _objects_ in the format `{date: ..., label: ...}` | `{ 'var(--g-weekend-highlight-color)': 'weekend' }` | +| `ignore` | Ignored areas in the rendering | `weekend` _or_ Array of strings or date objects (`weekend` can be present to the array also). | `[]` | +| `infinite_padding` | Whether to extend timeline infinitely when user scrolls. | `true`, `false` | `true` | +| `language` | Language for localization. | ISO 639-1 codes like `en`, `fr`, `es`. | `en` | +| `lines` | Determines which grid lines to display. | `none` for no lines, `vertical` for only vertical lines, `horizontal` for only horizontal lines, `both` for complete grid. | `both` | +| `lower_header_height` | Height of the lower header in the timeline (in pixels). | Any positive integer. | `30` | +| `move_dependencies` | Whether moving a task automatically moves its dependencies. | `true`, `false` | `true` | +| `padding` | Padding around task bars (in pixels). | Any positive integer. | `18` | +| `popup_on` | Event to trigger the popup display. | `click` _or_ `hover` | `click` | +| `readonly` | Disables all editing features. | `true`, `false` | `false` | +| `readonly_dates` | Disables editing task dates. | `true`, `false` | `false` | +| `readonly_progress` | Disables editing task progress. | `true`, `false` | `false` | +| `scroll_to` | Determines the starting point when chart is rendered. | `today`, `start`, `end`, or a date string. | `today` | +| `show_expected_progress` | Shows expected progress for tasks. | `true`, `false` | `false` | +| `snap_at` | Snap tasks at particular intervel while resizing or dragging. | Any _interval_ (see below) | `1d` | +| `today_button` | Adds a button to navigate to today’s date. | `true`, `false` | `true` | +| `upper_header_height` | Height of the upper header in the timeline (in pixels). | Any positive integer. | `45` | +| `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` | +| `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` | + +Apart from these ones, four options - `left_sidebar_list_config`, `popup`, `task_groups` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. #### View Mode Configuration The `view_modes` option determines all the available view modes for the chart. It should be an array of objects. @@ -121,11 +123,25 @@ For details, see the above table. The function receives one object as an argument, containing: - `task` - the task as an object +- `task_group` - the related task group as an object. **NOTE:** it is `undefined` if task groups feature is disabled - `chart` - the entire Gantt chart - `get_title`, `get_subtitle`, `get_details` (functions) - get the relevant section as a HTML node. - `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section - `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed. +#### Left Sidebar List Configuration +`left_sidebar_list_config` is an object. +- `display_bar_labels` (boolean) - define if labels are displayed in bars +- `width`, the width of the sidebar (in pixels) + +#### Task Groups Configuration +`task_groups` is an array of objects representing groupings or categories for tasks. By passing it, bars will be gather in the same row. It should be an array of objects. +**NOTE:** This feature **doesn't support** date overlaps for tasks within the same task group. + +Each object can have the following properties: +- `id` (string) - the id of the task group. +- `name` (string) - the name of the task group. + ### API Frappe Gantt exposes a few helpful methods for you to interact with the chart: diff --git a/src/defaults.js b/src/defaults.js index 35507416c..82b865183 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -120,6 +120,10 @@ const DEFAULT_OPTIONS = { ignore: [], infinite_padding: true, language: 'en', + left_sidebar_list_config: { + display_bar_labels: false, + width: 200, + }, lines: 'both', lower_header_height: 30, move_dependencies: true, @@ -150,15 +154,11 @@ const DEFAULT_OPTIONS = { ); }, popup_on: 'click', + readonly: false, readonly_progress: false, readonly_dates: false, - readonly: false, scroll_to: 'today', show_expected_progress: false, - left_sidebar_list_config: { - display_bar_labels: false, - width: 200, - }, snap_at: null, task_groups: [], task_groups_enabled: false, From 09e248a65aa170ab43bd6f33dd0b2533cb3171e2 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 17:42:50 +0000 Subject: [PATCH 15/35] set package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fa9e5c24..c8fc7d05d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frappe-gantt", - "version": "1.0.2", + "version": "1.1.0", "description": "A simple, modern, interactive gantt library for the web", "main": "src/index.js", "type": "module", From ddbbbd4847d1d960c8698a7553f84c2fae5f4c54 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 18 Jan 2025 23:58:20 +0000 Subject: [PATCH 16/35] improve scroll_to with sidebar. Add custom config to bars --- README.md | 7 +++++-- src/bar.js | 25 +++++++++++++++++++------ src/defaults.js | 2 +- src/index.js | 5 ++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3295d2906..102021921 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Frappe Gantt offers a wide range of options to customize your chart. | `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` | | `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` | -Apart from these ones, four options - `left_sidebar_list_config`, `popup`, `task_groups` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. +Apart from these ones, four options - `custom_config_bar`, `left_sidebar_list_config`, `popup`, `task_groups` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. #### View Mode Configuration The `view_modes` option determines all the available view modes for the chart. It should be an array of objects. @@ -129,9 +129,12 @@ The function receives one object as an argument, containing: - `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section - `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed. +#### Custom Config Bar +`custom_config_bar` is an function that should return an object to configure bars. Options from this object: +- `label` (string) - the label displayed inside the bar. If it's not defined, name of task will be used + #### Left Sidebar List Configuration `left_sidebar_list_config` is an object. -- `display_bar_labels` (boolean) - define if labels are displayed in bars - `width`, the width of the sidebar (in pixels) #### Task Groups Configuration diff --git a/src/bar.js b/src/bar.js index 20868636c..37b44e56c 100644 --- a/src/bar.js +++ b/src/bar.js @@ -48,6 +48,9 @@ export default class Bar { } prepare_values() { + const { label } = this.get_config(); + this.label = label; + this.invalid = this.task.invalid; this.height = this.gantt.options.bar_height; this.image_size = this.height - 5; @@ -62,6 +65,21 @@ export default class Bar { if (this.task.progress > 100) this.task.progress = 100; } + get_config() { + const default_config = { + label: this.task.name, + }; + + if (typeof this.gantt.options.custom_config_bar === 'function') { + return { + ...default_config, + ...this.gantt.options.custom_config_bar({ task: this.task }), + }; + } + + return default_config; + } + prepare_helpers() { SVGElement.prototype.getX = function () { return +this.getAttribute('x'); @@ -220,15 +238,10 @@ export default class Bar { x_coord = this.x + this.image_size + 5; } - const displayBarLabel = - !this.gantt.options.enable_left_sidebar_list || - (this.gantt.options.enable_left_sidebar_list && - this.gantt.options.left_sidebar_list_config.display_bar_labels); - createSVG('text', { x: x_coord, y: this.y + this.height / 2, - innerHTML: displayBarLabel ? this.task.name : '', + innerHTML: this.label, class: 'bar-label', append_to: this.bar_group, }); diff --git a/src/defaults.js b/src/defaults.js index 82b865183..7749c3c2e 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -114,6 +114,7 @@ const DEFAULT_OPTIONS = { bar_height: 30, container_height: 'auto', column_width: null, + custom_config_bar: null, date_format: 'YYYY-MM-DD HH:mm', enable_left_sidebar_list: false, holidays: { 'var(--g-weekend-highlight-color)': 'weekend' }, @@ -121,7 +122,6 @@ const DEFAULT_OPTIONS = { infinite_padding: true, language: 'en', left_sidebar_list_config: { - display_bar_labels: false, width: 200, }, lines: 'both', diff --git a/src/index.js b/src/index.js index 15f4a408a..a9a8fa36e 100644 --- a/src/index.js +++ b/src/index.js @@ -118,7 +118,10 @@ export default class Gantt { this.config.ignored_function = this.options.ignore; } - if (this.options.enable_left_sidebar_list) { + if ( + !this.original_options.scroll_to && + this.options.enable_left_sidebar_list + ) { this.options.scroll_to = 'start'; } From cc42519717825ce2311c312c4f053b5803e3e538 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 19 Jan 2025 00:04:50 +0000 Subject: [PATCH 17/35] pass task_group to custom_config_bar --- README.md | 8 +++++++- src/bar.js | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 102021921..6cd7ab4b5 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,13 @@ The function receives one object as an argument, containing: - `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed. #### Custom Config Bar -`custom_config_bar` is an function that should return an object to configure bars. Options from this object: +`custom_config_bar` is a function that should return an object to configure bars. Options from this object: + +The function receives one object as an argument, containing: +- `task` - the task as an object +- `task_group` - the related task group as an object. **NOTE:** it is `undefined` if task groups feature is disabled + +The returned object may contain: - `label` (string) - the label displayed inside the bar. If it's not defined, name of task will be used #### Left Sidebar List Configuration diff --git a/src/bar.js b/src/bar.js index 37b44e56c..124e9c913 100644 --- a/src/bar.js +++ b/src/bar.js @@ -71,9 +71,14 @@ export default class Bar { }; if (typeof this.gantt.options.custom_config_bar === 'function') { + // TODO: if task_group is not found, should this fail? + const task_group = this.gantt.get_task_group_for_task(this.task); return { ...default_config, - ...this.gantt.options.custom_config_bar({ task: this.task }), + ...this.gantt.options.custom_config_bar({ + task: this.task, + task_group, + }), }; } From 55902e1d2ea1477de6e41047b1404a40a7337e9e Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 19 Jan 2025 01:06:15 +0000 Subject: [PATCH 18/35] set bar label text properly to generate contrast automatically --- src/bar.js | 17 +++++++++- src/color_utils.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/color_utils.js diff --git a/src/bar.js b/src/bar.js index 124e9c913..ffae19b69 100644 --- a/src/bar.js +++ b/src/bar.js @@ -1,3 +1,4 @@ +import color_utils from './color_utils'; import date_utils from './date_utils'; import { $, createSVG, animateSVG } from './svg_utils'; @@ -243,13 +244,27 @@ export default class Bar { x_coord = this.x + this.image_size + 5; } - createSVG('text', { + const labelEl = createSVG('text', { x: x_coord, y: this.y + this.height / 2, innerHTML: this.label, class: 'bar-label', append_to: this.bar_group, }); + + if (this.task.color) { + try { + labelEl.style.fill = color_utils.getTextColorForBackground( + this.task.color, + ); + } catch (err) { + // do not fail if the text color was invalid + // maybe the passed was a name like 'red' + // and this feature only supports HEX, HSL and RGB + console.warn(err); + } + } + // labels get BBox in the next tick requestAnimationFrame(() => this.update_label_position()); } diff --git a/src/color_utils.js b/src/color_utils.js new file mode 100644 index 000000000..273dea49c --- /dev/null +++ b/src/color_utils.js @@ -0,0 +1,85 @@ +function parseColorToRGB(color) { + color = color.trim(); + + // HEX (#RRGGBB or #RGB) + if (color.startsWith('#')) { + let hex = color.slice(1); + if (hex.length === 3) { + hex = hex + .split('') + .map((c) => c + c) + .join(''); + } + if (hex.length === 6) { + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return [r, g, b]; + } + } + + // RGB or RGBA (e.g., rgb(255, 87, 51) or rgba(255, 87, 51, 0.8)) + const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + return [ + parseInt(rgbMatch[1]), + parseInt(rgbMatch[2]), + parseInt(rgbMatch[3]), + ]; + } + + // HSL or HSLA (e.g., hsl(15, 100%, 50%) or hsla(15, 100%, 50%, 0.8)) + const hslMatch = color.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%/); + if (hslMatch) { + const h = parseInt(hslMatch[1]); + const s = parseInt(hslMatch[2]) / 100; + const l = parseInt(hslMatch[3]) / 100; + + // Convert HSL to RGB + const a = s * Math.min(l, 1 - l); + const f = (n) => { + const k = (n + h / 30) % 12; + return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); + }; + return [ + Math.round(f(0) * 255), + Math.round(f(8) * 255), + Math.round(f(4) * 255), + ]; + } + + // Invalid format + return null; +} + +export default { + getTextColorForBackground(color) { + // Convert any color format to RGB + const rgb = parseColorToRGB(color); + if (!rgb) { + throw new Error(`Invalid color format: ${color}`); + } + + // Normalize RGB values to [0, 1] + const [rNorm, gNorm, bNorm] = rgb.map((c) => c / 255); + + // Calculate luminance + const rL = + rNorm <= 0.03928 + ? rNorm / 12.92 + : Math.pow((rNorm + 0.055) / 1.055, 2.4); + const gL = + gNorm <= 0.03928 + ? gNorm / 12.92 + : Math.pow((gNorm + 0.055) / 1.055, 2.4); + const bL = + bNorm <= 0.03928 + ? bNorm / 12.92 + : Math.pow((bNorm + 0.055) / 1.055, 2.4); + + const luminance = 0.2126 * rL + 0.7152 * gL + 0.0722 * bL; + + // Choose black or white text based on luminance + return luminance > 0.5 ? '#000000' : '#FFFFFF'; + }, +}; From 98a2e1350f7796ce768e13f7c3e20a884c466d1e Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Tue, 21 Jan 2025 23:39:10 +0000 Subject: [PATCH 19/35] create annotations API for tasks --- README.md | 8 ++++ src/bar.js | 97 ++++++++++++++++++++++++++++++++++++++++++-- src/popup.js | 7 +--- src/styles/gantt.css | 4 +- 4 files changed, 105 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6cd7ab4b5..7d1d0a60c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ let tasks = [ end: '2016-12-31', progress: 20, task_group_id: 'task-group-1', + annotations: { + 'red': [ + { + date: '2016-12-30', + name: 'Mom\'s birthday' + }, + ] + } }, ... ] diff --git a/src/bar.js b/src/bar.js index ffae19b69..315224b5d 100644 --- a/src/bar.js +++ b/src/bar.js @@ -115,6 +115,7 @@ export default class Bar { draw() { this.draw_bar(); this.draw_progress_bar(); + this.draw_all_annotations(); if (this.gantt.options.show_expected_progress) { this.prepare_expected_progress_values(); this.draw_expected_progress_bar(); @@ -206,6 +207,90 @@ export default class Bar { animateSVG(this.$bar_progress, 'width', 0, this.progress_width); } + draw_all_annotations() { + if (!this.task.annotations) { + return; + } + + for (let color in this.task.annotations) { + const annotations = this.task.annotations[color]; + for (let index = 0; index < annotations.length; index++) { + const bar_annotation = this.draw_annotation( + color, + annotations[index], + ); + this.setup_annotation_popup(annotations[index], bar_annotation); + } + } + } + + draw_annotation(color, annotation) { + const x = + (date_utils.diff( + date_utils.parse(annotation.date), + this.gantt.gantt_start, + this.gantt.config.unit, + ) / + this.gantt.config.step) * + this.gantt.config.column_width; + + const bar_annotation = createSVG('rect', { + // TODO: so far, 4 pixels need to be substracted + // from the x position to match properly in the UI. + // Is there a way to improve this? + x: x - 4, + y: this.y, + width: + this.gantt.config.column_width / + date_utils.convert_scales( + this.gantt.config.view_mode.step, + 'day', + ), + height: this.height, + rx: this.corner_radius + 2, + ry: this.corner_radius + 2, + class: 'bar-progress', + append_to: this.bar_group, + }); + bar_annotation.style.fill = color; + + return bar_annotation; + } + + setup_annotation_popup(annotation, bar_annotation) { + let timeout; + + $.on(bar_annotation, 'mouseenter', (e) => { + timeout = setTimeout(() => { + this.gantt.show_popup({ + x: e.clientX, + y: e.clientY, + // TODO: creating and passing a task object + // from annotations since current popup implementation + // depends entirely on tasks and now we have multiple + // components like sidebar items or annotations. + task: { + name: annotation.name, + _start: date_utils.parse(annotation.date), + _end: date_utils.add( + date_utils.parse(annotation.date), + 1, + 'day', + ), + actual_duration: 1, + progress: this.task.progress, + }, + target: bar_annotation, + }); + }, 200); + }); + + $.on(bar_annotation, 'mouseleave', () => { + clearTimeout(timeout); + this.gantt.popup?.hide?.(); + }); + } + calculate_progress_width() { const width = this.$bar.getWidth(); const ignored_end = this.x + width; @@ -387,9 +472,10 @@ export default class Bar { if (this.gantt.bar_being_dragged) return; } this.gantt.show_popup({ - x: e.offsetX || e.layerX, - y: e.offsetY || e.layerY, + x: e.clientX, + y: e.clientY, task: this.task, + task_group: this.gantt.get_task_group_for_task(this.task), target: this.$bar, }); }); @@ -399,9 +485,12 @@ export default class Bar { timeout = setTimeout(() => { if (this.gantt.options.popup_on === 'hover') this.gantt.show_popup({ - x: e.offsetX || e.layerX, - y: e.offsetY || e.layerY, + x: e.clientX, + y: e.clientY, task: this.task, + task_group: this.gantt.get_task_group_for_task( + this.task, + ), target: this.$bar, }); this.gantt.$container diff --git a/src/popup.js b/src/popup.js index d6dd63edf..fa766f289 100644 --- a/src/popup.js +++ b/src/popup.js @@ -22,13 +22,10 @@ export default class Popup { this.actions = this.parent.querySelector('.actions'); } - show({ x, y, task, target }) { + show({ x, y, target, ...data }) { this.actions.innerHTML = ''; - // TODO: if task_group is not found, should this fail? - const task_group = this.gantt.get_task_group_for_task(task); let html = this.popup_func({ - task, - task_group, + ...data, chart: this.gantt, get_title: () => this.title, set_title: (title) => (this.title.innerHTML = title), diff --git a/src/styles/gantt.css b/src/styles/gantt.css index aacb29c4d..55000aeeb 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -16,7 +16,7 @@ border-radius: 8px; & .popup-wrapper { - position: absolute; + position: fixed; top: 0; left: 0; background: #fff; @@ -24,7 +24,7 @@ padding: 10px; border-radius: 5px; width: max-content; - z-index: 1000; + z-index: 1001; & .title { margin-bottom: 2px; From a135d6d7eb0189b1f84cb66fa29fbcac286fef6c Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Wed, 22 Jan 2025 14:16:08 +0000 Subject: [PATCH 20/35] pass type to popup options --- README.md | 1 + src/bar.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 7d1d0a60c..7407068b2 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ For details, see the above table. - a HTML string, the popup will be that string. The function receives one object as an argument, containing: +- `type` - a string defining the type of component related: `annotation` or `task` - `task` - the task as an object - `task_group` - the related task group as an object. **NOTE:** it is `undefined` if task groups feature is disabled - `chart` - the entire Gantt chart diff --git a/src/bar.js b/src/bar.js index 315224b5d..217678cab 100644 --- a/src/bar.js +++ b/src/bar.js @@ -265,6 +265,7 @@ export default class Bar { this.gantt.show_popup({ x: e.clientX, y: e.clientY, + type: 'annotation', // TODO: creating and passing a task object // from annotations since current popup implementation // depends entirely on tasks and now we have multiple @@ -474,6 +475,7 @@ export default class Bar { this.gantt.show_popup({ x: e.clientX, y: e.clientY, + type: 'task', task: this.task, task_group: this.gantt.get_task_group_for_task(this.task), target: this.$bar, @@ -487,6 +489,7 @@ export default class Bar { this.gantt.show_popup({ x: e.clientX, y: e.clientY, + type: 'task', task: this.task, task_group: this.gantt.get_task_group_for_task( this.task, From 2c6c17facb577981b58b4ded2b9f12537f5596f7 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 25 Jan 2025 00:51:50 +0000 Subject: [PATCH 21/35] initial approach --- src/index.js | 81 +++++++++++++++++++++++--------------------- src/styles/gantt.css | 18 +++------- 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/src/index.js b/src/index.js index a9a8fa36e..59ac384ec 100644 --- a/src/index.js +++ b/src/index.js @@ -389,7 +389,6 @@ export default class Gantt { bind_events() { this.bind_grid_click(); - this.bind_holiday_labels(); this.bind_bar_events(); } @@ -703,18 +702,8 @@ export default class Gantt { this.config.step) * this.config.column_width; const height = this.grid_height - this.config.header_height; - const d_formatted = date_utils - .format(d, 'YYYY-MM-DD', this.options.language) - .replace(' ', '_'); - - if (labels[d]) { - let label = this.create_el({ - classes: 'holiday-label ' + 'label_' + d_formatted, - append_to: this.$extras, - }); - label.textContent = labels[d]; - } - createSVG('rect', { + + const bar_holiday = createSVG('rect', { x: Math.round(x), y: this.config.header_height, width: @@ -724,15 +713,55 @@ export default class Gantt { 'day', ), height, - class: 'holiday-highlight ' + d_formatted, + class: 'holiday-highlight', style: `fill: ${color};`, append_to: this.layers.grid, }); + + if (labels[d]) { + this.setup_holiday_popup(labels[d], bar_holiday); + } } } } } + setup_holiday_popup(label, bar_holiday) { + let timeout; + + $.on(bar_holiday, 'mouseenter', (e) => { + timeout = setTimeout(() => { + console.log('hello') + this.show_popup({ + x: e.clientX, + y: e.clientY, + type: 'holiday', + // TODO: creating and passing a task object + // from annotations since current popup implementation + // depends entirely on tasks and now we have multiple + // components like sidebar items or annotations. + task: { + name: label, + _start: date_utils.parse('2025-01-10'), + _end: date_utils.add( + date_utils.parse('2025-01-10'), + 1, + 'day', + ), + actual_duration: 1, + progress: 1, + }, + target: bar_holiday, + }); + }, 200); + }); + + $.on(bar_holiday, 'mouseenter', () => { + clearTimeout(timeout); + this.popup?.hide?.(); + }); + } + /** * Compute the horizontal x-axis distance and associated date for the current date and view. * @@ -1145,30 +1174,6 @@ export default class Gantt { ); } - bind_holiday_labels() { - const $highlights = - this.$container.querySelectorAll('.holiday-highlight'); - for (let h of $highlights) { - const label = this.$container.querySelector( - '.label_' + h.classList[1], - ); - if (!label) continue; - let timeout; - h.onmouseenter = (e) => { - timeout = setTimeout(() => { - label.classList.add('show'); - label.style.left = (e.offsetX || e.layerX) + 'px'; - label.style.top = (e.offsetY || e.layerY) + 'px'; - }, 300); - }; - - h.onmouseleave = (e) => { - clearTimeout(timeout); - label.classList.remove('show'); - }; - } - } - get_start_end_positions() { if (!this.bars.length) return [0, 0, 0]; let { x, width } = this.bars[0].group.getBBox(); diff --git a/src/styles/gantt.css b/src/styles/gantt.css index 55000aeeb..e39c99b77 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -202,23 +202,13 @@ border-radius: 5px; } - & .holiday-label { - position: absolute; - top: 0; - left: 0; - opacity: 0; - z-index: 1000; - background: --g-weekend-label-color; - border-radius: 5px; - padding: 2px 5px; - - &.show { - opacity: 100; - } + & .holiday-highlight { + cursor: pointer; } & .extras { - position: sticky; + position: fixed; + top: 0px; left: 0px; & .adjust { From 41b065df88e053b9f4ae1b2708c824c09b216293 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 25 Jan 2025 15:15:20 +0000 Subject: [PATCH 22/35] fix popups for holidays --- src/index.js | 72 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/index.js b/src/index.js index 59ac384ec..abd358c48 100644 --- a/src/index.js +++ b/src/index.js @@ -716,6 +716,7 @@ export default class Gantt { class: 'holiday-highlight', style: `fill: ${color};`, append_to: this.layers.grid, + date: date_utils.format(d, 'YYYY-MM-DD', this.options.language), }); if (labels[d]) { @@ -729,37 +730,46 @@ export default class Gantt { setup_holiday_popup(label, bar_holiday) { let timeout; - $.on(bar_holiday, 'mouseenter', (e) => { - timeout = setTimeout(() => { - console.log('hello') - this.show_popup({ - x: e.clientX, - y: e.clientY, - type: 'holiday', - // TODO: creating and passing a task object - // from annotations since current popup implementation - // depends entirely on tasks and now we have multiple - // components like sidebar items or annotations. - task: { - name: label, - _start: date_utils.parse('2025-01-10'), - _end: date_utils.add( - date_utils.parse('2025-01-10'), - 1, - 'day', - ), - actual_duration: 1, - progress: 1, - }, - target: bar_holiday, - }); - }, 200); - }); + $.on( + this.$container, + 'mouseover', + '.holiday-highlight', + (e) => { + timeout = setTimeout(() => { + this.show_popup({ + x: e.clientX, + y: e.clientY, + type: 'holiday', + // TODO: creating and passing a task object + // from holidays since current popup implementation + // depends entirely on tasks and now we have multiple + // components like sidebar items or annotations. + task: { + name: label, + _start: date_utils.parse(e.target.getAttribute('date')), + _end: date_utils.add( + date_utils.parse(e.target.getAttribute('date')), + 1, + 'day', + ), + actual_duration: 1, + progress: 1, + }, + target: bar_holiday, + }); + }, 200); + }, + ); - $.on(bar_holiday, 'mouseenter', () => { - clearTimeout(timeout); - this.popup?.hide?.(); - }); + $.on( + this.$container, + 'mouseout', + '.holiday-highlight', + () => { + clearTimeout(timeout); + this.popup?.hide?.(); + }, + ); } /** @@ -1166,7 +1176,7 @@ export default class Gantt { $.on( this.$container, 'click', - '.grid-row, .grid-header, .ignored-bar, .holiday-highlight', + '.grid-row, .grid-header, .ignored-bar', () => { this.unselect_all(); this.hide_popup(); From 78789565e525c8cfee032ba963f83fd5771f0e41 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 25 Jan 2025 15:18:15 +0000 Subject: [PATCH 23/35] fix prettier issues --- src/index.js | 76 ++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/index.js b/src/index.js index abd358c48..6ba1910fa 100644 --- a/src/index.js +++ b/src/index.js @@ -702,7 +702,7 @@ export default class Gantt { this.config.step) * this.config.column_width; const height = this.grid_height - this.config.header_height; - + const bar_holiday = createSVG('rect', { x: Math.round(x), y: this.config.header_height, @@ -716,7 +716,11 @@ export default class Gantt { class: 'holiday-highlight', style: `fill: ${color};`, append_to: this.layers.grid, - date: date_utils.format(d, 'YYYY-MM-DD', this.options.language), + date: date_utils.format( + d, + 'YYYY-MM-DD', + this.options.language, + ), }); if (labels[d]) { @@ -730,46 +734,36 @@ export default class Gantt { setup_holiday_popup(label, bar_holiday) { let timeout; - $.on( - this.$container, - 'mouseover', - '.holiday-highlight', - (e) => { - timeout = setTimeout(() => { - this.show_popup({ - x: e.clientX, - y: e.clientY, - type: 'holiday', - // TODO: creating and passing a task object - // from holidays since current popup implementation - // depends entirely on tasks and now we have multiple - // components like sidebar items or annotations. - task: { - name: label, - _start: date_utils.parse(e.target.getAttribute('date')), - _end: date_utils.add( - date_utils.parse(e.target.getAttribute('date')), - 1, - 'day', - ), - actual_duration: 1, - progress: 1, - }, - target: bar_holiday, - }); - }, 200); - }, - ); + $.on(this.$container, 'mouseover', '.holiday-highlight', (e) => { + timeout = setTimeout(() => { + this.show_popup({ + x: e.clientX, + y: e.clientY, + type: 'holiday', + // TODO: creating and passing a task object + // from holidays since current popup implementation + // depends entirely on tasks and now we have multiple + // components like sidebar items or annotations. + task: { + name: label, + _start: date_utils.parse(e.target.getAttribute('date')), + _end: date_utils.add( + date_utils.parse(e.target.getAttribute('date')), + 1, + 'day', + ), + actual_duration: 1, + progress: 1, + }, + target: bar_holiday, + }); + }, 200); + }); - $.on( - this.$container, - 'mouseout', - '.holiday-highlight', - () => { - clearTimeout(timeout); - this.popup?.hide?.(); - }, - ); + $.on(this.$container, 'mouseout', '.holiday-highlight', () => { + clearTimeout(timeout); + this.popup?.hide?.(); + }); } /** From 5a5b49bcdf734ac6472c7292c5475dbef09d1305 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 25 Jan 2025 15:34:19 +0000 Subject: [PATCH 24/35] fix bug on recent fix for popups. Only apply for holidays --- src/index.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 6ba1910fa..f88dfecac 100644 --- a/src/index.js +++ b/src/index.js @@ -713,29 +713,35 @@ export default class Gantt { 'day', ), height, - class: 'holiday-highlight', style: `fill: ${color};`, append_to: this.layers.grid, - date: date_utils.format( + }); + + if (d && labels[d]) { + // this means is a holiday + bar_holiday.setAttribute('label', labels[d]) + bar_holiday.setAttribute('date', date_utils.format( d, 'YYYY-MM-DD', this.options.language, - ), - }); - - if (labels[d]) { - this.setup_holiday_popup(labels[d], bar_holiday); + )) + bar_holiday.classList.add('holiday-highlight'); } } } + + this.setup_holiday_popup(); } } - setup_holiday_popup(label, bar_holiday) { + setup_holiday_popup() { let timeout; $.on(this.$container, 'mouseover', '.holiday-highlight', (e) => { timeout = setTimeout(() => { + const date = e.target.getAttribute('date'); + const label = e.target.getAttribute('label'); + this.show_popup({ x: e.clientX, y: e.clientY, @@ -755,7 +761,7 @@ export default class Gantt { actual_duration: 1, progress: 1, }, - target: bar_holiday, + target: e.target, }); }, 200); }); From 0664b376ecd2c520f7a93c89baa2bb644cbe999c Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sat, 25 Jan 2025 15:36:12 +0000 Subject: [PATCH 25/35] fix prettier issues --- src/index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index f88dfecac..9fb44fd17 100644 --- a/src/index.js +++ b/src/index.js @@ -719,12 +719,15 @@ export default class Gantt { if (d && labels[d]) { // this means is a holiday - bar_holiday.setAttribute('label', labels[d]) - bar_holiday.setAttribute('date', date_utils.format( - d, - 'YYYY-MM-DD', - this.options.language, - )) + bar_holiday.setAttribute('label', labels[d]); + bar_holiday.setAttribute( + 'date', + date_utils.format( + d, + 'YYYY-MM-DD', + this.options.language, + ), + ); bar_holiday.classList.add('holiday-highlight'); } } From 667b0c673a48be81db402ae45b8edcaa32efb1b0 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Mon, 27 Jan 2025 14:41:55 +0000 Subject: [PATCH 26/35] config display label bars --- src/bar.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/bar.js b/src/bar.js index 217678cab..a60eac4a9 100644 --- a/src/bar.js +++ b/src/bar.js @@ -49,8 +49,9 @@ export default class Bar { } prepare_values() { - const { label } = this.get_config(); + const { label, show_label_on_offset } = this.get_config(); this.label = label; + this.show_label_on_offset = show_label_on_offset; this.invalid = this.task.invalid; this.height = this.gantt.options.bar_height; @@ -69,6 +70,7 @@ export default class Bar { get_config() { const default_config = { label: this.task.name, + show_label_on_offset: true, }; if (typeof this.gantt.options.custom_config_bar === 'function') { @@ -798,13 +800,17 @@ export default class Bar { const labelWidth = label.getBBox().width; const barWidth = bar.getWidth(); if (labelWidth > barWidth) { - label.classList.add('big'); - if (img) { - img.setAttribute('x', bar.getEndX() + padding); - img_mask.setAttribute('x', bar.getEndX() + padding); - label.setAttribute('x', bar.getEndX() + x_offset_label_img); + if (this.show_label_on_offset) { + label.classList.add('big'); + if (img) { + img.setAttribute('x', bar.getEndX() + padding); + img_mask.setAttribute('x', bar.getEndX() + padding); + label.setAttribute('x', bar.getEndX() + x_offset_label_img); + } else { + label.setAttribute('x', bar.getEndX() + padding); + } } else { - label.setAttribute('x', bar.getEndX() + padding); + label.style.display = "none"; } } else { label.classList.remove('big'); From ed02a4afbd7678ab96d12aa10494a4896952dd4d Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Mon, 27 Jan 2025 15:07:34 +0000 Subject: [PATCH 27/35] automatic: preparing to publish --- src/bar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bar.js b/src/bar.js index a60eac4a9..c762c8b1d 100644 --- a/src/bar.js +++ b/src/bar.js @@ -810,7 +810,7 @@ export default class Bar { label.setAttribute('x', bar.getEndX() + padding); } } else { - label.style.display = "none"; + label.style.display = 'none'; } } else { label.classList.remove('big'); From 21632421ef1e5b999fd7d796f75283a7177945bf Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Mon, 27 Jan 2025 22:43:47 +0000 Subject: [PATCH 28/35] clickable sidebar list items --- src/index.js | 21 +++++++++++++++++++++ src/styles/gantt.css | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/src/index.js b/src/index.js index 9fb44fd17..a36d9fd4c 100644 --- a/src/index.js +++ b/src/index.js @@ -995,6 +995,25 @@ export default class Gantt { row.style.height = this.options.bar_height + this.options.padding + 'px'; + + if ( + typeof this.options.left_sidebar_list_config.on_click === + 'function' + ) { + row.classList.add('clickable'); + + $.on(row, 'click', () => { + const payload = this.options.task_groups_enabled + ? { + task_group: item, + } + : { + task: item, + }; + + this.options.left_sidebar_list_config.on_click(payload); + }); + } }); // add empty row for cover little empty row from grid @@ -1696,6 +1715,8 @@ export default class Gantt { this.$side_header?.remove?.(); this.$current_highlight?.remove?.(); this.$extras?.remove?.(); + this.$left_sidebar_list_fixer_container?.remove?.(); + this.$left_sidebar_list_container?.remove?.(); this.popup?.hide?.(); } } diff --git a/src/styles/gantt.css b/src/styles/gantt.css index e39c99b77..cb8040615 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -354,6 +354,14 @@ font-weight: 400; justify-content: center; text-align: left; + + &.clickable { + cursor: pointer; + + &:hover { + background: var(--g-weekend-highlight-color); + } + } } & .left-sidebar-list-row:not(:last-child) { From d763724cbc4fcaafd35c33cad19226733e6d9ce0 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Wed, 29 Jan 2025 13:36:00 +0000 Subject: [PATCH 29/35] set base z index --- src/defaults.js | 1 + src/index.js | 9 +++++++++ src/styles/gantt.css | 6 ------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/defaults.js b/src/defaults.js index 7749c3c2e..7c58a64a6 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -112,6 +112,7 @@ const DEFAULT_OPTIONS = { auto_move_label: false, bar_corner_radius: 3, bar_height: 30, + base_z_index: 1000, container_height: 'auto', column_width: null, custom_config_bar: null, diff --git a/src/index.js b/src/index.js index a36d9fd4c..21cab48c4 100644 --- a/src/index.js +++ b/src/index.js @@ -128,6 +128,8 @@ export default class Gantt { this.options.task_groups_enabled = Array.isArray(this.options.task_groups) && this.options.task_groups.length > 0; + + this.$popup_wrapper.style.zIndex = this.options.base_z_index + 2; } update_options(options) { @@ -504,6 +506,7 @@ export default class Gantt { classes: 'grid-header', append_to: this.$container, }); + this.$header.style.zIndex = this.options.base_z_index + 1; this.$upper_header = this.create_el({ classes: 'upper-header', @@ -531,6 +534,8 @@ export default class Gantt { classes: 'left-sidebar-list', append_to: this.$main_wrapper, }); + this.$left_sidebar_list_container.style.zIndex = + this.options.base_z_index + 2; this.$left_sidebar_list_fixer_container.style.flexBasis = this.options.left_sidebar_list_config.width + 'px'; @@ -538,6 +543,7 @@ export default class Gantt { make_side_header() { this.$side_header = this.create_el({ classes: 'side-header' }); + this.$side_header.style.zIndex = this.options.base_z_index + 1; this.$upper_header.prepend(this.$side_header); // Create view mode change select @@ -803,6 +809,7 @@ export default class Gantt { classes: 'current-highlight', append_to: this.$container, }); + this.$current_highlight.style.zIndex = this.options.base_z_index; this.$current_ball_highlight = this.create_el({ top: this.config.header_height - 6, left: left - 2.5, @@ -811,6 +818,8 @@ export default class Gantt { classes: 'current-ball-highlight', append_to: this.$header, }); + this.$current_ball_highlight.style.zIndex = + this.options.base_z_index + 2; } make_grid_highlights() { diff --git a/src/styles/gantt.css b/src/styles/gantt.css index cb8040615..ac6ca2d1f 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -24,7 +24,6 @@ padding: 10px; border-radius: 5px; width: max-content; - z-index: 1001; & .title { margin-bottom: 2px; @@ -81,7 +80,6 @@ position: sticky; top: 0; left: 0; - z-index: 1000; } & .lower-text, @@ -130,7 +128,6 @@ right: 0; float: right; - z-index: 1000; line-height: 20px; font-weight: 400; width: max-content; @@ -186,13 +183,11 @@ position: absolute; background: var(--g-today-highlight); width: 1px; - z-index: 999; } & .current-ball-highlight { position: absolute; background: var(--g-today-highlight); - z-index: 1001; border-radius: 50%; } @@ -342,7 +337,6 @@ box-shadow: 5px 0px 5px -5px rgba(0, 0, 0, 0.3); overflow-y: auto; position: absolute; - z-index: 1001; & .left-sidebar-list-row { align-items: center; From 5ffa89c4a7d374d09ce75e1366c3586d0e6555ff Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Thu, 30 Jan 2025 22:47:52 +0000 Subject: [PATCH 30/35] add on click for bars --- src/bar.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bar.js b/src/bar.js index c762c8b1d..2ab15d50a 100644 --- a/src/bar.js +++ b/src/bar.js @@ -49,9 +49,10 @@ export default class Bar { } prepare_values() { - const { label, show_label_on_offset } = this.get_config(); + const { label, show_label_on_offset, on_click } = this.get_config(); this.label = label; this.show_label_on_offset = show_label_on_offset; + this.on_click = on_click; this.invalid = this.task.invalid; this.height = this.gantt.options.bar_height; @@ -71,6 +72,7 @@ export default class Bar { const default_config = { label: this.task.name, show_label_on_offset: true, + on_click: null, }; if (typeof this.gantt.options.custom_config_bar === 'function') { @@ -456,6 +458,12 @@ export default class Bar { } setup_click_event() { + if (this.on_click) { + $.on(this.$bar, 'click', () => { + this.on_click(); + }); + } + let task_id = this.task.id; $.on(this.group, 'mouseover', (e) => { this.gantt.trigger_event('hover', [ From 2227b848018e317e110585ecfc8fcd5b9ff630d9 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Thu, 30 Jan 2025 23:27:55 +0000 Subject: [PATCH 31/35] improve bar config object --- src/bar.js | 50 +++++++++++++++++++------------------------------ src/defaults.js | 4 +++- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/bar.js b/src/bar.js index 2ab15d50a..0531586e6 100644 --- a/src/bar.js +++ b/src/bar.js @@ -49,11 +49,6 @@ export default class Bar { } prepare_values() { - const { label, show_label_on_offset, on_click } = this.get_config(); - this.label = label; - this.show_label_on_offset = show_label_on_offset; - this.on_click = on_click; - this.invalid = this.task.invalid; this.height = this.gantt.options.bar_height; this.image_size = this.height - 5; @@ -68,28 +63,6 @@ export default class Bar { if (this.task.progress > 100) this.task.progress = 100; } - get_config() { - const default_config = { - label: this.task.name, - show_label_on_offset: true, - on_click: null, - }; - - if (typeof this.gantt.options.custom_config_bar === 'function') { - // TODO: if task_group is not found, should this fail? - const task_group = this.gantt.get_task_group_for_task(this.task); - return { - ...default_config, - ...this.gantt.options.custom_config_bar({ - task: this.task, - task_group, - }), - }; - } - - return default_config; - } - prepare_helpers() { SVGElement.prototype.getX = function () { return +this.getAttribute('x'); @@ -334,10 +307,19 @@ export default class Bar { x_coord = this.x + this.image_size + 5; } + let label = this.task.name; + if (typeof this.gantt.options.bar_config.get_label === 'function') { + const task_group = this.gantt.get_task_group_for_task(this.task); + label = this.gantt.options.bar_config.get_label({ + task: this.task, + task_group, + }); + } + const labelEl = createSVG('text', { x: x_coord, y: this.y + this.height / 2, - innerHTML: this.label, + innerHTML: label, class: 'bar-label', append_to: this.bar_group, }); @@ -458,9 +440,15 @@ export default class Bar { } setup_click_event() { - if (this.on_click) { + if (this.gantt.options.bar_config.on_click) { $.on(this.$bar, 'click', () => { - this.on_click(); + const task_group = this.gantt.get_task_group_for_task( + this.task, + ); + this.gantt.options.bar_config.on_click({ + task: this.task, + task_group, + }); }); } @@ -808,7 +796,7 @@ export default class Bar { const labelWidth = label.getBBox().width; const barWidth = bar.getWidth(); if (labelWidth > barWidth) { - if (this.show_label_on_offset) { + if (this.gantt.options.bar_config.show_label_on_offset) { label.classList.add('big'); if (img) { img.setAttribute('x', bar.getEndX() + padding); diff --git a/src/defaults.js b/src/defaults.js index 7c58a64a6..757b6da53 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -112,10 +112,12 @@ const DEFAULT_OPTIONS = { auto_move_label: false, bar_corner_radius: 3, bar_height: 30, + bar_config: { + show_label_on_offset: true, + }, base_z_index: 1000, container_height: 'auto', column_width: null, - custom_config_bar: null, date_format: 'YYYY-MM-DD HH:mm', enable_left_sidebar_list: false, holidays: { 'var(--g-weekend-highlight-color)': 'weekend' }, From eb89e92fc9d4850d95f11dae79e65d5793c5e38b Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 2 Feb 2025 15:38:05 +0000 Subject: [PATCH 32/35] change task groups api and set refresh feature. Update readme --- README.md | 32 +++++++++++++++----------------- src/defaults.js | 1 - src/index.js | 27 +++++++++++++++++---------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7407068b2..a8afcda93 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,15 @@ let tasks = [ } }, ... +]; +const task_groups = [ + { + id: 'task-group-1', + name: 'Task Group 1', + }, + ... ] -let gantt = new Gantt("#gantt", tasks); +let gantt = new Gantt("#gantt", tasks, {}, task_groups); ``` ### Configuration @@ -103,7 +110,7 @@ Frappe Gantt offers a wide range of options to customize your chart. | `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` | | `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` | -Apart from these ones, four options - `custom_config_bar`, `left_sidebar_list_config`, `popup`, `task_groups` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. +Apart from these ones, four options - `bar_config`, `left_sidebar_list_config`, `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. #### View Mode Configuration The `view_modes` option determines all the available view modes for the chart. It should be an array of objects. @@ -138,28 +145,19 @@ The function receives one object as an argument, containing: - `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section - `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed. -#### Custom Config Bar -`custom_config_bar` is a function that should return an object to configure bars. Options from this object: - -The function receives one object as an argument, containing: -- `task` - the task as an object -- `task_group` - the related task group as an object. **NOTE:** it is `undefined` if task groups feature is disabled +#### Bar config +`bar_config` is an object to configure bars. Options from this object: The returned object may contain: -- `label` (string) - the label displayed inside the bar. If it's not defined, name of task will be used +- `get_label` (function) - the label displayed inside the bar. If it's not defined, name of task will be used. This function receives an object as an argument containing `task` and `task_group` if defined. +- `on_click` (function) - the function that will be triggered when a bar is clicked. This function receives an object as an argument containing `task` and `task_group` if defined. +- `show_label_on_offset` (boolean) - When the bar label has more width that the bar, label is relocated with an offset to the right to improve UI. This boolean defines if the label should be displayed in that scenario. #### Left Sidebar List Configuration `left_sidebar_list_config` is an object. +- `on_click` (function) - the function that will be triggered when an item from the list is clicked. This function receives an object as an argument containing `task` and `task_group` if defined. - `width`, the width of the sidebar (in pixels) -#### Task Groups Configuration -`task_groups` is an array of objects representing groupings or categories for tasks. By passing it, bars will be gather in the same row. It should be an array of objects. -**NOTE:** This feature **doesn't support** date overlaps for tasks within the same task group. - -Each object can have the following properties: -- `id` (string) - the id of the task group. -- `name` (string) - the name of the task group. - ### API Frappe Gantt exposes a few helpful methods for you to interact with the chart: diff --git a/src/defaults.js b/src/defaults.js index 757b6da53..b2a4e6507 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -163,7 +163,6 @@ const DEFAULT_OPTIONS = { scroll_to: 'today', show_expected_progress: false, snap_at: null, - task_groups: [], task_groups_enabled: false, today_button: true, upper_header_height: 45, diff --git a/src/index.js b/src/index.js index 21cab48c4..d457d11cd 100644 --- a/src/index.js +++ b/src/index.js @@ -10,9 +10,10 @@ import { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES } from './defaults'; import './styles/gantt.css'; export default class Gantt { - constructor(wrapper, tasks, options) { + constructor(wrapper, tasks, options, task_groups = []) { this.setup_wrapper(wrapper); this.setup_options(options); + this.setup_task_groups(task_groups); this.setup_tasks(tasks); this.change_view_mode(); this.bind_events(); @@ -125,10 +126,6 @@ export default class Gantt { this.options.scroll_to = 'start'; } - this.options.task_groups_enabled = - Array.isArray(this.options.task_groups) && - this.options.task_groups.length > 0; - this.$popup_wrapper.style.zIndex = this.options.base_z_index + 2; } @@ -137,6 +134,15 @@ export default class Gantt { this.change_view_mode(undefined, true); } + setup_task_groups(task_groups) { + if (!Array.isArray(task_groups)) { + throw new TypeError('task_groups must be an array when defined'); + } + + this.task_groups = task_groups; + this.options.task_groups_enabled = this.task_groups.length > 0; + } + setup_tasks(tasks) { this.tasks = tasks .map((task, i) => { @@ -237,7 +243,7 @@ export default class Gantt { }; } - const task_group_index = this.options.task_groups.findIndex( + const task_group_index = this.task_groups.findIndex( (task_group) => task_group.id === task.task_group_id, ); if (task_group_index < 0) { @@ -261,7 +267,8 @@ export default class Gantt { } } - refresh(tasks) { + refresh(tasks, task_groups = []) { + this.setup_task_groups(task_groups); this.setup_tasks(tasks); this.change_view_mode(); } @@ -446,7 +453,7 @@ export default class Gantt { make_grid_background() { const grid_width = this.dates.length * this.config.column_width; const sidebar_list_items = this.options.task_groups_enabled - ? this.options.task_groups + ? this.task_groups : this.tasks; const grid_height = Math.max( this.config.header_height + @@ -992,7 +999,7 @@ export default class Gantt { } const sidebar_list_items = this.options.task_groups_enabled - ? this.options.task_groups + ? this.task_groups : this.tasks; sidebar_list_items.forEach((item, index) => { @@ -1708,7 +1715,7 @@ export default class Gantt { } get_task_group_for_task(task) { - return this.options.task_groups.find( + return this.task_groups.find( (task_group) => task_group.id === task.task_group_id, ); } From 7dedf51e26fbece14f4d72e9433c1641861aeb2c Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Sun, 2 Feb 2025 22:02:42 +0000 Subject: [PATCH 33/35] fix bar click action and bar width --- src/bar.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/bar.js b/src/bar.js index 0531586e6..42ede9948 100644 --- a/src/bar.js +++ b/src/bar.js @@ -212,10 +212,7 @@ export default class Bar { this.gantt.config.column_width; const bar_annotation = createSVG('rect', { - // TODO: so far, 4 pixels need to be substracted - // from the x position to match properly in the UI. - // Is there a way to improve this? - x: x - 4, + x, y: this.y, width: this.gantt.config.column_width / @@ -440,18 +437,6 @@ export default class Bar { } setup_click_event() { - if (this.gantt.options.bar_config.on_click) { - $.on(this.$bar, 'click', () => { - const task_group = this.gantt.get_task_group_for_task( - this.task, - ); - this.gantt.options.bar_config.on_click({ - task: this.task, - task_group, - }); - }); - } - let task_id = this.task.id; $.on(this.group, 'mouseover', (e) => { this.gantt.trigger_event('hover', [ @@ -509,6 +494,15 @@ export default class Bar { }); $.on(this.group, 'click', () => { + if (this.gantt.options.bar_config.on_click) { + const task_group = this.gantt.get_task_group_for_task( + this.task, + ); + this.gantt.options.bar_config.on_click({ + task: this.task, + task_group, + }); + } this.gantt.trigger_event('click', [this.task]); }); @@ -685,7 +679,10 @@ export default class Bar { date_utils.diff(task_start, gantt_start, this.gantt.config.unit) / this.gantt.config.step; - let x = diff * column_width; + // TODO: so far, 4 pixels need to be substracted + // from the x position to match properly in the UI. + // Is there a way to improve this? + let x = diff * column_width + 4; /* Since the column width is based on 30, we count the month-difference, multiply it by 30 for a "pseudo-month" @@ -723,7 +720,10 @@ export default class Bar { duration_in_days = 0; for ( let d = new Date(this.task._start); - d < this.task._end; + // Possible hack: last update changed comparison from '<' to '<= + // This in order to make tasks to take final day as well. + // Even this is good to tasks that lasts one day only + d <= this.task._end; d.setDate(d.getDate() + 1) ) { duration_in_days++; From 4d5b9d7e9270e3016fe536eef58509acb6582829 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Thu, 6 Feb 2025 12:50:43 +0000 Subject: [PATCH 34/35] prevent reset scroll on refresh --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index d457d11cd..39edd1174 100644 --- a/src/index.js +++ b/src/index.js @@ -267,10 +267,10 @@ export default class Gantt { } } - refresh(tasks, task_groups = []) { + refresh(tasks, task_groups = [], reset_scroll = false) { this.setup_task_groups(task_groups); this.setup_tasks(tasks); - this.change_view_mode(); + this.change_view_mode(undefined, !reset_scroll); } update_task(id, new_details) { From f785d0cfe256cb98c945ae0ca7a74510429d1fb4 Mon Sep 17 00:00:00 2001 From: Marlon Gomez Date: Thu, 27 Feb 2025 16:44:45 +0000 Subject: [PATCH 35/35] change center style --- src/styles/gantt.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/gantt.css b/src/styles/gantt.css index ac6ca2d1f..f6cbd7ef1 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -347,7 +347,7 @@ font-size: 13px; font-weight: 400; justify-content: center; - text-align: left; + text-align: center; &.clickable { cursor: pointer;