From 350dc88785480b1af7569efb08237a1f01f8a5a4 Mon Sep 17 00:00:00 2001 From: Safwan Samsudeen Date: Tue, 17 Dec 2024 14:41:24 +0530 Subject: [PATCH] feat: replace tooltip with popup --- src/bar.js | 126 +++++++++++++++++++++---------------------- src/defaults.js | 43 ++++++++++++++- src/index.js | 16 +++--- src/popup.js | 72 ++++++++++++------------- src/styles/dark.css | 4 -- src/styles/gantt.css | 55 +++++++++++++------ src/styles/light.css | 1 - 7 files changed, 188 insertions(+), 129 deletions(-) diff --git a/src/bar.js b/src/bar.js index 87a262c..a3a6a5e 100644 --- a/src/bar.js +++ b/src/bar.js @@ -136,7 +136,6 @@ export default class Bar { draw_progress_bar() { if (this.invalid) return; this.progress_width = this.calculate_progress_width(); - this.$bar_progress = createSVG('rect', { x: this.x, y: this.y, @@ -157,7 +156,7 @@ export default class Bar { this.gantt.config.column_width; let $date_highlight = this.gantt.create_el({ - classes: `date-highlight highlight-${this.task.id}`, + classes: `date-highlight hide highlight-${this.task.id}`, width: this.width, left: x, }); @@ -172,11 +171,18 @@ export default class Bar { const ignored_end = this.x + width; const total_ignored_area = this.gantt.config.ignored_positions.reduce((acc, val) => { + if (this.task._index === 2) + console.log('IN', val >= this.x, val < ignored_end); return acc + (val >= this.x && val < ignored_end); }, 0) * this.gantt.config.column_width; let progress_width = ((width - total_ignored_area) * this.task.progress) / 100; - + console.log( + this.task, + this.gantt.config.ignored_positions.reduce((acc, val) => { + return acc + (val >= this.x && val < ignored_end); + }, 0), + ); const progress_end = this.x + progress_width; const total_ignored_progress = this.gantt.config.ignored_positions.reduce((acc, val) => { @@ -304,6 +310,22 @@ export default class Bar { this.setup_click_event(); } + toggle_popup(e) { + if ( + !this.gantt.popup || + this.gantt.popup.parent.classList.contains('hide') + ) { + this.gantt.show_popup({ + x: e.offsetX || e.layerX, + y: e.offsetY || e.layerY, + task: this.task, + target: this.$bar, + }); + } else { + this.gantt.popup.hide(); + } + } + setup_click_event() { let task_id = this.task.id; $.on(this.group, 'mouseover', (e) => { @@ -316,40 +338,38 @@ export default class Bar { }); if (this.gantt.options.popup_on === 'click') { - let opened = false; $.on(this.group, 'click', (e) => { - if (!opened) { - this.show_popup(e.offsetX || e.layerX); - this.gantt.$container.querySelector( - `.highlight-${task_id}`, - ).style.display = 'block'; - } else { - this.gantt.hide_popup(); - } - opened = !opened; + console.log('CLICKED'); + this.toggle_popup(e); + this.gantt.$container + .querySelector(`.highlight-${task_id}`) + .classList.toggle('hide'); }); } else { - let timeout; - $.on( - this.group, - 'mouseenter', - (e) => - (timeout = setTimeout(() => { - this.show_popup(e.offsetX || e.layerX); - this.gantt.$container.querySelector( - `.highlight-${task_id}`, - ).style.display = 'block'; - }, 200)), - ); - - $.on(this.group, 'mouseleave', () => { - clearTimeout(timeout); - this.gantt.popup?.hide?.(); - - this.gantt.$container.querySelector( - `.highlight-${task_id}`, - ).style.display = 'none'; - }); + // let timeout; + // $.on( + // this.group, + // 'mouseenter', + // (e) => + // (timeout = setTimeout(() => { + // this.gantt.show_popup({ + // x: e.offsetX || e.layerX, + // y: e.offsetY || e.layerY, + // task: this.task, + // target: this.$bar, + // }); + // this.gantt.$container.querySelector( + // `.highlight-${task_id}`, + // ).style.display = 'block'; + // }, 200)), + // ); + // $.on(this.group, 'mouseleave', () => { + // clearTimeout(timeout); + // this.gantt.popup?.hide?.(); + // this.gantt.$container.querySelector( + // `.highlight-${task_id}`, + // ).style.display = 'none'; + // }); } $.on(this.group, 'click', () => { @@ -363,36 +383,12 @@ export default class Bar { } this.group.classList.remove('active'); if (this.gantt.popup) - this.gantt.popup.parent.classList.remove('hidden'); + this.gantt.popup.parent.classList.remove('hide'); this.gantt.trigger_event('double_click', [this.task]); }); } - show_popup(x) { - if (this.gantt.bar_being_dragged) return; - - const start_date = date_utils.format( - this.task._start, - 'MMM D', - this.gantt.options.language, - ); - const end_date = date_utils.format( - date_utils.add(this.task._end, -1, 'second'), - 'MMM D', - this.gantt.options.language, - ); - - const subtitle = `${start_date} - ${end_date} (${this.actual_duration_in_days} days)
Progress: ${this.task.progress}`; - this.gantt.show_popup({ - x, - target_element: this.$bar, - title: this.task.name, - subtitle: subtitle, - task: this.task, - }); - } - update_bar_position({ x = null, width = null }) { const bar = this.$bar; @@ -541,7 +537,7 @@ export default class Bar { if (progress < 0) return 0; const total = this.$bar.getWidth() - - this.ignored_duration * this.gantt.config.column_width; + this.ignored_duration_raw * this.gantt.config.column_width; return parseInt((progress / total) * 100, 10); } @@ -617,7 +613,8 @@ export default class Bar { actual_duration_in_days++; } } - this.actual_duration_in_days = actual_duration_in_days; + this.task.actual_duration = actual_duration_in_days; + this.task.ignored_duration = duration_in_days - actual_duration_in_days; this.duration = date_utils.convert_scales( @@ -625,12 +622,13 @@ export default class Bar { this.gantt.config.unit, ) / this.gantt.config.step; - this.actual_duration = + this.actual_duration_raw = date_utils.convert_scales( actual_duration_in_days + 'd', this.gantt.config.unit, ) / this.gantt.config.step; - this.ignored_duration = this.duration - this.actual_duration; + + this.ignored_duration_raw = this.duration - this.actual_duration_raw; } update_attr(element, attr, value) { @@ -648,7 +646,7 @@ export default class Bar { this.$expected_bar_progress.setAttribute( 'width', this.gantt.config.column_width * - this.actual_duration * + this.actual_duration_raw * (this.expected_progress / 100) || 0, ); } diff --git a/src/defaults.js b/src/defaults.js index b4e6d23..c10dd43 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -122,8 +122,47 @@ const DEFAULT_OPTIONS = { lines: 'both', move_dependencies: true, padding: 18, - popup: null, - popup_on: 'hover', + popup: (ctx) => { + ctx.set_title(ctx.task.name); + if (ctx.task.description) ctx.set_subtitle(ctx.task.description); + else ctx.set_subtitle(''); + + const start_date = date_utils.format( + ctx.task._start, + 'MMM D', + ctx.chart.options.language, + ); + const end_date = date_utils.format( + date_utils.add(ctx.task._end, -1, 'second'), + 'MMM D', + ctx.chart.options.language, + ); + + ctx.set_details( + `${start_date} - ${end_date} (${ctx.task.actual_duration} days${ctx.task.ignored_duration ? ' + ' + ctx.task.ignored_duration + ' excluded' : ''})
Progress: ${ctx.task.progress}%`, + ); + + ctx.add_action('Toggle Priority', (task, chart) => { + task.important = !task.important; + chart.refresh( + chart.tasks.map((t) => (t.id !== task.id ? t : task)), + ); + }); + + ctx.add_action('+', (task, chart) => { + task.progress += (1 / task.actual_duration) * 100; + chart.refresh( + chart.tasks.map((t) => (t.id !== task.id ? t : task)), + ); + }); + ctx.add_action('-', (task, chart) => { + task.progress -= (1 / task.actual_duration) * 100; + chart.refresh( + chart.tasks.map((t) => (t.id !== task.id ? t : task)), + ); + }); + }, + popup_on: 'click', readonly_progress: false, readonly_dates: false, readonly: false, diff --git a/src/index.js b/src/index.js index d92ca4b..1d2e3fb 100644 --- a/src/index.js +++ b/src/index.js @@ -669,6 +669,7 @@ export default class Gantt { make_grid_highlights() { this.highlightHolidays(); + this.config.ignored_positions = []; const height = (this.options.bar_height + this.options.padding) * @@ -1028,7 +1029,7 @@ export default class Gantt { bar_wrapper.classList.add('active'); - if (this.popup) this.popup.parent.classList.add('hidden'); + if (this.popup) this.popup.hide(); x_on_start = e.offsetX || e.layerX; y_on_start = e.offsetY || e.layerY; @@ -1360,7 +1361,6 @@ export default class Gantt { this.options.snap_at || this.config.view_mode.default_snap || '1d'; if (default_snap !== 'unit') { - console.log(default_snap); const { duration, scale } = date_utils.parse_duration(default_snap); unit_length = date_utils.convert_scales(this.config.view_mode.step, scale) / @@ -1404,7 +1404,7 @@ export default class Gantt { [...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => { el.classList.remove('active'); }); - if (this.popup) this.popup.parent.classList.remove('hidden'); + if (this.popup) this.popup.parent.classList.remove('hide'); } view_is(modes) { @@ -1431,12 +1431,16 @@ export default class Gantt { }); } - show_popup(options) { + show_popup(opts) { if (this.options.popup === false) return; if (!this.popup) { - this.popup = new Popup(this.$popup_wrapper, this.options.popup); + this.popup = new Popup( + this.$popup_wrapper, + this.options.popup, + this, + ); } - this.popup.show(options); + this.popup.show(opts); } hide_popup() { diff --git a/src/popup.js b/src/popup.js index 56ea444..c9cda46 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,7 +1,9 @@ export default class Popup { - constructor(parent, custom_html) { + constructor(parent, popup_func, gantt) { this.parent = parent; - this.custom_html = custom_html; + this.popup_func = popup_func; + this.gantt = gantt; + this.make(); } @@ -9,55 +11,53 @@ export default class Popup { this.parent.innerHTML = `
-
+
+
`; - this.hide(); this.title = this.parent.querySelector('.title'); this.subtitle = this.parent.querySelector('.subtitle'); - this.pointer = this.parent.querySelector('.pointer'); + this.details = this.parent.querySelector('.details'); + this.actions = this.parent.querySelector('.actions'); } - show(options) { - if (!options.target_element) { - throw new Error('target_element is required to show popup'); - } - const target_element = options.target_element; + show({ x, y, task, target }) { + this.actions.innerHTML = ''; + let html = this.popup_func({ + task, + chart: this.gantt, + set_title: (title) => (this.title.innerHTML = title), + set_subtitle: (subtitle) => (this.subtitle.innerHTML = subtitle), + set_details: (details) => (this.details.innerHTML = details), + add_action: (html, func) => { + let action = this.gantt.create_el({ + classes: 'action-btn', + type: 'button', + append_to: this.actions, + }); + if (typeof html === 'function') html = html(task); + action.innerHTML = html; + action.onclick = (e) => func(task, this.gantt, e); + }, + }); - if (this.custom_html) { - let html = this.custom_html(options.task); - html += '
'; - this.parent.innerHTML = html; - this.pointer = this.parent.querySelector('.pointer'); - } else { - // set data - this.title.innerHTML = options.title; - this.subtitle.innerHTML = options.subtitle; - } + if (html) this.parent.innerHTML = html; // set position let position_meta; - if (target_element instanceof HTMLElement) { - position_meta = target_element.getBoundingClientRect(); - } else if (target_element instanceof SVGElement) { - position_meta = options.target_element.getBBox(); + if (target instanceof HTMLElement) { + position_meta = target.getBoundingClientRect(); + } else if (target instanceof SVGElement) { + position_meta = target.getBBox(); } - this.parent.style.left = options.x - this.parent.clientWidth / 2 + 'px'; - this.parent.style.top = - position_meta.y + position_meta.height + 10 + 'px'; - - this.parent.classList.remove('hidden'); - this.pointer.style.left = this.parent.clientWidth / 2 + 'px'; - this.pointer.style.top = '-10px'; - - // show - this.parent.style.opacity = 1; + this.parent.style.left = x + 10 + 'px'; + this.parent.style.top = y - 10 + 'px'; + this.parent.classList.remove('hide'); } hide() { - this.parent.style.opacity = 0; - this.parent.style.left = 0; + this.parent.classList.add('hide'); } } diff --git a/src/styles/dark.css b/src/styles/dark.css index 1adfb37..15ad1d6 100644 --- a/src/styles/dark.css +++ b/src/styles/dark.css @@ -91,9 +91,5 @@ & .title { border-color: lighten(var(--g-blue-dark, 5)); } - - & .pointer { - border-top-color: #333; - } } } diff --git a/src/styles/gantt.css b/src/styles/gantt.css index 9fdb2a6..47f6b8d 100644 --- a/src/styles/gantt.css +++ b/src/styles/gantt.css @@ -13,32 +13,56 @@ position: absolute; top: 0; left: 0; - background: #171b1f; + background: #fff; + box-shadow: 0px 10px 24px -3px rgba(0, 0, 0, 0.2); padding: 10px; border-radius: 5px; width: max-content; z-index: 1000; - &.hidden { - opacity: 0 !important; - } - & .title { - margin-bottom: 5px; - text-align: -webkit-center; - text-align: center; - color: var(--g-text-light); + margin-bottom: 2px; + color: var(--g-text-dark); + font-size: 0.85rem; + font-weight: 650; + line-height: 15px; } & .subtitle { - color: var(--g-text-secondary); + color: var(--g-text-dark); + font-size: 0.8rem; + margin-bottom: 5px; } - & .pointer { - position: absolute; - height: 5px; - border: 5px solid transparent; - border-bottom-color: rgba(0, 0, 0, 0.8); + & .details { + color: var(--g-text-muted); + font-size: 0.7rem; + } + + & .actions { + margin-top: 10px; + margin-left: 3px; + } + + & .action-btn { + border: none; + padding: 5px 8px; + background-color: #dbeafe; + border-right: 1px solid var(--g-text-light); + + &:hover { + background-color: #93c5fd; + } + + &:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + + &:last-child { + border-right: none; + border-radius: 0 4px 4px 0; + } } } @@ -140,7 +164,6 @@ var(--gv-upper-header-height) + var(--gv-lower-header-height) * 0.1 ); position: absolute; - display: none; } & .current-highlight { diff --git a/src/styles/light.css b/src/styles/light.css index b95c310..9ca0ad5 100644 --- a/src/styles/light.css +++ b/src/styles/light.css @@ -10,7 +10,6 @@ --g-border-color: #ebeff2; --g-text-muted: #7c7c7c; --g-text-light: #fff; - --g-text-secondary: #98a1a9; --g-text-dark: #171717; --g-progress-color: #ebeef0; --g-handle-color: #dcdce4;