Skip to content

Commit a047177

Browse files
mariohmolclaude
andcommitted
Merge upstream/master, keep our dist/jsgantt.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents a316bfc + 6fae9eb commit a047177

7 files changed

Lines changed: 98 additions & 25 deletions

File tree

docs/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ function start(e) {
5656
vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box
5757
// {{token}} syntax (issue #382): text outside {{}} is treated as a literal label.
5858
// "Week" and "week" stay as-is; only {{ww}}, {{mon}}, {{yyyy}} etc. are substituted.
59-
vDayMajorDateDisplayFormat: "{{mon}} {{yyyy}} - Week {{ww}}", // "Week" is literal; {{ww}} → week number
60-
vWeekMajorDateDisplayFormat: "{{yyyy}} - week {{ww}}", // "week" is literal; legacy "week" token would give ISO date
59+
vDayMajorDateDisplayFormat: "{{dd}} {{mon}}", // combined with vShowEndWeekDate gives "28 Apr - 04 May"
60+
vWeekMajorDateDisplayFormat: "{{yyyy}}", // major heading groups all weeks in a year; show year only
6161
vWeekMinorDateDisplayFormat: "{{dd}} {{mon}}", // equivalent to legacy "dd mon" but explicit
6262
vLang: lang,
6363
vUseSingleCell, // Set the threshold at which we will only use one cell per table row (0 disables). Helps with rendering performance for large charts.

src/draw.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const GanttChart = function (pDiv, pFormat) {
6969
5: true,
7070
6: true,
7171
};
72+
this.vFirstDayOfWeek = 1; // 0=Sunday, 1=Monday (default), …, 6=Saturday
7273

7374
this.vEventClickCollapse = null;
7475
this.vEventClickRow = null;
@@ -381,9 +382,17 @@ export const GanttChart = function (pDiv, pFormat) {
381382

382383
vTmpDate.setDate(vTmpDate.getDate() + 1);
383384
} else if (this.vFormat == "week") {
384-
const vTmpCell = newNode(vTmpRow, "td", null, vHeaderCellClass, null, vColWidth);
385-
newNode(vTmpCell, "div", null, null, formatDateStr(vTmpDate, this.vWeekMajorDateDisplayFormat, this.vLangs[this.vLang]), vColWidth);
386-
vTmpDate.setDate(vTmpDate.getDate() + 7);
385+
// Group weeks by year: span all weeks whose start date falls in the same year
386+
const thisYear = vTmpDate.getFullYear();
387+
let countDate = new Date(vTmpDate);
388+
vColSpan = 0;
389+
while (countDate.getTime() <= vMaxDate.getTime() && countDate.getFullYear() === thisYear) {
390+
vColSpan++;
391+
countDate.setDate(countDate.getDate() + 7);
392+
}
393+
const vTmpCell = newNode(vTmpRow, "td", null, vHeaderCellClass, null, null, null, null, vColSpan);
394+
newNode(vTmpCell, "div", null, null, formatDateStr(vTmpDate, this.vWeekMajorDateDisplayFormat, this.vLangs[this.vLang]), vColWidth * vColSpan);
395+
vTmpDate.setDate(vTmpDate.getDate() + vColSpan * 7);
387396
} else if (this.vFormat == "month") {
388397
vColSpan = 12 - vTmpDate.getMonth();
389398
if (vTmpDate.getFullYear() == vMaxDate.getFullYear()) vColSpan -= 11 - vMaxDate.getMonth();
@@ -417,7 +426,9 @@ export const GanttChart = function (pDiv, pFormat) {
417426
let vMinorHeaderCellClass = "gminorheading";
418427

419428
if (this.vFormat == "day") {
420-
if (vTmpDate.getDay() % 6 == 0) {
429+
const vWkLastDay = (this.vFirstDayOfWeek + 6) % 7;
430+
const vWkPenultDay = (this.vFirstDayOfWeek + 5) % 7;
431+
if (vTmpDate.getDay() === vWkLastDay || vTmpDate.getDay() === vWkPenultDay) {
421432
if (!this.vShowWeekends) {
422433
vTmpDate.setDate(vTmpDate.getDate() + 1);
423434
continue;
@@ -528,8 +539,8 @@ export const GanttChart = function (pDiv, pFormat) {
528539
let curTaskStart = this.vTaskList[i].getStart() ? this.vTaskList[i].getStart() : this.vTaskList[i].getPlanStart();
529540
let curTaskEnd = this.vTaskList[i].getEnd() ? this.vTaskList[i].getEnd() : this.vTaskList[i].getPlanEnd();
530541

531-
const vTaskLeftPx = getOffset(vMinDate, curTaskStart, vColWidth, this.vFormat, this.vShowWeekends);
532-
const vTaskRightPx = getOffset(curTaskStart, curTaskEnd, vColWidth, this.vFormat, this.vShowWeekends);
542+
const vTaskLeftPx = getOffset(vMinDate, curTaskStart, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
543+
const vTaskRightPx = getOffset(curTaskStart, curTaskEnd, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
533544

534545
let curTaskPlanStart, curTaskPlanEnd;
535546

@@ -538,8 +549,8 @@ export const GanttChart = function (pDiv, pFormat) {
538549
let vTaskPlanLeftPx = 0;
539550
let vTaskPlanRightPx = 0;
540551
if (curTaskPlanStart && curTaskPlanEnd) {
541-
vTaskPlanLeftPx = getOffset(vMinDate, curTaskPlanStart, vColWidth, this.vFormat, this.vShowWeekends);
542-
vTaskPlanRightPx = getOffset(curTaskPlanStart, curTaskPlanEnd, vColWidth, this.vFormat, this.vShowWeekends);
552+
vTaskPlanLeftPx = getOffset(vMinDate, curTaskPlanStart, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
553+
vTaskPlanRightPx = getOffset(curTaskPlanStart, curTaskPlanEnd, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
543554
}
544555

545556
const vID = this.vTaskList[i].getID();
@@ -782,8 +793,8 @@ export const GanttChart = function (pDiv, pFormat) {
782793
this.vProcessNeeded = false;
783794

784795
// get overall min/max dates plus padding
785-
vMinDate = getMinDate(this.vTaskList, this.vFormat, this.getMinDate() && coerceDate(this.getMinDate()));
786-
vMaxDate = getMaxDate(this.vTaskList, this.vFormat, this.getMaxDate() && coerceDate(this.getMaxDate()));
796+
vMinDate = getMinDate(this.vTaskList, this.vFormat, this.getMinDate() && coerceDate(this.getMinDate()), this.vFirstDayOfWeek);
797+
vMaxDate = getMaxDate(this.vTaskList, this.vFormat, this.getMaxDate() && coerceDate(this.getMaxDate()), this.vFirstDayOfWeek);
787798

788799
// Calculate chart width variables.
789800
if (this.vFormat == "day") vColWidth = this.vDayColWidth;
@@ -872,13 +883,13 @@ export const GanttChart = function (pDiv, pFormat) {
872883

873884
if (this.vFormat == "hour") vScrollDate.setMinutes(0, 0, 0);
874885
else vScrollDate.setHours(0, 0, 0, 0);
875-
vScrollPx = getOffset(vMinDate, vScrollDate, vColWidth, this.vFormat, this.vShowWeekends) - 30;
886+
vScrollPx = getOffset(vMinDate, vScrollDate, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek) - 30;
876887
}
877888
this.getChartBody().scrollLeft = vScrollPx;
878889
}
879890

880891
if (vMinDate.getTime() <= new Date().getTime() && vMaxDate.getTime() >= new Date().getTime()) {
881-
this.vTodayPx = getOffset(vMinDate, new Date(), vColWidth, this.vFormat, this.vShowWeekends);
892+
this.vTodayPx = getOffset(vMinDate, new Date(), vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
882893
} else this.vTodayPx = -1;
883894

884895
// DEPENDENCIES: Draw lines of Dependencies
@@ -916,7 +927,7 @@ export const GanttChart = function (pDiv, pFormat) {
916927

917928
updateGridHeaderWidth(this);
918929
this.chartRowDateToX = function (date) {
919-
return getOffset(vMinDate, date, vColWidth, this.vFormat, this.vShowWeekends);
930+
return getOffset(vMinDate, date, vColWidth, this.vFormat, this.vShowWeekends, this.vFirstDayOfWeek);
920931
};
921932

922933
if (this.vEvents && this.vEvents.afterDraw) {

src/events.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ export const showToolTip = function (pGanttChartObj, e, pContents, pWidth, pTime
138138
pGanttChartObj.vTool.style.visibility = 'hidden';
139139
pGanttChartObj.vTool.style.left = Math.floor(((e) ? e.clientX : (<MouseEvent>window.event).clientX) / 2) + 'px';
140140
pGanttChartObj.vTool.style.top = Math.floor(((e) ? e.clientY : (<MouseEvent>window.event).clientY) / 2) + 'px';
141-
this.addListener('mouseover', function () { clearTimeout(pGanttChartObj.vTool.delayTimeout); }, pGanttChartObj.vTool);
142-
this.addListener('mouseout', function () { delayedHide(pGanttChartObj, pGanttChartObj.vTool, pTimer); }, pGanttChartObj.vTool);
141+
addListener('mouseover', function () { clearTimeout(pGanttChartObj.vTool.delayTimeout); }, pGanttChartObj.vTool);
142+
addListener('mouseout', function () { delayedHide(pGanttChartObj, pGanttChartObj.vTool, pTimer); }, pGanttChartObj.vTool);
143143
}
144144
clearTimeout(pGanttChartObj.vTool.delayTimeout);
145145

src/options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const includeGetSet = function () {
8888
this.Draw();
8989
};
9090
this.setWorkingDays = function (workingDays) { this.vWorkingDays = workingDays; };
91+
this.setFirstDayOfWeek = function (pVal) { this.vFirstDayOfWeek = parseInt(String(pVal), 10); };
9192
this.setMinGpLen = function (pMinGpLen) { this.vMinGpLen = pMinGpLen; };
9293
this.setScrollTo = function (pDate) { this.vScrollTo = pDate; };
9394
this.setHourColWidth = function (pWidth) { this.vHourColWidth = pWidth; };
@@ -164,6 +165,7 @@ export const includeGetSet = function () {
164165
this.getShowTaskInfoLink = function () { return this.vShowTaskInfoLink; };
165166
this.getShowEndWeekDate = function () { return this.vShowEndWeekDate; };
166167
this.getShowWeekends = function () { return this.vShowWeekends; };
168+
this.getFirstDayOfWeek = function () { return this.vFirstDayOfWeek; };
167169
this.getShowSelector = function () { return this.vShowSelector; };
168170
this.getShowDeps = function () { return this.vShowDeps; };
169171
this.getDateInputFormat = function () { return this.vDateInputFormat; };

src/utils/date_utils.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11

2+
/**
3+
* Returns the last day of the week (0=Sun … 6=Sat) for a given first day.
4+
*/
5+
const lastDayOfWeek = (firstDay: number): number => (firstDay + 6) % 7;
6+
27
/**
38
* DATES
49
*/
5-
export const getMinDate = function (pList, pFormat, pMinDate) {
10+
export const getMinDate = function (pList, pFormat, pMinDate, pFirstDayOfWeek = 1) {
611
let vDate = new Date();
712
if (pList.length <= 0) return pMinDate || vDate;
813

@@ -19,11 +24,11 @@ export const getMinDate = function (pList, pFormat, pMinDate) {
1924
// Adjust min date to specific format boundaries (first of week or first of month)
2025
if (pFormat == 'day') {
2126
vDate.setDate(vDate.getDate() - 1);
22-
while (vDate.getDay() % 7 != 1) vDate.setDate(vDate.getDate() - 1);
27+
while (vDate.getDay() != pFirstDayOfWeek) vDate.setDate(vDate.getDate() - 1);
2328
}
2429
else if (pFormat == 'week') {
2530
vDate.setDate(vDate.getDate() - 1);
26-
while (vDate.getDay() % 7 != 1) vDate.setDate(vDate.getDate() - 1);
31+
while (vDate.getDay() != pFirstDayOfWeek) vDate.setDate(vDate.getDate() - 1);
2732
}
2833
else if (pFormat == 'month') {
2934
vDate.setDate(vDate.getDate() - 15);
@@ -50,7 +55,7 @@ export const getMinDate = function (pList, pFormat, pMinDate) {
5055
return (vDate);
5156
};
5257

53-
export const getMaxDate = function (pList, pFormat, pMaxDate) {
58+
export const getMaxDate = function (pList, pFormat, pMaxDate, pFirstDayOfWeek = 1) {
5459
let vDate = new Date();
5560

5661
if (pList.length <= 0) return pMaxDate || vDate;
@@ -68,13 +73,13 @@ export const getMaxDate = function (pList, pFormat, pMaxDate) {
6873
// Adjust max date to specific format boundaries (end of week or end of month)
6974
if (pFormat == 'day') {
7075
vDate.setDate(vDate.getDate() + 1);
71-
while (vDate.getDay() % 7 != 0) vDate.setDate(vDate.getDate() + 1);
76+
while (vDate.getDay() != lastDayOfWeek(pFirstDayOfWeek)) vDate.setDate(vDate.getDate() + 1);
7277
}
7378
else if (pFormat == 'week') {
7479
//For weeks, what is the last logical boundary?
7580
vDate.setDate(vDate.getDate() + 1);
7681

77-
while (vDate.getDay() % 7 != 0) vDate.setDate(vDate.getDate() + 1);
82+
while (vDate.getDay() != lastDayOfWeek(pFirstDayOfWeek)) vDate.setDate(vDate.getDate() + 1);
7883
}
7984
else if (pFormat == 'month') {
8085
// Set to last day of current Month

src/utils/general_utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export const calculateCurrentDateOffset = function(curTaskStart, curTaskEnd){
118118
return (tmpTaskEnd - tmpTaskStart);
119119
}
120120

121-
export const getOffset = function (pStartDate, pEndDate, pColWidth, pFormat, pShowWeekends) {
121+
export const getOffset = function (pStartDate, pEndDate, pColWidth, pFormat, pShowWeekends, pFirstDayOfWeek = 1) {
122122
const DAY_CELL_MARGIN_WIDTH = 3; // Cell margin for 'day' format
123123
const WEEK_CELL_MARGIN_WIDTH = 3; // Cell margin for 'week' format
124124
const MONTH_CELL_MARGIN_WIDTH = 3; // Cell margin for 'month' format
@@ -142,7 +142,9 @@ export const getOffset = function (pStartDate, pEndDate, pColWidth, pFormat, pSh
142142
let countWeekends = 0;
143143
while (start < end) {
144144
const day = start.getDay();
145-
if (day === 6 || day == 0) {
145+
const vLastDay = (pFirstDayOfWeek + 6) % 7;
146+
const vPenultDay = (pFirstDayOfWeek + 5) % 7;
147+
if (day === vLastDay || day === vPenultDay) {
146148
countWeekends++
147149
}
148150
start = new Date(start.getTime() + 24 * oneHour);

test/unit/date-utils.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,56 @@ describe('getMaxDate', () => {
228228
`Expected maxDate in September or later 2025, got month ${maxDate.getMonth()}`);
229229
});
230230
});
231+
232+
// ── Issue #379: Configurable first day of week ────────────────────────────────
233+
234+
describe('getMinDate — vFirstDayOfWeek', () => {
235+
// 2025-04-09 is a Wednesday. For 'day' format the function steps back to the
236+
// start of the week, so the result depends on which day opens the week.
237+
const task = makeTask({ id: 100, parent: 0, start: new Date('2025-04-09'), end: new Date('2025-04-16') });
238+
239+
it('default (Monday=1): min boundary is the preceding Monday', () => {
240+
const minDate = getMinDate([task], 'day');
241+
expect(minDate.getDay()).to.equal(1, `Expected Monday (1), got day ${minDate.getDay()} (${minDate.toDateString()})`);
242+
});
243+
244+
it('Sunday-first (0): min boundary is the preceding Sunday', () => {
245+
const minDate = getMinDate([task], 'day', undefined, 0);
246+
expect(minDate.getDay()).to.equal(0, `Expected Sunday (0), got day ${minDate.getDay()} (${minDate.toDateString()})`);
247+
});
248+
249+
it('Saturday-first (6): min boundary is the preceding Saturday', () => {
250+
const minDate = getMinDate([task], 'day', undefined, 6);
251+
expect(minDate.getDay()).to.equal(6, `Expected Saturday (6), got day ${minDate.getDay()} (${minDate.toDateString()})`);
252+
});
253+
254+
it('Sunday-first (0): week format also snaps to Sunday', () => {
255+
const minDate = getMinDate([task], 'week', undefined, 0);
256+
expect(minDate.getDay()).to.equal(0, `Expected Sunday (0) in week format, got ${minDate.toDateString()}`);
257+
});
258+
});
259+
260+
describe('getMaxDate — vFirstDayOfWeek', () => {
261+
// 2025-04-09 is a Wednesday.
262+
const task = makeTask({ id: 110, parent: 0, start: new Date('2025-04-09'), end: new Date('2025-04-09') });
263+
264+
it('default (Monday=1): max boundary is the following Sunday (6)', () => {
265+
const maxDate = getMaxDate([task], 'day');
266+
expect(maxDate.getDay()).to.equal(0, `Expected Sunday (0), got day ${maxDate.getDay()} (${maxDate.toDateString()})`);
267+
});
268+
269+
it('Sunday-first (0): max boundary is the following Saturday (6)', () => {
270+
const maxDate = getMaxDate([task], 'day', undefined, 0);
271+
expect(maxDate.getDay()).to.equal(6, `Expected Saturday (6), got day ${maxDate.getDay()} (${maxDate.toDateString()})`);
272+
});
273+
274+
it('Saturday-first (6): max boundary is the following Friday (5)', () => {
275+
const maxDate = getMaxDate([task], 'day', undefined, 6);
276+
expect(maxDate.getDay()).to.equal(5, `Expected Friday (5), got day ${maxDate.getDay()} (${maxDate.toDateString()})`);
277+
});
278+
279+
it('Sunday-first (0): week format max boundary is Saturday (6)', () => {
280+
const maxDate = getMaxDate([task], 'week', undefined, 0);
281+
expect(maxDate.getDay()).to.equal(6, `Expected Saturday (6) in week format, got ${maxDate.toDateString()}`);
282+
});
283+
});

0 commit comments

Comments
 (0)