Skip to content

Commit 6cab2bd

Browse files
committed
Align Gantt behavior with supported surface
1 parent 84130bb commit 6cab2bd

6 files changed

Lines changed: 165 additions & 246 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Gauge styles: `heat` (blue→amber→red), `load` (emerald→amber→red), `emer
201201

202202
Interactive task scheduling with Frappe Gantt. Split-pane layout: task grid
203203
on top, SVG timeline chart on bottom. Drag to reschedule, resize to change
204-
duration, project-colored bars, dependency arrows.
204+
duration, sortable grid columns, project-colored bars, and pinned task styling.
205205

206206
```html
207207
<link rel="stylesheet" href="/sf/vendor/frappe-gantt/frappe-gantt.min.css">
@@ -216,9 +216,9 @@ var gantt = SF.gantt.create({
216216
viewMode: 'Quarter Day',
217217
splitSizes: [40, 60],
218218
columns: [
219-
{ key: 'name', label: 'Task' },
220-
{ key: 'start', label: 'Start' },
221-
{ key: 'end', label: 'End' },
219+
{ key: 'name', label: 'Task', sortable: true },
220+
{ key: 'start', label: 'Start', sortable: true },
221+
{ key: 'end', label: 'End', sortable: true },
222222
{ key: 'priority', label: 'P', render: function (t) {
223223
return '<span class="sf-priority-badge priority-' + t.priority + '">P' + t.priority + '</span>';
224224
}},
@@ -236,6 +236,8 @@ gantt.setTasks([
236236
start: '2026-03-15 09:00',
237237
end: '2026-03-15 10:30',
238238
priority: 1,
239+
projectIndex: 0,
240+
pinned: true,
239241
custom_class: 'project-color-0 priority-1',
240242
dependencies: '',
241243
},
@@ -255,6 +257,7 @@ gantt.highlightTask('task-1');
255257
```
256258

257259
View modes: `Quarter Day`, `Half Day`, `Day`, `Week`, `Month`.
260+
Sortable headers are opt-in per column via `sortable: true`.
258261

259262
## Backend Adapters
260263

‎css-src/17-gantt-layout.css‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,6 @@
9494
border-radius: var(--sf-radius-sm);
9595
}
9696

97-
/* ── Maximized Pane ── */
98-
.sf-gantt-pane.maximized {
99-
position: absolute !important;
100-
inset: 0 !important;
101-
z-index: 50;
102-
height: 100% !important;
103-
width: 100% !important;
104-
}
105-
10697
/* ── Grid Table ── */
10798
.sf-gantt-grid {
10899
height: 100%;

‎css-src/18-gantt-bars.css‎

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@
2020
.gantt .bar-wrapper.priority-2 .bar { opacity: 0.85; }
2121
.gantt .bar-wrapper.priority-3 .bar { opacity: 0.7; }
2222

23-
/* Cross-project dependency arrows */
24-
.gantt .arrow path.cross-project-arrow {
25-
stroke: var(--sf-emerald-500);
26-
stroke-dasharray: 5 3;
27-
stroke-width: 1.4;
28-
}
29-
3023
/* Bar hover glow */
3124
.gantt .bar-wrapper:hover .bar {
3225
filter: brightness(1.12) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.22));
@@ -58,61 +51,13 @@
5851
width 0.12s var(--sf-ease-out);
5952
}
6053

61-
/* Dragging state */
62-
.gantt .bar-wrapper.dragging {
63-
opacity: 0.88;
64-
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
65-
z-index: 100;
66-
transition: none !important;
67-
}
68-
69-
.gantt .bar-wrapper.dragging .bar {
70-
stroke-width: 2;
71-
stroke: rgba(255, 255, 255, 0.7);
72-
}
73-
74-
body.sf-gantt-dragging {
75-
cursor: grabbing !important;
76-
user-select: none !important;
77-
}
78-
79-
body.sf-gantt-dragging * {
80-
cursor: grabbing !important;
81-
}
82-
83-
/* ── Drag Tooltip ── */
84-
.sf-gantt-drag-tooltip {
85-
position: fixed;
86-
z-index: 9999;
87-
background: rgba(15, 23, 42, 0.92);
88-
color: #f1f5f9;
89-
font-size: 11.5px;
90-
font-family: var(--sf-font-mono);
91-
font-weight: 500;
92-
padding: 5px 10px;
93-
border-radius: 6px;
94-
pointer-events: none;
95-
white-space: nowrap;
96-
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
97-
backdrop-filter: blur(4px);
98-
border: 1px solid rgba(255, 255, 255, 0.12);
99-
letter-spacing: 0.02em;
100-
display: none;
101-
}
102-
10354
/* ── Pinned Task Indicators ── */
10455
.gantt .bar-wrapper.pinned .bar {
10556
stroke: var(--sf-amber-500);
10657
stroke-width: 2;
10758
stroke-dasharray: 6 3;
10859
}
10960

110-
.sf-gantt-pin-icon {
111-
color: var(--sf-amber-500);
112-
font-size: 11px;
113-
opacity: 0.85;
114-
}
115-
11661
/* ── Highlighted task (pulse from grid click) ── */
11762
.gantt .bar-wrapper.highlighted .bar {
11863
animation: sf-gantt-pulse 0.5s ease-in-out 3;
@@ -149,40 +94,6 @@ body.sf-gantt-dragging * {
14994
color: var(--sf-gray-500);
15095
}
15196

152-
/* ── Project Badge (grid table) ── */
153-
.sf-project-badge {
154-
display: inline-flex;
155-
align-items: center;
156-
padding: var(--sf-badge-padding-y) var(--sf-badge-padding-x);
157-
padding-left: 20px;
158-
border-radius: var(--sf-badge-radius);
159-
font-size: var(--sf-badge-font-size);
160-
font-weight: var(--sf-badge-font-weight);
161-
position: relative;
162-
background: var(--sf-gray-100);
163-
}
164-
165-
.sf-project-badge::before {
166-
content: '';
167-
position: absolute;
168-
left: 7px;
169-
top: 50%;
170-
transform: translateY(-50%);
171-
width: 6px;
172-
height: 6px;
173-
border-radius: 50%;
174-
background: currentColor;
175-
}
176-
177-
.sf-project-badge.sf-project-0 { color: var(--sf-project-0); background: var(--sf-project-0-light); }
178-
.sf-project-badge.sf-project-1 { color: var(--sf-project-1); background: var(--sf-project-1-light); }
179-
.sf-project-badge.sf-project-2 { color: var(--sf-project-2); background: var(--sf-project-2-light); }
180-
.sf-project-badge.sf-project-3 { color: var(--sf-project-3); background: var(--sf-project-3-light); }
181-
.sf-project-badge.sf-project-4 { color: var(--sf-project-4); background: var(--sf-project-4-light); }
182-
.sf-project-badge.sf-project-5 { color: var(--sf-project-5); background: var(--sf-project-5-light); }
183-
.sf-project-badge.sf-project-6 { color: var(--sf-project-6); background: var(--sf-project-6-light); }
184-
.sf-project-badge.sf-project-7 { color: var(--sf-project-7); background: var(--sf-project-7-light); }
185-
18697
/* ── Gantt Popup ── */
18798
.sf-gantt-popup {
18899
padding: 8px 12px;

‎js-src/14-gantt.js‎

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
var ganttChart = null;
1717
var splitInstance = null;
1818
var tasks = [];
19+
var sortState = { key: null, direction: 'asc' };
1920

2021
// ── Build DOM ──
2122
var wrapper = sf.el('div', { className: 'sf-gantt-split' });
@@ -38,7 +39,6 @@
3839
var chartHeader = sf.el('div', { className: 'sf-gantt-pane-header' });
3940
chartHeader.appendChild(sf.el('h3', null, config.chartTitle || 'Timeline'));
4041

41-
// View mode selector
4242
var viewControls = sf.el('div', { className: 'sf-gantt-view-controls' });
4343
var viewSelect = sf.el('select', { className: 'sf-gantt-view-select' });
4444
var modes = [
@@ -54,9 +54,7 @@
5454
viewSelect.appendChild(opt);
5555
});
5656
viewSelect.addEventListener('change', function () {
57-
if (ganttChart) {
58-
ganttChart.change_view_mode(viewSelect.value);
59-
}
57+
if (ganttChart) ganttChart.change_view_mode(viewSelect.value);
6058
});
6159
viewControls.appendChild(viewSelect);
6260

@@ -83,15 +81,14 @@
8381
};
8482

8583
ctrl.setTasks = function (newTasks) {
86-
tasks = newTasks;
87-
renderGrid(newTasks);
88-
renderChart(newTasks);
84+
tasks = Array.isArray(newTasks) ? newTasks.slice() : [];
85+
renderGrid(tasks);
86+
renderChart(tasks);
8987
};
9088

9189
ctrl.refresh = function () {
9290
if (ganttChart && tasks.length > 0) {
93-
var frappeTasks = tasksToFrappe(tasks);
94-
ganttChart.refresh(frappeTasks);
91+
ganttChart.refresh(tasksToFrappe(tasks));
9592
}
9693
};
9794

@@ -103,11 +100,9 @@
103100
};
104101

105102
ctrl.highlightTask = function (taskId) {
106-
// Grid highlight
107103
grid.querySelectorAll('.sf-gantt-row').forEach(function (row) {
108104
row.classList.toggle('selected', row.dataset.taskId === taskId);
109105
});
110-
// Bar highlight
111106
var svg = chartContainer.querySelector('svg');
112107
if (svg) {
113108
svg.querySelectorAll('.bar-wrapper').forEach(function (bw) {
@@ -119,15 +114,16 @@
119114
};
120115

121116
ctrl.destroy = function () {
122-
if (splitInstance) { splitInstance.destroy(); splitInstance = null; }
117+
if (splitInstance) {
118+
splitInstance.destroy();
119+
splitInstance = null;
120+
}
123121
ganttChart = null;
124122
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
125123
};
126124

127125
return ctrl;
128126

129-
// ── Internal ──
130-
131127
function initSplit() {
132128
if (typeof Split !== 'function') return;
133129
splitInstance = Split(['#' + gridPaneId, '#' + chartPaneId], {
@@ -149,12 +145,16 @@
149145
return taskList
150146
.filter(function (t) { return t.start && t.end; })
151147
.map(function (t) {
148+
var customClass = t.custom_class || '';
149+
if (t.pinned) {
150+
customClass = customClass ? customClass + ' pinned' : 'pinned';
151+
}
152152
return {
153153
id: t.id,
154154
name: t.name || t.label || t.id,
155155
start: t.start,
156156
end: t.end,
157-
custom_class: t.custom_class || '',
157+
custom_class: customClass,
158158
dependencies: t.dependencies || '',
159159
};
160160
});
@@ -188,32 +188,36 @@
188188
function renderGrid(taskList) {
189189
grid.innerHTML = '';
190190
var table = sf.el('table', { className: 'sf-gantt-table' });
191-
192-
// Header
193-
var thead = sf.el('thead');
194-
var headerRow = sf.el('tr');
195191
var columns = config.columns || [
196192
{ key: 'name', label: 'Task' },
197193
{ key: 'start', label: 'Start' },
198194
{ key: 'end', label: 'End' },
199195
];
196+
var sortedTasks = sortTasks(taskList);
197+
198+
var thead = sf.el('thead');
199+
var headerRow = sf.el('tr');
200200
columns.forEach(function (col) {
201-
headerRow.appendChild(sf.el('th', null, col.label));
201+
headerRow.appendChild(buildHeaderCell(col));
202202
});
203203
thead.appendChild(headerRow);
204204
table.appendChild(thead);
205205

206-
// Body
207206
var tbody = sf.el('tbody');
208-
taskList.forEach(function (task) {
207+
sortedTasks.forEach(function (task) {
208+
var rowClasses = ['sf-gantt-row'];
209+
if (task.custom_class) rowClasses.push(task.custom_class);
210+
if (task.projectIndex != null) rowClasses.push('sf-project-' + task.projectIndex);
211+
209212
var tr = sf.el('tr', {
210-
className: 'sf-gantt-row' + (task.custom_class ? ' ' + task.custom_class : ''),
213+
className: rowClasses.join(' '),
211214
dataset: { taskId: task.id },
212215
onClick: function () {
213216
ctrl.highlightTask(task.id);
214217
if (config.onTaskClick) config.onTaskClick(task);
215218
},
216219
});
220+
217221
columns.forEach(function (col) {
218222
var td = sf.el('td');
219223
if (col.key === 'name') {
@@ -230,12 +234,64 @@
230234
}
231235
tr.appendChild(td);
232236
});
237+
233238
tbody.appendChild(tr);
234239
});
235240
table.appendChild(tbody);
236241
grid.appendChild(table);
237242
}
238243

244+
function buildHeaderCell(col) {
245+
if (!col.sortable) {
246+
return sf.el('th', null, col.label);
247+
}
248+
249+
var isCurrent = sortState.key === col.key;
250+
var th = sf.el('th', {
251+
className: 'sortable' + (isCurrent ? ' active' : ''),
252+
role: 'button',
253+
tabIndex: 0,
254+
'aria-sort': isCurrent ? (sortState.direction === 'asc' ? 'ascending' : 'descending') : 'none',
255+
});
256+
th.appendChild(document.createTextNode(col.label));
257+
th.appendChild(sf.el('span', { className: 'sort-icon' }, isCurrent ? (sortState.direction === 'asc' ? '▲' : '▼') : ''));
258+
259+
sf.bindActivation(th, function () {
260+
if (sortState.key === col.key) {
261+
sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc';
262+
} else {
263+
sortState.key = col.key;
264+
sortState.direction = 'asc';
265+
}
266+
renderGrid(tasks);
267+
});
268+
269+
return th;
270+
}
271+
272+
function sortTasks(taskList) {
273+
if (!sortState.key) return taskList.slice();
274+
var sorted = taskList.slice();
275+
sorted.sort(function (a, b) {
276+
var aVal = sortValue(a[sortState.key], sortState.key);
277+
var bVal = sortValue(b[sortState.key], sortState.key);
278+
if (aVal === bVal) return 0;
279+
if (sortState.direction === 'asc') return aVal < bVal ? -1 : 1;
280+
return aVal > bVal ? -1 : 1;
281+
});
282+
return sorted;
283+
}
284+
285+
function sortValue(value, key) {
286+
if (value == null) return '';
287+
if (key === 'start' || key === 'end') {
288+
var parsed = Date.parse(value);
289+
return isNaN(parsed) ? String(value).toLowerCase() : parsed;
290+
}
291+
if (typeof value === 'number') return value;
292+
return String(value).toLowerCase();
293+
}
294+
239295
function defaultPopup(task) {
240296
var t = tasks.find(function (x) { return x.id === task.id; });
241297
if (!t) return '';

0 commit comments

Comments
 (0)