zul/src/main/resources/web/js/zul/db/Calendar.ts
/* Calendar.js
{{IS_NOTE
Purpose:
Description:
History:
Fri Jan 23 10:32:34 TST 2009, Created by Flyworld
}}IS_NOTE
Copyright (C) 2008 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
}}IS_RIGHT
*/
/** The date related widgets, such as datebox and calendar.
*/
//zk.$package('zul.db');
export interface MarkCalOptions {
sameMonth?: boolean;
silent?: boolean;
timeout?: number;
}
interface TimeZoneWidget extends zk.Widget {
getTimeZone?: () => string;
}
declare global {
interface HTMLTableCellElement {
/** @internal */
_monofs?: number;
}
}
function _newDate(year, month, day, bFix, tz?: string): DateImpl {
var v = Dates.newInstance([year, month, day], tz);
return bFix && v.getMonth() != month && v.getDate() != day ? //Bug ZK-1213: also need to check date
Dates.newInstance([year, month + 1, 0], tz)/*last day of month*/ : v;
}
function _getTimeZone(wgt: zul.db.Calendar): string | undefined {
var parent: TimeZoneWidget | undefined = wgt.parent,
tz = parent?.getTimeZone?.();
return tz ? tz : wgt._defaultTzone;
}
/** @class zul.db.Renderer
* The renderer used to render a calendar.
* It is designed to be overridden
*/
export var Renderer = {
/**
* @returns the HTML fragment representing a day cell.
* By overriding this method, you could customize the look of a day cell.
* @defaultValue day
* @param cal - the calendar
* @param y - the year
* @param m - the month (between 0 to 11)
* @param day - the day (between 1 to 31)
* @param monthofs - the month offset. If the day is in the same month
* @since 5.0.3
*/
cellHTML(cal: zul.db.Calendar, y: number, m: number, day: number, monthofs: number): string {
return String(day);
},
/**
* @returns the label of a date cell.
* By overriding this method, you could customize the aria-label of a day cell.
* @defaultValue dd MMMM, yyyy
* @param cal - the calendar
* @param y - the year
* @param m - the month (between 0 to 11)
* @param day - the day (between 1 to 31)
* @param monthofs - the month offset. If the day is in the same month
* @param dayofweek - the day of the week (between 0 to 6)
* @since 9.5.0
*/
cellAriaLabel(cal: zul.db.Calendar, y: number, m: number, day: number, monthofs: number, dayofweek: number): string {
var localizedSymbols = cal.getLocalizedSymbols();
return day + ' ' + localizedSymbols.FMON![m] + ', ' + y;
},
/**
* Called before {@link zul.db.Calendar#redraw} is invoked.
* @defaultValue does nothing
* @param cal - the calendar
* @since 5.0.3
*/
beforeRedraw(cal: zul.db.Calendar): void {
// empty on purpose
},
/**
* Tests if the specified date is disabled.
* @defaultValue it depends on the constraint, if any
* @param cal - the calendar
* @param y - the year
* @param m - the month (between 0 to 11)
* @param v - the day (between 1 to 31)
* @param today - today
* @since 5.0.3
*/
disabled(cal: zul.db.Calendar, y: number, m: number, v: number, today: DateImpl): boolean {
var d = Dates.newInstance([y, m, v, 0, 0, 0, 0], _getTimeZone(cal)),
constraint;
if ((constraint = cal._constraint) && typeof constraint == 'string') {
// Bug ID: 3106676
if ((constraint.includes('no past') && (+d - +today) / 86400000 < 0)
|| (constraint.includes('no future') && (+today - +d) / 86400000 < 0)
|| (constraint.includes('no today') && +today - +d == 0))
return true;
}
var result = false;
if (cal._beg && (result = (+d - +cal._beg) / 86400000 < 0))
return result;
if (cal._end && (result = (+cal._end - +d) / 86400000 < 0))
return result;
return result;
},
/**
* @returns the label of the week of year.
* @defaultValue the string of the value
* @param wgt - the calendar widget
* @param val - number of the week of the value
* @since 6.5.0
*/
labelOfWeekOfYear(wgt: zul.db.Calendar, val: number): string {
return String(val);
},
/**
* @returns the title of the week of year.
* @defaultValue 'Wk'
* @param wgt - the calendar widget
* @since 6.5.0
*/
titleOfWeekOfYear(wgt: zul.db.Calendar): string {
return 'Wk';
},
/**
* Generates the title of the content HTML.
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 6.5.3
*/
titleHTML(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var uuidHTML = /*safe*/ wgt.uuid,
view = wgt._view,
val = wgt.getTime(),
m = val.getMonth(),
y = val.getFullYear(),
date = val._moment.toDate(),
localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, {year: 'numeric'}),
displayYear = this._getDisplayYear(date, localizedSymbols, localeDateTimeFormat),
yofs = y - (y % 10 + 1),
ydec = zk.parseInt(y / 100),
textHTML = wgt.$s('text'),
minyear = wgt._minyear,
maxyear = wgt._maxyear,
endYearLength = this._getPadYearLength(wgt, localizedSymbols, localeDateTimeFormat);
switch (view) {
case 'day':
out.push('<span id="', uuidHTML, '-tm" class="', textHTML, '">',
/*safe*/ localizedSymbols.SMON![m], '</span> <span id="', uuidHTML,
'-ty" class="', textHTML, '">', /*safe*/ displayYear, '</span>');
break;
case 'month':
out.push('<span id="', uuidHTML,
'-ty" class="', textHTML, '">', /*safe*/ displayYear, '</span>');
break;
case 'year':
var yearGap = 11,
startYear = yofs < minyear ? minyear : yofs,
startDate = new Date(startYear, m),
displayStartYear = this._getDisplayYear(startDate, localizedSymbols, localeDateTimeFormat, endYearLength),
expectedEndYear = yofs + yearGap;
endYear = expectedEndYear > maxyear ? maxyear : expectedEndYear,
endDate = new Date(endYear, m),
displayEndYear = this._getDisplayYear(endDate, localizedSymbols, localeDateTimeFormat, endYearLength);
out.push('<span id="', uuidHTML, '-tyd" class="', textHTML, '">',
/*safe*/ displayStartYear, ' - ', /*safe*/ displayEndYear, '</span>');
break;
case 'decade':
// each start year of cell is ten more than previous one,
// so the end year of last cell equals the start year of first cell add 10 * 11 + 9.
var yearGap = 10 * 11 + 9,
expectedStartYear = ydec * 100 - 10,
startYear = expectedStartYear < minyear ? minyear : expectedStartYear,
startDate = new Date(startYear, m),
displayStartYear = this._getDisplayYear(startDate, localizedSymbols, localeDateTimeFormat, endYearLength),
expectedEndYear = expectedStartYear + yearGap,
endYear = expectedEndYear > maxyear ? maxyear : expectedEndYear,
endDate = new Date(endYear, m),
displayEndYear = this._getDisplayYear(endDate, localizedSymbols, localeDateTimeFormat, endYearLength);
out.push('<span id="', uuidHTML, '-tyd" class="', textHTML, '">',
/*safe*/ displayStartYear, ' - ', /*safe*/ displayEndYear, '</span>');
break;
}
},
/**
* Renderer the dayView for this calendar
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 6.5.0
*/
dayView(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var uuidHTML = /*safe*/ wgt.uuid,
sun = (7 - localizedSymbols.DOW_1ST!) % 7, sat = (6 + sun) % 7,
wkend = wgt.$s('weekend'),
wkday = wgt.$s('weekday'),
cell = wgt.$s('cell');
out.push('<table role="grid" class="', wgt.$s('body'), '" id="', uuidHTML, '-mid"',
/*safe*/ zUtl.cellps0, '>', '<thead><tr>');
for (var j = 0; j < 7; ++j)
out.push('<th class="', (j == sun || j == sat) ? /*safe*/ wkend : /*safe*/ wkday,
'" aria-label="', /*safe*/ localizedSymbols.FDOW![j], '">', /*safe*/ localizedSymbols.S2DOW![j], '</th>');
out.push('</tr></thead><tbody>');
for (var j = 0; j < 6; ++j) { //at most 7 rows
out.push('<tr id="', uuidHTML, '-w', j as unknown as string, '">');
for (var k = 0; k < 7; ++k)
out.push('<td id="', uuidHTML, '-w', j as unknown as string, '-p', k as unknown as string, '" class="', /*safe*/ cell, ' ', (k == sun || k == sat) ? /*safe*/ wkend : /*safe*/ wkday,
'"></td>');
out.push('</tr>');
}
out.push('</tbody></table>');
},
/**
* Renderer the monthView for this calendar
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 6.5.0
*/
monthView(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var uuid = wgt.uuid,
cell = wgt.$s('cell');
out.push('<table role="grid" class="', wgt.$s('body'), ' ', wgt.$s('month'),
'" id="', /*safe*/ uuid, '-mid"', /*safe*/ zUtl.cellps0, '><tbody>');
for (var j = 0; j < 12; ++j) {
if (!(j % 4)) out.push('<tr>');
out.push('<td class="', /*safe*/ cell, '" id="', /*safe*/ uuid, '-m',
j as unknown as string, '" data-value="', j as unknown as string,
'" aria-label="', /*safe*/ localizedSymbols.FMON![j], '">',
/*safe*/ localizedSymbols.SMON![j], '</td>');
if (!((j + 1) % 4)) out.push('</tr>');
}
out.push('</tbody></table>');
},
/**
* Renderer the yearView for this calendar
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 6.5.0
*/
yearView(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var uuid = wgt.uuid,
cell = wgt.$s('cell'),
disd = wgt.$s('disabled'),
val = wgt.getTime(),
y = val.getFullYear(),
yofs = y - (y % 10 + 1),
minyear = wgt._minyear,
maxyear = wgt._maxyear,
localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, {year: 'numeric'}),
endYearLength = this._getPadYearLength(wgt, localizedSymbols, localeDateTimeFormat);
out.push('<table role="grid" class="', wgt.$s('body'), ' ', wgt.$s('year'), '" id="', /*safe*/ uuid, '-mid"',
/*safe*/ zUtl.cellps0, '><tbody>');
for (var j = 0; j < 12; ++j) {
if (!(j % 4)) out.push('<tr>');
if (yofs < minyear || yofs > maxyear) {
out.push('<td class="', /*safe*/ disd, '"> </td>');
if (j + 1 == 12)
out.push('</tr>');
yofs++;
continue;
}
var date = new Date(yofs, 0);
out.push('<td class="', /*safe*/ cell, '" data-value="', yofs as unknown as string, '" id="', /*safe*/ uuid, '-y', j as unknown as string, '" >',
/*safe*/ this._getDisplayYear(date, localizedSymbols, localeDateTimeFormat, endYearLength), '</td>');
if (!((j + 1) % 4)) out.push('</tr>');
yofs++;
}
out.push('</tbody></table>');
},
/**
* Renderer the decadeView for this calendar
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 6.5.0
*/
decadeView(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var uuid = wgt.uuid,
cell = wgt.$s('cell'),
disd = wgt.$s('disabled'),
val = wgt.getTime(),
y = val.getFullYear(),
ydec = zk.parseInt(y / 100),
minyear = wgt._minyear,
maxyear = wgt._maxyear,
mindec = zk.parseInt(minyear / 10) * 10,
maxdec = zk.parseInt(maxyear / 10) * 10,
localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, {year: 'numeric'}),
endYearLength = this._getPadYearLength(wgt, localizedSymbols, localeDateTimeFormat);
out.push('<table role="grid" class="', wgt.$s('body'), ' ', wgt.$s('decade'),
'" id="', /*safe*/ uuid, '-mid"',
/*safe*/ zUtl.cellps0, '><tbody>');
var temp = ydec * 100 - 10,
selected = wgt.$s('selected');
for (var j = 0; j < 12; ++j, temp += 10) {
if (!(j % 4)) out.push('<tr>');
if (temp < mindec || temp > maxdec) {
out.push('<td class="', /*safe*/ disd, '"> </td>');
if (j + 1 == 12)
out.push('</tr>');
continue;
}
var startDate = new Date(temp < minyear ? minyear : temp, 0),
endDate = new Date(temp + 9 > maxyear ? maxyear : temp + 9, 11);
out.push('<td data-value="', temp as unknown as string, '" id="', /*safe*/ uuid, '-de', j as unknown as string, '" class="',
/*safe*/ cell, (y >= temp && y <= (temp + 9)) ? ' ' + /*safe*/ selected : '', '" >',
/*safe*/ this._getDisplayYear(startDate, localizedSymbols, localeDateTimeFormat, endYearLength) + ' -<br aria-hidden="true" />'
+ /*safe*/ this._getDisplayYear(endDate, localizedSymbols, localeDateTimeFormat, endYearLength) + '</td>');
if (!((j + 1) % 4)) out.push('</tr>');
}
out.push('</tbody></table>');
},
/**
* Renderer the today link for this calendar
* @param wgt - the calendar widget
* @param out - an array to output HTML fragments.
* @param localizedSymbols - the symbols for localization
* @since 8.0.0
*/
todayView(wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols): void {
var val = wgt.getTodayLinkLabel();
if (!val) {
var tz = _getTimeZone(wgt);
val = new zk.fmt.Calendar().formatDate(zUtl.today(!!wgt.parent, tz), wgt.getFormat(), localizedSymbols);
}
out.push(zUtl.encodeXML(val));
},
/** @internal */
_getDisplayYear(date: Date, localizedSymbols: zk.LocalizedSymbols, localeDateTimeFormat: Intl.DateTimeFormat, padLength?: number): string { // override
return date.getFullYear() + localizedSymbols.YDELTA! + '';
},
/** @internal */
_getPadYearLength(wgt: zul.db.Calendar, localizedSymbols: zk.LocalizedSymbols, localeDateTimeFormat: Intl.DateTimeFormat): number {
var y = wgt.getTime().getFullYear(),
yearGap = 10 * 11 + 9,
maxyear = wgt._maxyear,
ydec = zk.parseInt(y / 100),
expectedStartYear = ydec * 100 - 10,
expectedEndYear = expectedStartYear + yearGap,
endYear = expectedEndYear > maxyear ? maxyear : expectedEndYear,
endYearLength = this._getDisplayYear(new Date(endYear, 0), localizedSymbols, localeDateTimeFormat).replace(/^\D+/g, '').length;
return endYearLength;
}
};
zul.db.Renderer = Renderer;
export interface CalendarOnChangeData {
value?: DateImpl;
shallClose: boolean;
shiftView: boolean;
}
@zk.WrapClass('zul.db.Calendar')
export class Calendar extends zul.Widget {
/** @internal */
_view = 'day';
/** @internal */
_minyear = 1900; //"day", "month", "year", "decade",
/** @internal */
_maxyear = 2099;
/** @internal */
_beg?: DateImpl;
/** @internal */
_end?: DateImpl;
/** @internal */
_constraint?: string;
/** @internal */
_localizedSymbols?: zk.LocalizedSymbols;
/** @internal */
_selectedValue?: DateImpl;
/** @internal */
_value?: DateImpl;
/** @internal */
_defaultTzone?: string;
/** @internal */
_name?: string;
/** @internal */
_weekOfYear?: boolean;
/** @internal */
_showTodayLink?: boolean;
efield?: HTMLInputElement;
/** @internal */
_fmt?: string;
/** @internal */
_todayLinkLabel?: string;
constructor() {
super();
this.listen({onChange: this}, -1000);
}
/**
* Assigns a value to this component.
* @param value - the date to assign. If null, today is assumed.
*/
setValue(value: DateImpl, opts?: Record<string, boolean>): this {
const o = this._value;
this._value = value;
if (o !== value || opts?.force) {
var parent: TimeZoneWidget | undefined = this.parent;
if (!parent || !parent.getTimeZone) {
this._value.tz(this._defaultTzone);
}
this.rerender();
}
return this;
}
/**
* @returns the value that is assigned to this component.
*/
getValue(): DateImpl | undefined {
return this._value;
}
/**
* Sets default time zone that this calendar belongs to.
* @param defaultTzone - the time zone's ID, such as "America/Los_Angeles".
*/
setDefaultTzone(defaultTzone: string): this {
this._defaultTzone = defaultTzone;
return this;
}
/**
* @returns default time zone that this calendar belongs to, such as "America/Los_Angeles".
*/
getDefaultTzone(): string | undefined {
return this._defaultTzone;
}
/**
* Set the date limit for this component with yyyyMMdd format,
* such as 20100101 is mean Jan 01 2010
*
* <dl>
* <dt>Example:</dt>
* <dd>between 20091201 and 20091231</dd>
* <dd>before 20091201</dd>
* <dd>after 20091231</dd>
* </dl>
*
*/
setConstraint(constraint: string | undefined, opts?: Record<string, boolean>): this {
const o = this._constraint;
this._constraint = constraint;
if (o !== constraint || opts?.force) {
this._fixConstraint();
// ZK-3619, this method could be called when datebox opening the calendar,
// inServer means there is a calendar tag in zul file.
if (this.desktop && this.inServer) {
this.rerender();
}
}
return this;
}
/**
* @returns the constraint of this component.
*/
getConstraint(): string | undefined {
return this._constraint;
}
/**
* Sets the name of this component.
* <p>The name is used only to work with "legacy" Web application that
* handles user's request by servlets.
* It works only with HTTP/HTML-based browsers. It doesn't work
* with other kind of clients.
* <p>Don't use this method if your application is purely based
* on ZK's event-driven model.
*
* @param name - the name of this component.
*/
setName(name: string, opts?: Record<string, boolean>): this {
const o = this._name;
this._name = name;
if (o !== name || opts?.force) {
if (this.efield)
this.efield.name = this._name;
}
return this;
}
/**
* @returns the name of this component.
* <p>The name is used only to work with "legacy" Web application that
* handles user's request by servlets.
* It works only with HTTP/HTML-based browsers. It doesn't work
* with other kind of clients.
* <p>Don't use this method if your application is purely based
* on ZK's event-driven model.
* @defaultValue `null`.
*/
getName(): string | undefined {
return this._name;
}
/**
* Sets whether enable to show the week number within the current year or
* not. [ZK EE]
* @since 6.5.0
*/
setWeekOfYear(weekOfYear: boolean, opts?: Record<string, boolean>): this {
const o = this._weekOfYear;
this._weekOfYear = weekOfYear;
if (o !== weekOfYear || opts?.force) {
if (this.desktop && zk.feature.ee)
this.rerender();
}
return this;
}
/**
* @returns whether enable to show the week number within the current year or not.
* @defaultValue `false`
* @since 6.5.0
*/
isWeekOfYear(): boolean {
return !!this._weekOfYear;
}
/**
* Sets whether enable to show the link that jump to today in day view
* @since 8.0.0
*/
setShowTodayLink(showTodayLink: boolean, opts?: Record<string, boolean>): this {
const o = this._showTodayLink;
this._showTodayLink = showTodayLink;
if (o !== showTodayLink || opts?.force) {
this.rerender();
}
return this;
}
/**
* @returns whether enable to show the link that jump to today in day view
* @defaultValue `false`
* @since 8.0.0
*/
isShowTodayLink(): boolean {
return !!this._showTodayLink;
}
/**
* Sets the label of the link that jump to today in day view
* @since 8.0.4
*/
setTodayLinkLabel(todayLinkLabel: string, opts?: Record<string, boolean>): this {
const o = this._todayLinkLabel;
this._todayLinkLabel = todayLinkLabel;
if (o !== todayLinkLabel || opts?.force) {
this.rerender();
}
return this;
}
/**
* @returns the label of the link that jump to today in day view
* @since 8.0.4
*/
getTodayLinkLabel(): string | undefined {
return this._todayLinkLabel;
}
/**
* A method for component getter symmetry, it will call getValue
* @since 10.0.0
*/
getValueInZonedDateTime(): DateImpl | undefined {
return this.getValue();
}
/**
* A method for component setter symmetry, it will call setValue
* @since 10.0.0
*/
setValueInZonedDateTime(valueInZonedDateTime: DateImpl, opts?: Record<string, boolean>): this {
return this.setValue(valueInZonedDateTime, opts);
}
/**
* A method for component getter symmetry, it will call getValue
* @since 10.0.0
*/
getValueInLocalDateTime(): DateImpl | undefined {
return this.getValue();
}
/**
* A method for component setter symmetry, it will call setValue
* @since 10.0.0
*/
setValueInLocalDateTime(valueInLocalDateTime: DateImpl, opts?: Record<string, boolean>): this {
return this.setValue(valueInLocalDateTime, opts);
}
/**
* A method for component getter symmetry, it will call getValue
* @since 10.0.0
*/
getValueInLocalDate(): DateImpl | undefined {
return this.getValue();
}
/**
* A method for component setter symmetry, it will call setValue
* @since 10.0.0
*/
setValueInLocalDate(valueInLocalDate: DateImpl, opts?: Record<string, boolean>): this {
return this.setValue(valueInLocalDate, opts);
}
/**
* A method for component getter symmetry, it will call getValue
* @since 10.0.0
*/
getValueInLocalTime(): DateImpl | undefined {
return this.getValue();
}
/**
* A method for component setter symmetry, it will call setValue
* @since 10.0.0
*/
setValueInLocalTime(valueInLocalTime: DateImpl, opts?: Record<string, boolean>): this {
return this.setValue(valueInLocalTime, opts);
}
override redraw(out: string[], skipper?: zk.Skipper): void {
Renderer.beforeRedraw(this);
super.redraw(out, skipper);
}
onChange(evt: zk.Event): void {
this._updFormData((evt.data as {value: DateImpl}).value);
}
/** @internal */
override doKeyDown_(evt: zk.Event): void {
const currentView = this._view;
if (evt.key === 'PageUp') {
// PageUp: prev month ; Shift + PageUp: prev year
this._setView(evt.shiftKey ? 'year' : 'month');
this._shift(-1);
} else if (evt.key === 'PageDown') {
// PageDown: next month ; Shift + PageDown: next year
this._setView(evt.shiftKey ? 'year' : 'month');
this._shift(1);
}
this._setView(currentView);
var keyCode = evt.keyCode,
ofs = keyCode == 37 ? -1 : keyCode == 39 ? 1 : keyCode == 38 ? -7 : keyCode == 40 ? 7 : 0;
if (ofs) {
this._shift(ofs);
evt.stop(); // Bug ZK-2306: using the arrow keys in the calendar widget scrolls the browser window
} else if (keyCode == 32 || keyCode == 13) {
// pass a fake event
this._clickDate({
target: this,
domTarget: jq(this.$n('mid')).find('.' + this.$s('selected'))[0],
stop: zk.$void
});
}
}
/** @internal */
setMinYear_(minYear: number): void {
if (minYear) {
var y = this.getTime().getFullYear();
this._minyear = minYear > y ? y : (minYear > 100 ? minYear : 100);
} else {
this._minyear = 1900;
}
}
/** @internal */
setMaxYear_(maxYear: number): void {
if (maxYear) {
var y = this.getTime().getFullYear();
this._maxyear = maxYear < y ? y : (maxYear > this._minyear ? maxYear : this._minyear);
} else {
this._maxyear = 2099;
}
}
/** @internal */
_shift(ofs: number, opts?: MarkCalOptions): void {
var oldTime = this.getTime(),
tz = _getTimeZone(this),
shiftTime = Dates.newInstance(oldTime.getTime(), tz),
minTime = Dates.newInstance([this._minyear, 0, 1, 0, 0, 0, 0], tz),
maxTime = Dates.newInstance([this._maxyear, 11, 31, 23, 59, 59, 999], tz),
today = zUtl.today(false, tz);
switch (this._view) {
case 'day':
shiftTime.setDate(oldTime.getDate() + ofs);
break;
case 'month':
if (ofs == 7)
ofs = 4;
else if (ofs == -7)
ofs = -4;
shiftTime.setMonth(oldTime.getMonth() + ofs);
break;
case 'year':
if (ofs == 7)
ofs = 4;
else if (ofs == -7)
ofs = -4;
shiftTime.setYear(oldTime.getFullYear() + ofs);
break;
case 'decade':
if (ofs == 7)
ofs = 4;
else if (ofs == -7)
ofs = -4;
ofs *= 10;
shiftTime.setYear(oldTime.getFullYear() + ofs);
break;
}
//Bug B65-ZK-1804: Constraint the shifted time should not be out of range between _minyear and _maxyear
//Bug B96-ZK-4543: Calendar should respect the constraint while Month changing
if (shiftTime.getTime() < minTime.getTime() || shiftTime.getTime() > maxTime.getTime() ||
Renderer.disabled(this, shiftTime.getFullYear(), shiftTime.getMonth(), shiftTime.getDate(), today))
return; // out of range
this._shiftDate(this._view, ofs);
var newTime = this.getTime();
switch (this._view) {
case 'day':
if (oldTime.getYear() == newTime.getYear()
&& oldTime.getMonth() == newTime.getMonth()) {
opts = opts || {};
opts.sameMonth = true; //optimize
this._markCal(opts);
} else {
this.rerender(-1);
this.focus();
}
break;
case 'month':
if (oldTime.getYear() == newTime.getYear())
this._markCal(opts);
else {
this.rerender(-1);
this.focus();
}
break;
default:
this.rerender(-1);
this.focus();
}
}
/** @internal */
_fixConstraint(): void {
var constraint = this._constraint || '';
// ZK-4641: Datebox doesn't clean beginning and end at client when removing constraint
this._beg = undefined;
this._end = undefined;
if (typeof constraint != 'string' || constraint == '') return;
// B50-ZK-591: Datebox constraint combination yyyymmdd and
// no empty cause javascript error in zksandbox
var constraints = constraint.split(','),
format = 'yyyyMMdd',
len = format.length + 1,
tz = _getTimeZone(this);
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (var i = 0; i < constraints.length; i++) {
constraint = jq.trim(constraints[i]); //Bug ZK-1718: should trim whitespace
if (constraint.startsWith('between')) {
var j = constraint.indexOf('and', 7);
if (j < 0 && zk.debugJS)
zk.error('Unknown constraint: ' + constraint);
this._beg = new zk.fmt.Calendar(undefined, this._localizedSymbols).parseDate(constraint.substring(7, j), format, undefined, undefined, undefined, tz);
this._end = new zk.fmt.Calendar(undefined, this._localizedSymbols).parseDate(constraint.substring(j + 3, j + 3 + len), format, undefined, undefined, undefined, tz);
if (this._beg!.getTime() > this._end!.getTime()) {
var d = this._beg;
this._beg = this._end;
this._end = d;
}
this._beg!.setHours(0, 0, 0, 0);
this._end!.setHours(0, 0, 0, 0);
} else if (constraint.startsWith('before_') || constraint.startsWith('after_')) {
continue; //Constraint start with 'before_' and 'after_' means errorbox position, skip it
} else if (constraint.startsWith('before')) {
this._end = new zk.fmt.Calendar(undefined, this._localizedSymbols).parseDate(constraint.substring(6, 6 + len), format, undefined, undefined, undefined, tz);
this._end!.setHours(0, 0, 0, 0);
} else if (constraint.startsWith('after')) {
this._beg = new zk.fmt.Calendar(undefined, this._localizedSymbols).parseDate(constraint.substring(5, 5 + len), format, undefined, undefined, undefined, tz);
this._beg!.setHours(0, 0, 0, 0);
}
}
}
/**
* @returns the format of this component.
*/
getFormat(): string {
return this._fmt || 'yyyy/MM/dd';
}
/** @internal */
_updFormData(formData: DateImpl): void {
let val = new zk.fmt.Calendar().formatDate(formData, this.getFormat(), this._localizedSymbols);
if (this._name) {
val = val || '';
if (!this.efield)
this.efield = jq.newHidden(this._name, val, this.$n());
else
this.efield.value = val;
}
}
/** @internal */
override focus_(timeout: number): boolean {
if (this._view != 'decade')
this._markCal({timeout: timeout});
else {
var anc: HTMLAnchorElement | undefined;
if (anc = this.getAnchor_())
this._doFocus(anc, true);
}
return true;
}
/** @internal */
override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
super.bind_(desktop, skipper, after);
var node = this.$n(),
title = this.$n('title'),
mid = this.$n('mid'),
left = this.$n('left'),
right = this.$n('right'),
today = this.$n('today');
if (this._view != 'decade')
this._markCal({silent: true});
this.domListen_(title!, 'onClick', '_changeView')
.domListen_(mid!, 'onClick', '_clickDate')
.domListen_(left!, 'onClick', '_clickArrow')
.domListen_(right!, 'onClick', '_clickArrow')
.domListen_(today!, 'onClick', '_clickToday')
.domListen_(node!, 'onMousewheel');
this._updFormData(this.getTime());
}
/** @internal */
override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
var node = this.$n(),
title = this.$n('title'),
mid = this.$n('mid'),
left = this.$n('left'),
right = this.$n('right'),
today = this.$n('today');
this.domUnlisten_(title!, 'onClick', '_changeView')
.domUnlisten_(mid!, 'onClick', '_clickDate')
.domUnlisten_(left!, 'onClick', '_clickArrow')
.domUnlisten_(right!, 'onClick', '_clickArrow')
.domUnlisten_(today!, 'onClick', '_clickToday')
.domUnlisten_(node!, 'onMousewheel');
super.unbind_(skipper, after, keepRod);
this.efield = undefined;
}
override rerender(skipper?: zk.Skipper | number): this {
if (this.desktop) {
var s = this.$n()!.style,
w = s.width,
h = s.height,
result = super.rerender(skipper);
s = this.$n()!.style;
s.width = w;
s.height = h;
return result;
}
return this;
}
/** @internal */
_clickArrow(evt: zk.Event): void {
if (zk.animating()) return; // ignore
var node = jq.nodeName(evt.domTarget, 'a') ? evt.domTarget
: jq(evt.domTarget).parent('a')[0];
if (jq(node).attr('disabled'))
return;
this._shiftView(jq(node).hasClass(this.$s('left')) ? -1 : 1);
//ZK-2679: prevent default behavior of clicking anchor
evt.stop();
}
/** @internal */
_clickToday(): void {
this.setValue(zUtl.today(!!this.parent, _getTimeZone(this)));
this._setView('day');
}
/** @internal */
_shiftView(ofs: number, disableAnima?: boolean): void {
switch (this._view) {
case 'day':
this._shiftDate('month', ofs);
break;
case 'month':
this._shiftDate('year', ofs);
break;
case 'year':
this._shiftDate('year', ofs * 10);
break;
case 'decade':
this._shiftDate('year', ofs * 100);
break;
}
if (!disableAnima)
this._setView(this._view, ofs);
else {
this.rerender(-1);
this.focus();
}
}
/** @internal */
_doMousewheel(evt: zk.Event, intDelta: number): void {
if (jq(this.$n(-intDelta > 0 ? 'right' : 'left')).attr('disabled'))
return;
this._shiftView(intDelta > 0 ? -1 : 1, true);
evt.stop();
}
/**
* @returns the Date that is assigned to this component.
* <p>returns today if value is null
*/
getTime(): DateImpl {
return this._value || zUtl.today(this.getFormat(), _getTimeZone(this));
}
/** @internal */
_setTime(y: number | undefined, m?: number, d?: number, fireOnChange?: boolean): this {
var dateobj = this.getTime(),
year = y != null ? y : dateobj.getFullYear(),
month = m != null ? m : dateobj.getMonth(),
day = d != null ? d : dateobj.getDate(),
tz = _getTimeZone(this),
val = new zk.fmt.Calendar().escapeDSTConflict(_newDate(year, month, day, d == null, tz), tz); // B70-ZK-2382
this._value = val;
this._selectedValue = val;
if (fireOnChange)
this.fire('onChange', {value: val});
return this;
}
// calendar-ctrl.js will override this function
/** @internal */
_clickDate(evt: Pick<zk.Event, 'target' | 'domTarget' | 'stop'>): void {
var target = evt.domTarget as HTMLTableCellElement | undefined,
val: number;
for (; target; target = target.parentNode as HTMLTableCellElement | undefined)
try { //Note: data-dt is also used in mold/calendar.js
if ((val = jq(target).data('value') as number) !== undefined) {
val = zk.parseInt(val);
break;
}
} catch (e) {
continue; //skip
}
this._chooseDate(target, val!);
var anc: HTMLAnchorElement | undefined;
if (anc = this.getAnchor_())
this._doFocus(anc, true);
evt.stop();
}
/** @internal */
_chooseDate(target: HTMLTableCellElement | undefined, val: number): void {
if (target && !jq(target).hasClass(this.$s('disabled'))) {
var cell = target,
dateobj = this.getTime();
switch (this._view) {
case 'day':
var oldTime = this.getTime();
this._setTime(undefined, cell._monofs != null && cell._monofs != 0 ?
dateobj.getMonth() + cell._monofs : undefined, val, true /*fire onChange */);
var newTime = this.getTime();
if (oldTime.getYear() == newTime.getYear()
&& oldTime.getMonth() == newTime.getMonth()) {
this._markCal({sameMonth: true}); // optimize
} else {
this.rerender(-1);
this.focus();
}
break;
case 'month':
this._setTime(undefined, val);
this._setView('day');
break;
case 'year':
this._setTime(val);
this._setView('month');
break;
case 'decade':
//Decade mode Set Year Also
this._setTime(val);
this._setView('year');
break;
}
}
}
/** @internal */
_shiftDate(opt: string, ofs: number, ignoreUpdate?: boolean): DateImpl {
var dateobj = this.getTime(),
year = dateobj.getFullYear(),
month = dateobj.getMonth(),
day = dateobj.getDate(),
tz = _getTimeZone(this),
nofix;
switch (opt) {
case 'day':
day += ofs;
nofix = true;
break;
case 'month':
month += ofs;
break;
case 'year':
year += ofs;
break;
case 'decade':
year += ofs;
break;
}
var newTime = _newDate(year, month, day, !nofix, tz);
if (!ignoreUpdate) {
this._value = newTime;
if (!Renderer.disabled(this, year, month, day, zUtl.today(false, this._defaultTzone))) {
this._selectedValue = newTime;
this.fire('onChange', {value: this._selectedValue, shallClose: false, shiftView: true});
}
}
return newTime;
}
/** @internal */
_changeView(evt: zk.Event): void {
var tm = this.$n('tm'),
ty = this.$n('ty'),
tyd = this.$n('tyd'),
title = this.$n('title');
if (evt.domTarget == tm)
this._setView('month');
else if (evt.domTarget == ty)
this._setView('year');
else if (evt.domTarget == tyd)
this._setView('decade');
else if (evt.domTarget == title) {
if (tm == null && ty != null)
this._setView('year');
else if (ty == null)
this._setView('decade');
else
this._setView('month');
}
evt.stop();
}
/** @internal */
_setView(view: string, force?: number): this {
// check whether to disable the arrow
/** @internal */
function _updateArrow(wgt: zul.db.Calendar): void {
if (wgt.isOutOfRange(true)) {
jq(wgt.$n('left')).attr('disabled', 'disabled');
} else {
jq(wgt.$n('left')).removeAttr('disabled');
}
if (wgt.isOutOfRange()) {
jq(wgt.$n('right')).attr('disabled', 'disabled');
} else {
jq(wgt.$n('right')).removeAttr('disabled');
}
}
type ComputedView = (wgt: zul.db.Calendar, out: string[], localizedSymbols: zk.LocalizedSymbols) => void;
if (this._view != view) {
this._view = view;
var out = new zk.Buffer(),
localizedSymbols = this.getLocalizedSymbols();
(Renderer[view + 'View'] as ComputedView)(this, out, localizedSymbols);
jq(this.$n('mid')).after(out.join('')).remove();
var after = [];
// unlisten event
this.unbind_(undefined, after);
// listen event
this.bind_(this.desktop, undefined, after);
out = []; // reset
Renderer.titleHTML(this, out, localizedSymbols);
// eslint-disable-next-line @microsoft/sdl/no-html-method
jq(this.$n('title')).html(DOMPurify.sanitize(out.join('')));
jq(this.$n('mid')).transition({scale: 0}, 0).transition({scale: 1}, this.animationSpeed_() as number);
_updateArrow(this);
var anc = this.getAnchor_();
if (anc)
this._doFocus(anc, true);
} else if (force) {
var out: string[] = [],
localizedSymbols = this.getLocalizedSymbols(),
oldMid = this.$n('mid')!,
isLeft = force == -1,
width = oldMid.offsetWidth,
x = width * -1,
self = this,
animaCSSHTML = this.$s('anima'),
todayBtn = this.isShowTodayLink() ? jq(this.$n('today')).parent() : undefined;
if (todayBtn) todayBtn.is(':hidden') && todayBtn.css('display', 'none');
(Renderer[view + 'View'] as ComputedView)(this, out, localizedSymbols);
jq(oldMid).after(/*safe*/ '<div style="height:' + jq.px(oldMid.offsetHeight)
+ ';width:' + jq.px(width) + '" class="' + animaCSSHTML
+ '"><div class="' + animaCSSHTML + '-inner"></div');
var animaInner = oldMid.nextSibling!.firstChild!;
jq(animaInner).append(oldMid);
oldMid = animaInner.firstChild as HTMLElement;
if (isLeft) {
jq(oldMid).before(out.join('')).remove();
} else {
jq(oldMid).after(out.join('')).remove();
}
// clear for _makrCal to get the latest reference
this.clearCache();
if (view != 'decade')
this._markCal();
var newMid: HTMLElement;
if (isLeft) {
jq(animaInner.firstChild!).after(oldMid);
newMid = oldMid.previousSibling as HTMLElement;
jq(animaInner).css({left: x});
x = 0;
} else {
jq(animaInner.firstChild!).before(oldMid);
newMid = oldMid.nextSibling as HTMLElement;
}
jq(animaInner).animate({left: x}, {
duration: this.animationSpeed_(),
always: function (/*callback*/) {
self.domUnlisten_(oldMid, 'onClick', '_clickDate');
jq(animaInner.parentNode!).after(newMid).remove();
self.domListen_(newMid, 'onClick', '_clickDate');
var out = []; // reset
Renderer.titleHTML(self, out, localizedSymbols);
// eslint-disable-next-line @microsoft/sdl/no-html-method
jq(self.$n('title')).html(DOMPurify.sanitize(out.join('')));
self.clearCache();
if (todayBtn) todayBtn.css('display', '');
}
});
_updateArrow(this);
}
return this;
}
getLocalizedSymbols(): zk.LocalizedSymbols {
return this._localizedSymbols || {
DOW_1ST: zk.DOW_1ST,
MINDAYS: zk.MINDAYS,
ERA: zk.ERA,
YDELTA: zk.YDELTA,
LAN_TAG: zk.LAN_TAG,
SDOW: zk.SDOW,
S2DOW: zk.S2DOW,
FDOW: zk.FDOW,
SMON: zk.SMON,
S2MON: zk.S2MON,
FMON: zk.FMON,
APM: zk.APM
};
}
/**
* Check whether the date is out of range between 1900~2100 years
* @param left - it is used for the left arrow button
* @param date - the date object for the range if null, the current value
* of {@link getTime} is assumed.
* @returns if true it means the date is out of range.
* @since 6.5.3
*/
isOutOfRange(left?: boolean, date?: Date): boolean {
var view = this._view,
val = date || this.getTime(),
y = val.getFullYear(),
yofs = y - (y % 10 + 1),
ydec = zk.parseInt(y / 100),
minyear = this._minyear,
maxyear = this._maxyear,
mincen = zk.parseInt(minyear / 100) * 100,
maxcen = zk.parseInt(maxyear / 100) * 100;
if (view == 'decade') {
var value = ydec * 100;
return left ? value == mincen : value == maxcen;
} else if (view == 'year') {
var value = yofs;
return left ? value < minyear : value + 10 >= maxyear;
} else if (view == 'day') {
var value = y,
m = val.getMonth();
return left ? value <= minyear && m == 0 : value >= maxyear && m == 11;
} else {
var value = y;
return left ? value <= minyear : value >= maxyear;
}
}
/** @internal */
_markCal(opts?: MarkCalOptions): void {
this._markCal0(opts);
var anc: HTMLAnchorElement | undefined;
if ((anc = this.getAnchor_()) && (!opts || !opts.silent))
this._doFocus(anc, opts && opts.timeout);
}
// calendar-ctrl.js will override this function
/** @internal */
_markCal0(opts?: MarkCalOptions): void {
var seldate = this.getTime(),
m = seldate.getMonth(),
mid = this.$n('mid')!,
$mid = jq(mid),
seldClass = this.$s('selected'),
y = seldate.getFullYear(),
minyear = this._minyear,
maxyear = this._maxyear;
if (this._view == 'day') {
//B70-ZK-2477, if zul declares the locale, don't use system's locale
var DOW_1ST = zk.DOW_1ST;
if (this._localizedSymbols && this._localizedSymbols.DOW_1ST != undefined) {
DOW_1ST = this._localizedSymbols.DOW_1ST;
}
var d = seldate.getDate(),
tz = seldate.getTimeZone(),
v = Dates.newInstance([y, m, 1], 'UTC').getDay() - DOW_1ST,
last = Dates.newInstance([y, m + 1, 0], 'UTC').getDate(), //last date of this month
prev = Dates.newInstance([y, m, 0], 'UTC').getDate(), //last date of previous month
today = zUtl.today(false, tz), //no time part
outsideClass = this.$s('outside'),
disdClass = this.$s('disabled');
$mid.find('.' + seldClass).removeClass(seldClass);
if (!opts || !opts.sameMonth) {
$mid.find('.' + outsideClass).removeClass(outsideClass);
$mid.find('.' + disdClass).removeClass(disdClass);
}
if (v < 0) v += 7;
for (var j = 0, cur = -v + 1; j < 6; ++j) {
var week = this.$n<HTMLTableRowElement>('w' + j);
if (week != null) {
for (var k = 0; k < 7; ++k, ++cur) {
v = cur <= 0 ? prev + cur : cur <= last ? cur : cur - last;
if (k == 0 && cur > last)
week.style.display = 'none';
else {
if (k == 0) week.style.display = '';
var monofs = cur <= 0 ? -1 : cur <= last ? 0 : 1,
bSel = cur == d;
// Bug B65-ZK-1804: check whether the date is out of range
if (y >= maxyear && m == 11 && monofs == 1
|| y <= minyear && m == 0 && monofs == -1)
continue;
var $cell = jq(week.cells[k]),
isSelectDisabled = Renderer.disabled(this, y, m + monofs, v, today);
$cell[0]._monofs = monofs;
if (bSel && !isSelectDisabled) {
$cell.addClass(seldClass);
}
//not same month
if (!opts || !opts.sameMonth) { // optimize
if (monofs) {
$cell.addClass(outsideClass);
}
if (isSelectDisabled) {
$cell.addClass(disdClass);
}
// eslint-disable-next-line @microsoft/sdl/no-inner-html
$cell[0].innerHTML = DOMPurify.sanitize(Renderer.cellHTML(this, y, m + monofs, v, monofs)) as unknown as string;
$cell[0].setAttribute('aria-label', Renderer.cellAriaLabel(this, y, m + monofs, v, monofs, k));
$cell.data('value', v);
}
}
}
}
}
} else {
var isMon = this._view == 'month',
field = isMon ? 'm' : 'y',
index = isMon ? m : y % 10 + 1,
node: HTMLElement | undefined;
$mid.find('.' + seldClass).removeClass(seldClass);
for (var j = 0; j < 12; ++j)
if (index == j && (node = this.$n(field + j)))
jq(node).addClass(seldClass);
}
}
/** @internal */
override domClass_(no?: zk.DomClassOptions): string {
var cls = '';
if (this._weekOfYear)
cls += this.$s('wk') + ' ';
return cls + super.domClass_(no);
}
/** @internal */
animationSpeed_(): 'slow' | 'fast' | number {
return zk(this).getAnimationSpeed();
}
/** @internal */
getAnchor_(): HTMLAnchorElement | undefined {
return this.$n('a');
}
// Bug 2936994, fixed unnecessary setting scrollTop
/** @internal */
_doFocus(n: HTMLElement, timeout?: number | boolean): void {
if (zk.gecko && timeout)
setTimeout(() => zk(n).focus()); // FIXME: missing timeout argument?
else
zk(n).focus();
}
}