feat: replace tooltip with popup

This commit is contained in:
Safwan Samsudeen 2024-12-17 14:41:24 +05:30
parent a3c9f15cf9
commit 350dc88785
7 changed files with 188 additions and 129 deletions

View File

@ -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)<br/>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,
);
}

View File

@ -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' : ''})<br/>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,

View File

@ -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() {

View File

@ -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 = `
<div class="title"></div>
<div class="subtitle"></div>
<div class="pointer"></div>
<div class="details"></div>
<div class="actions"></div>
`;
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 += '<div class="pointer"></div>';
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');
}
}

View File

@ -91,9 +91,5 @@
& .title {
border-color: lighten(var(--g-blue-dark, 5));
}
& .pointer {
border-top-color: #333;
}
}
}

View File

@ -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 {

View File

@ -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;