mn-angular-lib 0.0.52 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mn-angular-lib.mjs +1270 -2
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mn-angular-lib.d.ts +694 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewContainerRef, ViewChild, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
|
-
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, map, catchError
|
|
4
|
+
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
|
7
7
|
import { tv } from 'tailwind-variants';
|
|
@@ -4861,6 +4861,1274 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4861
4861
|
|
|
4862
4862
|
// Types
|
|
4863
4863
|
|
|
4864
|
+
/**
|
|
4865
|
+
* Available calendar view modes.
|
|
4866
|
+
*/
|
|
4867
|
+
var CalendarView;
|
|
4868
|
+
(function (CalendarView) {
|
|
4869
|
+
CalendarView["MONTH"] = "MONTH";
|
|
4870
|
+
CalendarView["WEEK"] = "WEEK";
|
|
4871
|
+
CalendarView["DAY"] = "DAY";
|
|
4872
|
+
})(CalendarView || (CalendarView = {}));
|
|
4873
|
+
/**
|
|
4874
|
+
* Builds locale-derived day name arrays from a BCP 47 locale string.
|
|
4875
|
+
* Uses January 1 2024 (a Monday) as the reference date.
|
|
4876
|
+
*/
|
|
4877
|
+
function buildDayNames(locale) {
|
|
4878
|
+
const base = new Date(2024, 0, 1); // 2024-01-01 is a Monday
|
|
4879
|
+
const short = [];
|
|
4880
|
+
const long = [];
|
|
4881
|
+
for (let i = 0; i < 7; i++) {
|
|
4882
|
+
const d = new Date(base);
|
|
4883
|
+
d.setDate(base.getDate() + i);
|
|
4884
|
+
short.push(d.toLocaleDateString(locale, { weekday: 'short' }));
|
|
4885
|
+
long.push(d.toLocaleDateString(locale, { weekday: 'long' }));
|
|
4886
|
+
}
|
|
4887
|
+
return { short, long };
|
|
4888
|
+
}
|
|
4889
|
+
/** Default calendar configuration values. */
|
|
4890
|
+
const DEFAULT_CALENDAR_CONFIG = (() => {
|
|
4891
|
+
const locale = 'en-US';
|
|
4892
|
+
const names = buildDayNames(locale);
|
|
4893
|
+
return {
|
|
4894
|
+
startHour: 7,
|
|
4895
|
+
endHour: 22,
|
|
4896
|
+
locale,
|
|
4897
|
+
todayLabel: 'Today',
|
|
4898
|
+
upcomingEventsTitle: 'Upcoming events',
|
|
4899
|
+
viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
|
|
4900
|
+
shortDayNames: names.short,
|
|
4901
|
+
longDayNames: names.long,
|
|
4902
|
+
mobileBreakpoint: 768,
|
|
4903
|
+
};
|
|
4904
|
+
})();
|
|
4905
|
+
/**
|
|
4906
|
+
* Injection token for the resolved calendar configuration.
|
|
4907
|
+
*
|
|
4908
|
+
* Prefer using {@link MN_CALENDAR_CONFIG} with `provideMnComponentConfig`
|
|
4909
|
+
* so that settings can be managed via `mn-config.json5`. This token is
|
|
4910
|
+
* kept for backward compatibility and manual `providers` usage.
|
|
4911
|
+
*
|
|
4912
|
+
* @example
|
|
4913
|
+
* ```ts
|
|
4914
|
+
* providers: [
|
|
4915
|
+
* { provide: CALENDAR_CONFIG, useValue: { startHour: 8, endHour: 20, locale: 'nl-NL' } }
|
|
4916
|
+
* ]
|
|
4917
|
+
* ```
|
|
4918
|
+
*/
|
|
4919
|
+
const CALENDAR_CONFIG = new InjectionToken('CalendarConfig', {
|
|
4920
|
+
providedIn: 'root',
|
|
4921
|
+
factory: () => DEFAULT_CALENDAR_CONFIG
|
|
4922
|
+
});
|
|
4923
|
+
/**
|
|
4924
|
+
* Injection token resolved via `MnConfigService` (the `mn-config.json5` system).
|
|
4925
|
+
*
|
|
4926
|
+
* Use the helper {@link provideMnCalendarConfig} in the component's `providers`
|
|
4927
|
+
* array so that calendar settings are read from the config file and support
|
|
4928
|
+
* `$translate` markers, section scoping, and instance-id overrides.
|
|
4929
|
+
*
|
|
4930
|
+
* Component name in the config file: `'mn-calendar'`.
|
|
4931
|
+
*
|
|
4932
|
+
* @example
|
|
4933
|
+
* ```json5
|
|
4934
|
+
* // mn-config.json5
|
|
4935
|
+
* {
|
|
4936
|
+
* defaults: {
|
|
4937
|
+
* "mn-calendar": {
|
|
4938
|
+
* startHour: 8,
|
|
4939
|
+
* endHour: 20,
|
|
4940
|
+
* locale: "nl-NL",
|
|
4941
|
+
* todayLabel: { $translate: "calendar.today" }
|
|
4942
|
+
* }
|
|
4943
|
+
* }
|
|
4944
|
+
* }
|
|
4945
|
+
* ```
|
|
4946
|
+
*/
|
|
4947
|
+
const MN_CALENDAR_CONFIG = new InjectionToken('MN_CALENDAR_CONFIG');
|
|
4948
|
+
/** Component name used to look up calendar settings in `mn-config.json5`. */
|
|
4949
|
+
const MN_CALENDAR_COMPONENT_NAME = 'mn-calendar';
|
|
4950
|
+
/**
|
|
4951
|
+
* Provider helper that wires the calendar into the `mn-config` system.
|
|
4952
|
+
*
|
|
4953
|
+
* Add this to the `providers` array of the component (or module) that hosts
|
|
4954
|
+
* `<app-calendar-view>`. It reads defaults and overrides from `mn-config.json5`
|
|
4955
|
+
* under the key `"mn-calendar"` and provides them via {@link MN_CALENDAR_CONFIG}.
|
|
4956
|
+
*
|
|
4957
|
+
* @param initial — optional partial defaults merged before config-file values.
|
|
4958
|
+
*/
|
|
4959
|
+
function provideMnCalendarConfig(initial) {
|
|
4960
|
+
return provideMnComponentConfig(MN_CALENDAR_CONFIG, MN_CALENDAR_COMPONENT_NAME, initial);
|
|
4961
|
+
}
|
|
4962
|
+
/**
|
|
4963
|
+
* Merges a partial config with defaults, re-deriving day names from locale when needed.
|
|
4964
|
+
*/
|
|
4965
|
+
function resolveCalendarConfig(partial) {
|
|
4966
|
+
if (!partial)
|
|
4967
|
+
return { ...DEFAULT_CALENDAR_CONFIG };
|
|
4968
|
+
const locale = partial.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
|
|
4969
|
+
const names = buildDayNames(locale);
|
|
4970
|
+
return {
|
|
4971
|
+
...DEFAULT_CALENDAR_CONFIG,
|
|
4972
|
+
...partial,
|
|
4973
|
+
locale,
|
|
4974
|
+
shortDayNames: partial.shortDayNames ?? names.short,
|
|
4975
|
+
longDayNames: partial.longDayNames ?? names.long,
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
/**
|
|
4980
|
+
* Injection token for the calendar date formatter.
|
|
4981
|
+
*
|
|
4982
|
+
* @example
|
|
4983
|
+
* ```ts
|
|
4984
|
+
* providers: [
|
|
4985
|
+
* { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
|
|
4986
|
+
* ]
|
|
4987
|
+
* ```
|
|
4988
|
+
*/
|
|
4989
|
+
const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
|
|
4990
|
+
|
|
4991
|
+
/**
|
|
4992
|
+
* Default implementation of {@link CalendarDateFormatter} that uses the
|
|
4993
|
+
* browser's `Intl.DateTimeFormat` API for locale-aware formatting.
|
|
4994
|
+
*
|
|
4995
|
+
* The locale is read from the injected {@link CALENDAR_CONFIG}. If no config
|
|
4996
|
+
* is provided, `'en-US'` is used as the fallback.
|
|
4997
|
+
*
|
|
4998
|
+
* This service has no dependency on `@ngx-translate` or any other i18n library,
|
|
4999
|
+
* so the calendar library works out of the box. Consumers can replace it with
|
|
5000
|
+
* their own implementation via the `CALENDAR_DATE_FORMATTER` injection token.
|
|
5001
|
+
*/
|
|
5002
|
+
class DefaultCalendarDateFormatter {
|
|
5003
|
+
locale;
|
|
5004
|
+
constructor(config) {
|
|
5005
|
+
this.locale = config?.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
|
|
5006
|
+
}
|
|
5007
|
+
/** Formats an hour and minute pair into a locale time string (e.g. "09:00 AM"). */
|
|
5008
|
+
formatTimeI(hour, minute) {
|
|
5009
|
+
const date = new Date();
|
|
5010
|
+
date.setHours(hour, minute, 0, 0);
|
|
5011
|
+
return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
|
|
5012
|
+
}
|
|
5013
|
+
/** Formats the time portion of a Date (e.g. "2:30 PM"). Returns empty string for undefined. */
|
|
5014
|
+
formatTime(date) {
|
|
5015
|
+
if (!date)
|
|
5016
|
+
return Promise.resolve('');
|
|
5017
|
+
return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
|
|
5018
|
+
}
|
|
5019
|
+
/** Formats a Date as a full date-time string (e.g. "May 15, 2026, 02:30 PM"). */
|
|
5020
|
+
formatDateTime(date) {
|
|
5021
|
+
return of(date.toLocaleString(this.locale, {
|
|
5022
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
5023
|
+
hour: '2-digit', minute: '2-digit'
|
|
5024
|
+
}));
|
|
5025
|
+
}
|
|
5026
|
+
/** Formats a Date as a date-only string (e.g. "May 15, 2026"). */
|
|
5027
|
+
formatDate(date) {
|
|
5028
|
+
return of(date.toLocaleDateString(this.locale, {
|
|
5029
|
+
year: 'numeric', month: 'short', day: 'numeric'
|
|
5030
|
+
}));
|
|
5031
|
+
}
|
|
5032
|
+
/** Formats a Date as `YYYY-MM-DD` for use in `<input type="date">` controls. */
|
|
5033
|
+
formatDateForFormControl(date) {
|
|
5034
|
+
const y = date.getFullYear();
|
|
5035
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
5036
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
5037
|
+
return `${y}-${m}-${d}`;
|
|
5038
|
+
}
|
|
5039
|
+
/** Returns `true` if both dates fall on the same calendar day. */
|
|
5040
|
+
isSameDay(date1, date2) {
|
|
5041
|
+
return date1.getFullYear() === date2.getFullYear()
|
|
5042
|
+
&& date1.getMonth() === date2.getMonth()
|
|
5043
|
+
&& date1.getDate() === date2.getDate();
|
|
5044
|
+
}
|
|
5045
|
+
/** Formats a Date as "Month Year" (e.g. "January 2026"). */
|
|
5046
|
+
formatMonthName(date) {
|
|
5047
|
+
return Promise.resolve(date.toLocaleString(this.locale, { month: 'long', year: 'numeric' }));
|
|
5048
|
+
}
|
|
5049
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, deps: [{ token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5050
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter });
|
|
5051
|
+
}
|
|
5052
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, decorators: [{
|
|
5053
|
+
type: Injectable
|
|
5054
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5055
|
+
type: Optional
|
|
5056
|
+
}, {
|
|
5057
|
+
type: Inject,
|
|
5058
|
+
args: [CALENDAR_CONFIG]
|
|
5059
|
+
}] }] });
|
|
5060
|
+
|
|
5061
|
+
/**
|
|
5062
|
+
* Month grid view showing a 7×6 grid of day cells.
|
|
5063
|
+
*
|
|
5064
|
+
* Each cell displays the day number and up to 3 coloured dots representing
|
|
5065
|
+
* events on that day. Clicking a cell emits `dayClicked`.
|
|
5066
|
+
*/
|
|
5067
|
+
class CalendarMonthComponent {
|
|
5068
|
+
/** The date whose month is displayed. */
|
|
5069
|
+
focusDay;
|
|
5070
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5071
|
+
eventsChanged;
|
|
5072
|
+
/** Observable that emits when the focus day changes. */
|
|
5073
|
+
focusDayChanged;
|
|
5074
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5075
|
+
config;
|
|
5076
|
+
/** Emits the date of a clicked day cell. */
|
|
5077
|
+
dayClicked = new EventEmitter();
|
|
5078
|
+
monthItems = [];
|
|
5079
|
+
longDayNames;
|
|
5080
|
+
events = [];
|
|
5081
|
+
destroy$ = new Subject();
|
|
5082
|
+
formatter;
|
|
5083
|
+
constructor() {
|
|
5084
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5085
|
+
this.longDayNames = DEFAULT_CALENDAR_CONFIG.longDayNames;
|
|
5086
|
+
}
|
|
5087
|
+
ngOnInit() {
|
|
5088
|
+
const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5089
|
+
this.longDayNames = resolved.longDayNames;
|
|
5090
|
+
this.buildMonth();
|
|
5091
|
+
if (this.eventsChanged) {
|
|
5092
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5093
|
+
this.events = events;
|
|
5094
|
+
this.buildMonth();
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
if (this.focusDayChanged) {
|
|
5098
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5099
|
+
this.focusDay = date;
|
|
5100
|
+
this.buildMonth();
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
ngOnDestroy() {
|
|
5105
|
+
this.destroy$.next();
|
|
5106
|
+
this.destroy$.complete();
|
|
5107
|
+
}
|
|
5108
|
+
/** Emits the clicked day's date. */
|
|
5109
|
+
onDayClick(date) {
|
|
5110
|
+
this.dayClicked.emit(date);
|
|
5111
|
+
}
|
|
5112
|
+
/** trackBy for day name headers. */
|
|
5113
|
+
trackByDayName(index) {
|
|
5114
|
+
return index;
|
|
5115
|
+
}
|
|
5116
|
+
/** trackBy for month grid cells. */
|
|
5117
|
+
trackByMonthItem(_index, item) {
|
|
5118
|
+
return item.date.getTime();
|
|
5119
|
+
}
|
|
5120
|
+
/** trackBy for event dots. */
|
|
5121
|
+
trackByEventDot(_index, event) {
|
|
5122
|
+
return event.id;
|
|
5123
|
+
}
|
|
5124
|
+
/** Builds the 42-cell month grid (6 rows × 7 columns). */
|
|
5125
|
+
buildMonth() {
|
|
5126
|
+
if (!this.focusDay)
|
|
5127
|
+
return;
|
|
5128
|
+
const year = this.focusDay.getFullYear();
|
|
5129
|
+
const month = this.focusDay.getMonth();
|
|
5130
|
+
const firstDay = new Date(year, month, 1);
|
|
5131
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
5132
|
+
let startOffset = firstDay.getDay() - 1;
|
|
5133
|
+
if (startOffset < 0)
|
|
5134
|
+
startOffset = 6;
|
|
5135
|
+
const today = new Date();
|
|
5136
|
+
this.monthItems = [];
|
|
5137
|
+
for (let i = startOffset - 1; i >= 0; i--) {
|
|
5138
|
+
const date = new Date(year, month, -i);
|
|
5139
|
+
this.monthItems.push(this.createMonthItem(date, false, today));
|
|
5140
|
+
}
|
|
5141
|
+
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
5142
|
+
const date = new Date(year, month, d);
|
|
5143
|
+
this.monthItems.push(this.createMonthItem(date, true, today));
|
|
5144
|
+
}
|
|
5145
|
+
const remaining = 42 - this.monthItems.length;
|
|
5146
|
+
for (let i = 1; i <= remaining; i++) {
|
|
5147
|
+
const date = new Date(year, month + 1, i);
|
|
5148
|
+
this.monthItems.push(this.createMonthItem(date, false, today));
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
createMonthItem(date, isCurrentMonth, today) {
|
|
5152
|
+
const isToday = this.formatter.isSameDay(date, today);
|
|
5153
|
+
const dayEvents = this.events.filter(e => this.formatter.isSameDay(e.startTime, date) ||
|
|
5154
|
+
this.formatter.isSameDay(e.endTime, date) ||
|
|
5155
|
+
(e.startTime < date && e.endTime > date));
|
|
5156
|
+
return {
|
|
5157
|
+
date,
|
|
5158
|
+
dayNumber: date.getDate(),
|
|
5159
|
+
isCurrentMonth,
|
|
5160
|
+
isToday,
|
|
5161
|
+
events: dayEvents
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5165
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "app-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
5166
|
+
}
|
|
5167
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
|
|
5168
|
+
type: Component,
|
|
5169
|
+
args: [{ selector: 'app-calendar-month', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"] }]
|
|
5170
|
+
}], ctorParameters: () => [], propDecorators: { focusDay: [{
|
|
5171
|
+
type: Input
|
|
5172
|
+
}], eventsChanged: [{
|
|
5173
|
+
type: Input
|
|
5174
|
+
}], focusDayChanged: [{
|
|
5175
|
+
type: Input
|
|
5176
|
+
}], config: [{
|
|
5177
|
+
type: Input
|
|
5178
|
+
}], dayClicked: [{
|
|
5179
|
+
type: Output
|
|
5180
|
+
}] } });
|
|
5181
|
+
|
|
5182
|
+
/**
|
|
5183
|
+
* Service that computes the visual layout of calendar events within a
|
|
5184
|
+
* time-grid (week or day view).
|
|
5185
|
+
*
|
|
5186
|
+
* Responsibilities:
|
|
5187
|
+
* - Splitting multi-day events into per-day segments.
|
|
5188
|
+
* - Assigning non-overlapping column indices to concurrent events.
|
|
5189
|
+
* - Computing the width (column span) each event should occupy.
|
|
5190
|
+
*
|
|
5191
|
+
* This service is stateless — all state is passed via method parameters.
|
|
5192
|
+
* Provide it per-component (not root) so each view gets its own instance.
|
|
5193
|
+
*/
|
|
5194
|
+
class CalendarEventLayoutService {
|
|
5195
|
+
/**
|
|
5196
|
+
* Returns `true` when two time ranges overlap (exclusive boundaries).
|
|
5197
|
+
*/
|
|
5198
|
+
eventsOverlap(startA, endA, startB, endB) {
|
|
5199
|
+
return startA < endB && startB < endA;
|
|
5200
|
+
}
|
|
5201
|
+
/**
|
|
5202
|
+
* Returns all events whose time range overlaps the given `[start, end)` window.
|
|
5203
|
+
*/
|
|
5204
|
+
getAllEventsOnSpecificTime(events, start, end) {
|
|
5205
|
+
return events.filter(e => this.eventsOverlap(e.startTime, e.endTime, start, end));
|
|
5206
|
+
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Splits multi-day events into per-day segments that fit within the
|
|
5209
|
+
* visible hour range (`startHour`–`endHour`) and date range.
|
|
5210
|
+
*
|
|
5211
|
+
* Single-day events are shallow-copied as-is. Multi-day events produce
|
|
5212
|
+
* one segment per day with `continued` / `continuedEnd` flags set.
|
|
5213
|
+
*/
|
|
5214
|
+
calculateMultiDayEvents(events, startHour, endHour, rangeStart, rangeEnd) {
|
|
5215
|
+
const result = [];
|
|
5216
|
+
for (const event of events) {
|
|
5217
|
+
const eventStart = new Date(event.startTime);
|
|
5218
|
+
const eventEnd = new Date(event.endTime);
|
|
5219
|
+
if (eventStart.toDateString() === eventEnd.toDateString()) {
|
|
5220
|
+
result.push({ ...event });
|
|
5221
|
+
continue;
|
|
5222
|
+
}
|
|
5223
|
+
const current = new Date(eventStart);
|
|
5224
|
+
let isFirst = true;
|
|
5225
|
+
while (current < eventEnd && current < rangeEnd) {
|
|
5226
|
+
if (current >= rangeStart) {
|
|
5227
|
+
const dayStart = new Date(current);
|
|
5228
|
+
const dayEnd = new Date(current);
|
|
5229
|
+
if (isFirst) {
|
|
5230
|
+
dayEnd.setHours(endHour, 0, 0, 0);
|
|
5231
|
+
}
|
|
5232
|
+
else {
|
|
5233
|
+
dayStart.setHours(startHour, 0, 0, 0);
|
|
5234
|
+
if (current.toDateString() === eventEnd.toDateString()) {
|
|
5235
|
+
dayEnd.setHours(eventEnd.getHours(), eventEnd.getMinutes(), 0, 0);
|
|
5236
|
+
}
|
|
5237
|
+
else {
|
|
5238
|
+
dayEnd.setHours(endHour, 0, 0, 0);
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
result.push({
|
|
5242
|
+
...event,
|
|
5243
|
+
startTime: isFirst ? eventStart : dayStart,
|
|
5244
|
+
endTime: current.toDateString() === eventEnd.toDateString() ? eventEnd : dayEnd,
|
|
5245
|
+
continued: !isFirst,
|
|
5246
|
+
continuedEnd: current.toDateString() !== eventEnd.toDateString()
|
|
5247
|
+
});
|
|
5248
|
+
}
|
|
5249
|
+
isFirst = false;
|
|
5250
|
+
current.setDate(current.getDate() + 1);
|
|
5251
|
+
current.setHours(0, 0, 0, 0);
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
return result;
|
|
5255
|
+
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Assigns a zero-based `column` index to each event so that overlapping
|
|
5258
|
+
* events occupy different columns.
|
|
5259
|
+
*
|
|
5260
|
+
* Events are processed in start-time order (longest duration first for ties).
|
|
5261
|
+
* Each event gets the earliest column not already occupied by an overlapping event.
|
|
5262
|
+
*/
|
|
5263
|
+
assignColumnsToEvents(events) {
|
|
5264
|
+
const sorted = [...events].sort((a, b) => {
|
|
5265
|
+
const diff = a.startTime.getTime() - b.startTime.getTime();
|
|
5266
|
+
if (diff !== 0)
|
|
5267
|
+
return diff;
|
|
5268
|
+
return (b.endTime.getTime() - b.startTime.getTime()) - (a.endTime.getTime() - a.startTime.getTime());
|
|
5269
|
+
});
|
|
5270
|
+
for (const event of sorted) {
|
|
5271
|
+
event.column = this.findEarliestPossibleColumn(event, sorted);
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Assigns a `width` (column span) to each event, expanding it to fill
|
|
5276
|
+
* unused columns to its right within the overlapping group.
|
|
5277
|
+
*/
|
|
5278
|
+
assignWidthsToEvents(events, scanStart, scanEnd) {
|
|
5279
|
+
for (const event of events) {
|
|
5280
|
+
const overlapping = this.getAllEventsOnSpecificTime(events, event.startTime, event.endTime);
|
|
5281
|
+
const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
|
|
5282
|
+
const biggestPossible = this.findBiggestPossibleWidth(event, events, scanStart, scanEnd);
|
|
5283
|
+
event.width = Math.max(1, biggestPossible);
|
|
5284
|
+
if ((event.column ?? 0) + event.width > maxCol + 1) {
|
|
5285
|
+
event.width = maxCol + 1 - (event.column ?? 0);
|
|
5286
|
+
}
|
|
5287
|
+
if (event.width < 1)
|
|
5288
|
+
event.width = 1;
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
/** Finds the lowest column index not occupied by any overlapping event. */
|
|
5292
|
+
findEarliestPossibleColumn(event, allEvents) {
|
|
5293
|
+
const overlapping = allEvents.filter(e => e !== event
|
|
5294
|
+
&& e.column !== undefined
|
|
5295
|
+
&& this.eventsOverlap(e.startTime, e.endTime, event.startTime, event.endTime));
|
|
5296
|
+
let column = 0;
|
|
5297
|
+
while (overlapping.some(e => e.column === column)) {
|
|
5298
|
+
column++;
|
|
5299
|
+
}
|
|
5300
|
+
return column;
|
|
5301
|
+
}
|
|
5302
|
+
/** Computes the maximum width an event can span without overlapping a neighbour to its right. */
|
|
5303
|
+
findBiggestPossibleWidth(event, allEvents, _scanStart, _scanEnd) {
|
|
5304
|
+
const overlapping = this.getAllEventsOnSpecificTime(allEvents, event.startTime, event.endTime);
|
|
5305
|
+
const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
|
|
5306
|
+
const totalColumns = maxCol + 1;
|
|
5307
|
+
const occupiedCols = overlapping
|
|
5308
|
+
.filter(e => e !== event && (e.column ?? 0) > (event.column ?? 0))
|
|
5309
|
+
.map(e => e.column ?? 0);
|
|
5310
|
+
if (occupiedCols.length === 0) {
|
|
5311
|
+
return totalColumns - (event.column ?? 0);
|
|
5312
|
+
}
|
|
5313
|
+
return Math.min(...occupiedCols) - (event.column ?? 0);
|
|
5314
|
+
}
|
|
5315
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5316
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService });
|
|
5317
|
+
}
|
|
5318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, decorators: [{
|
|
5319
|
+
type: Injectable
|
|
5320
|
+
}] });
|
|
5321
|
+
|
|
5322
|
+
/**
|
|
5323
|
+
* Static utility methods for calendar grid positioning.
|
|
5324
|
+
*/
|
|
5325
|
+
class CalendarUtility {
|
|
5326
|
+
/**
|
|
5327
|
+
* Converts a weekday (from `Date.getDay()`) to a 1-based Monday-first column index.
|
|
5328
|
+
* Monday = 1, Tuesday = 2, …, Sunday = 7.
|
|
5329
|
+
*/
|
|
5330
|
+
static getCorrectColumn(date) {
|
|
5331
|
+
const day = date.getDay();
|
|
5332
|
+
return day === 0 ? 7 : day;
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* Converts an hour + minute pair to a 1-based CSS grid row index
|
|
5336
|
+
* within a half-hour grid starting at `startHour`.
|
|
5337
|
+
*
|
|
5338
|
+
* Each hour occupies two rows (one per 30-minute slot).
|
|
5339
|
+
* Formula: `(hour - startHour) * 2 + (minute >= 30 ? 1 : 0) + 1`
|
|
5340
|
+
*
|
|
5341
|
+
* @returns Grid row number (minimum 1).
|
|
5342
|
+
*/
|
|
5343
|
+
static getCorrectRow(hour, minute, startHour) {
|
|
5344
|
+
const hourOffset = hour - startHour;
|
|
5345
|
+
const row = hourOffset * 2 + (minute >= 30 ? 1 : 0) + 1;
|
|
5346
|
+
return Math.max(1, row);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
/**
|
|
5351
|
+
* Default event renderer used when no custom component is provided.
|
|
5352
|
+
*
|
|
5353
|
+
* Displays the event title, formatted time range, and optional description
|
|
5354
|
+
* with the event's colour scheme applied as background and left-border accent.
|
|
5355
|
+
*/
|
|
5356
|
+
class CalendarEventDefaultComponent {
|
|
5357
|
+
/** The event to render. Set by {@link CalendarEventComponent} after creation. */
|
|
5358
|
+
event;
|
|
5359
|
+
formattedTime = '';
|
|
5360
|
+
formatter;
|
|
5361
|
+
constructor(formatter) {
|
|
5362
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5363
|
+
}
|
|
5364
|
+
async ngOnInit() {
|
|
5365
|
+
if (this.event) {
|
|
5366
|
+
const start = await this.formatter.formatTime(this.event.startTime);
|
|
5367
|
+
const end = await this.formatter.formatTime(this.event.endTime);
|
|
5368
|
+
this.formattedTime = `${start} - ${end}`;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
5372
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventDefaultComponent, isStandalone: true, selector: "app-calendar-event-default", ngImport: i0, template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
5373
|
+
}
|
|
5374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, decorators: [{
|
|
5375
|
+
type: Component,
|
|
5376
|
+
args: [{ selector: 'app-calendar-event-default', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
5377
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5378
|
+
type: Optional
|
|
5379
|
+
}, {
|
|
5380
|
+
type: Inject,
|
|
5381
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
5382
|
+
}] }] });
|
|
5383
|
+
|
|
5384
|
+
/**
|
|
5385
|
+
* Dynamic event renderer that injects a custom or default event component
|
|
5386
|
+
* into its view container.
|
|
5387
|
+
*
|
|
5388
|
+
* The component to render is resolved in this order:
|
|
5389
|
+
* 1. `customComponent` input (set on the parent week/day view)
|
|
5390
|
+
* 2. `event.component` (per-event override)
|
|
5391
|
+
* 3. {@link CalendarEventDefaultComponent} (library default)
|
|
5392
|
+
*/
|
|
5393
|
+
class CalendarEventComponent {
|
|
5394
|
+
/** The event data to render. */
|
|
5395
|
+
event;
|
|
5396
|
+
/** Optional custom component type that overrides the default renderer. */
|
|
5397
|
+
customComponent;
|
|
5398
|
+
/** Emits when the rendered event is clicked. */
|
|
5399
|
+
eventClicked = new EventEmitter();
|
|
5400
|
+
eventContainer;
|
|
5401
|
+
rendered = false;
|
|
5402
|
+
ngAfterViewInit() {
|
|
5403
|
+
this.renderComponent();
|
|
5404
|
+
}
|
|
5405
|
+
ngOnChanges(changes) {
|
|
5406
|
+
if (this.rendered && (changes['event'] || changes['customComponent'])) {
|
|
5407
|
+
this.renderComponent();
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
/** Emits the event click. */
|
|
5411
|
+
onEventClick() {
|
|
5412
|
+
this.eventClicked.emit(this.event);
|
|
5413
|
+
}
|
|
5414
|
+
/** Creates the event component dynamically and sets its `event` property. */
|
|
5415
|
+
renderComponent() {
|
|
5416
|
+
if (!this.eventContainer)
|
|
5417
|
+
return;
|
|
5418
|
+
this.eventContainer.clear();
|
|
5419
|
+
const component = this.customComponent ?? this.event?.component ?? CalendarEventDefaultComponent;
|
|
5420
|
+
const ref = this.eventContainer.createComponent(component);
|
|
5421
|
+
ref.instance.event = this.event;
|
|
5422
|
+
ref.changeDetectorRef.detectChanges();
|
|
5423
|
+
this.rendered = true;
|
|
5424
|
+
}
|
|
5425
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5426
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventComponent, isStandalone: true, selector: "app-calendar-event", inputs: { event: "event", customComponent: "customComponent" }, outputs: { eventClicked: "eventClicked" }, viewQueries: [{ propertyName: "eventContainer", first: true, predicate: ["eventContainer"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
5427
|
+
}
|
|
5428
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, decorators: [{
|
|
5429
|
+
type: Component,
|
|
5430
|
+
args: [{ selector: 'app-calendar-event', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"] }]
|
|
5431
|
+
}], propDecorators: { event: [{
|
|
5432
|
+
type: Input
|
|
5433
|
+
}], customComponent: [{
|
|
5434
|
+
type: Input
|
|
5435
|
+
}], eventClicked: [{
|
|
5436
|
+
type: Output
|
|
5437
|
+
}], eventContainer: [{
|
|
5438
|
+
type: ViewChild,
|
|
5439
|
+
args: ['eventContainer', { read: ViewContainerRef, static: true }]
|
|
5440
|
+
}] } });
|
|
5441
|
+
|
|
5442
|
+
/**
|
|
5443
|
+
* Week grid view showing 7 day columns with half-hour time slots.
|
|
5444
|
+
*
|
|
5445
|
+
* Overlapping events within the same day are laid out in sub-columns
|
|
5446
|
+
* so they appear side-by-side rather than stacked.
|
|
5447
|
+
*/
|
|
5448
|
+
class CalendarWeekComponent {
|
|
5449
|
+
layoutService;
|
|
5450
|
+
/** The date around which the week is centred. */
|
|
5451
|
+
focusDay;
|
|
5452
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5453
|
+
eventsChanged;
|
|
5454
|
+
/** Observable that emits when the focus day changes. */
|
|
5455
|
+
focusDayChanged;
|
|
5456
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5457
|
+
config;
|
|
5458
|
+
/** Optional custom event renderer component. */
|
|
5459
|
+
calendarEventComponent;
|
|
5460
|
+
/** Emits when a calendar event is clicked. */
|
|
5461
|
+
eventClicked = new EventEmitter();
|
|
5462
|
+
columns = [];
|
|
5463
|
+
hourRows = [];
|
|
5464
|
+
displayEvents = [];
|
|
5465
|
+
totalRows = 0;
|
|
5466
|
+
currentTimeRow = 0;
|
|
5467
|
+
currentTimeCol = '';
|
|
5468
|
+
gridTemplateColumns = 'repeat(7, 1fr)';
|
|
5469
|
+
dayColumnMap = [];
|
|
5470
|
+
events = [];
|
|
5471
|
+
destroy$ = new Subject();
|
|
5472
|
+
formatter;
|
|
5473
|
+
resolvedConfig;
|
|
5474
|
+
currentTimeInterval;
|
|
5475
|
+
constructor(layoutService) {
|
|
5476
|
+
this.layoutService = layoutService;
|
|
5477
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5478
|
+
}
|
|
5479
|
+
ngOnInit() {
|
|
5480
|
+
this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5481
|
+
this.buildHourRows();
|
|
5482
|
+
this.buildColumns();
|
|
5483
|
+
this.updateCurrentTime();
|
|
5484
|
+
this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
|
|
5485
|
+
if (this.eventsChanged) {
|
|
5486
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5487
|
+
this.events = events;
|
|
5488
|
+
this.refreshEvents();
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
if (this.focusDayChanged) {
|
|
5492
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5493
|
+
this.focusDay = date;
|
|
5494
|
+
this.buildColumns();
|
|
5495
|
+
this.refreshEvents();
|
|
5496
|
+
this.updateCurrentTime();
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
}
|
|
5500
|
+
ngOnDestroy() {
|
|
5501
|
+
this.destroy$.next();
|
|
5502
|
+
this.destroy$.complete();
|
|
5503
|
+
if (this.currentTimeInterval)
|
|
5504
|
+
clearInterval(this.currentTimeInterval);
|
|
5505
|
+
}
|
|
5506
|
+
/** Returns the CSS `grid-row` value for an event based on its start/end times. */
|
|
5507
|
+
getEventRow(event) {
|
|
5508
|
+
const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5509
|
+
const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5510
|
+
return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
|
|
5511
|
+
}
|
|
5512
|
+
/** Returns the CSS `grid-column` span for a day header, accounting for sub-columns. */
|
|
5513
|
+
getHeaderColumn(dayIndex) {
|
|
5514
|
+
if (!this.dayColumnMap.length)
|
|
5515
|
+
return `${dayIndex + 2} / span 1`;
|
|
5516
|
+
const dayInfo = this.dayColumnMap[dayIndex];
|
|
5517
|
+
return `${dayInfo.startCol + 1} / span ${dayInfo.subColumns}`;
|
|
5518
|
+
}
|
|
5519
|
+
/** Returns the CSS `grid-column` value for an event within its day's sub-columns. */
|
|
5520
|
+
getEventColumn(event) {
|
|
5521
|
+
const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, event.startTime));
|
|
5522
|
+
if (dayIdx < 0)
|
|
5523
|
+
return '1 / span 1';
|
|
5524
|
+
const dayInfo = this.dayColumnMap[dayIdx];
|
|
5525
|
+
const subCol = (event.column ?? 0) + dayInfo.startCol;
|
|
5526
|
+
const width = event.width ?? 1;
|
|
5527
|
+
return `${subCol} / span ${width}`;
|
|
5528
|
+
}
|
|
5529
|
+
/** Forwards event click to parent. */
|
|
5530
|
+
onEventClick(event) {
|
|
5531
|
+
this.eventClicked.emit(event);
|
|
5532
|
+
}
|
|
5533
|
+
/** trackBy for hour rows. */
|
|
5534
|
+
trackByHour(_index, row) {
|
|
5535
|
+
return row.hour;
|
|
5536
|
+
}
|
|
5537
|
+
/** trackBy for day columns. */
|
|
5538
|
+
trackByColumn(_index, col) {
|
|
5539
|
+
return col.date.getTime();
|
|
5540
|
+
}
|
|
5541
|
+
/** trackBy for events. */
|
|
5542
|
+
trackByEvent(_index, event) {
|
|
5543
|
+
return event.id;
|
|
5544
|
+
}
|
|
5545
|
+
async buildHourRows() {
|
|
5546
|
+
this.hourRows = [];
|
|
5547
|
+
const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
|
|
5548
|
+
this.totalRows = hours * 2;
|
|
5549
|
+
for (let i = 0; i < hours; i++) {
|
|
5550
|
+
const hour = this.resolvedConfig.startHour + i;
|
|
5551
|
+
const label = await this.formatter.formatTimeI(hour, 0);
|
|
5552
|
+
this.hourRows.push({
|
|
5553
|
+
hour,
|
|
5554
|
+
topRow: i * 2 + 1,
|
|
5555
|
+
bottomRow: i * 2 + 3,
|
|
5556
|
+
hourLabel: label
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
/** Builds the 7 day columns for the current week (Monday–Sunday). */
|
|
5561
|
+
buildColumns() {
|
|
5562
|
+
if (!this.focusDay)
|
|
5563
|
+
return;
|
|
5564
|
+
const shortNames = this.resolvedConfig.shortDayNames;
|
|
5565
|
+
const today = new Date();
|
|
5566
|
+
const day = this.focusDay.getDay();
|
|
5567
|
+
const mondayOffset = day === 0 ? -6 : 1 - day;
|
|
5568
|
+
const monday = new Date(this.focusDay);
|
|
5569
|
+
monday.setDate(this.focusDay.getDate() + mondayOffset);
|
|
5570
|
+
this.columns = [];
|
|
5571
|
+
for (let i = 0; i < 7; i++) {
|
|
5572
|
+
const date = new Date(monday);
|
|
5573
|
+
date.setDate(monday.getDate() + i);
|
|
5574
|
+
this.columns.push({
|
|
5575
|
+
date,
|
|
5576
|
+
dayName: shortNames[i],
|
|
5577
|
+
dayNumber: date.getDate(),
|
|
5578
|
+
isToday: this.formatter.isSameDay(date, today)
|
|
5579
|
+
});
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
/** Filters, splits, and lays out events for the current week. */
|
|
5583
|
+
refreshEvents() {
|
|
5584
|
+
if (!this.columns.length)
|
|
5585
|
+
return;
|
|
5586
|
+
const rangeStart = this.columns[0].date;
|
|
5587
|
+
const rangeEnd = new Date(this.columns[6].date);
|
|
5588
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
5589
|
+
const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
|
|
5590
|
+
this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
|
|
5591
|
+
// Assign columns per day so overlapping events within a day get sub-columns
|
|
5592
|
+
for (let i = 0; i < 7; i++) {
|
|
5593
|
+
const dayStart = new Date(this.columns[i].date);
|
|
5594
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
5595
|
+
const dayEnd = new Date(this.columns[i].date);
|
|
5596
|
+
dayEnd.setHours(23, 59, 59, 999);
|
|
5597
|
+
const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
|
|
5598
|
+
this.layoutService.assignColumnsToEvents(dayEvents);
|
|
5599
|
+
this.layoutService.assignWidthsToEvents(dayEvents, dayStart, dayEnd);
|
|
5600
|
+
}
|
|
5601
|
+
this.buildGridColumns();
|
|
5602
|
+
this.updateCurrentTime();
|
|
5603
|
+
}
|
|
5604
|
+
/** Computes the CSS grid-template-columns string based on per-day sub-column counts. */
|
|
5605
|
+
buildGridColumns() {
|
|
5606
|
+
this.dayColumnMap = [];
|
|
5607
|
+
let currentCol = 1;
|
|
5608
|
+
for (let i = 0; i < 7; i++) {
|
|
5609
|
+
const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
|
|
5610
|
+
let maxSubCols = 1;
|
|
5611
|
+
for (const e of dayEvents) {
|
|
5612
|
+
maxSubCols = Math.max(maxSubCols, (e.column ?? 0) + (e.width ?? 1));
|
|
5613
|
+
}
|
|
5614
|
+
this.dayColumnMap.push({ subColumns: maxSubCols, startCol: currentCol });
|
|
5615
|
+
currentCol += maxSubCols;
|
|
5616
|
+
}
|
|
5617
|
+
const parts = [];
|
|
5618
|
+
for (const day of this.dayColumnMap) {
|
|
5619
|
+
for (let j = 0; j < day.subColumns; j++) {
|
|
5620
|
+
parts.push(`${1 / day.subColumns}fr`);
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
this.gridTemplateColumns = parts.join(' ');
|
|
5624
|
+
}
|
|
5625
|
+
/** Updates the current-time red line position. */
|
|
5626
|
+
updateCurrentTime() {
|
|
5627
|
+
const now = new Date();
|
|
5628
|
+
const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, now));
|
|
5629
|
+
if (dayIdx >= 0 && this.dayColumnMap.length > 0) {
|
|
5630
|
+
const dayInfo = this.dayColumnMap[dayIdx];
|
|
5631
|
+
this.currentTimeCol = `${dayInfo.startCol} / span ${dayInfo.subColumns}`;
|
|
5632
|
+
this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
|
|
5633
|
+
}
|
|
5634
|
+
else {
|
|
5635
|
+
this.currentTimeCol = '';
|
|
5636
|
+
this.currentTimeRow = 0;
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5640
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "app-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
5641
|
+
}
|
|
5642
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
|
|
5643
|
+
type: Component,
|
|
5644
|
+
args: [{ selector: 'app-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
|
|
5645
|
+
}], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
|
|
5646
|
+
type: Input
|
|
5647
|
+
}], eventsChanged: [{
|
|
5648
|
+
type: Input
|
|
5649
|
+
}], focusDayChanged: [{
|
|
5650
|
+
type: Input
|
|
5651
|
+
}], config: [{
|
|
5652
|
+
type: Input
|
|
5653
|
+
}], calendarEventComponent: [{
|
|
5654
|
+
type: Input
|
|
5655
|
+
}], eventClicked: [{
|
|
5656
|
+
type: Output
|
|
5657
|
+
}] } });
|
|
5658
|
+
|
|
5659
|
+
/**
|
|
5660
|
+
* Day grid view showing a single day with half-hour time slots.
|
|
5661
|
+
*
|
|
5662
|
+
* Shares the same layout algorithm as the week view via
|
|
5663
|
+
* {@link CalendarEventLayoutService}.
|
|
5664
|
+
*/
|
|
5665
|
+
class CalendarDayComponent {
|
|
5666
|
+
layoutService;
|
|
5667
|
+
/** The date to display. */
|
|
5668
|
+
focusDay;
|
|
5669
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5670
|
+
eventsChanged;
|
|
5671
|
+
/** Observable that emits when the focus day changes. */
|
|
5672
|
+
focusDayChanged;
|
|
5673
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5674
|
+
config;
|
|
5675
|
+
/** Optional custom event renderer component. */
|
|
5676
|
+
calendarEventComponent;
|
|
5677
|
+
/** Emits when a calendar event is clicked. */
|
|
5678
|
+
eventClicked = new EventEmitter();
|
|
5679
|
+
hourRows = [];
|
|
5680
|
+
displayEvents = [];
|
|
5681
|
+
totalRows = 0;
|
|
5682
|
+
totalColumns = 1;
|
|
5683
|
+
currentTimeRow = 0;
|
|
5684
|
+
isToday = false;
|
|
5685
|
+
dayName = '';
|
|
5686
|
+
events = [];
|
|
5687
|
+
destroy$ = new Subject();
|
|
5688
|
+
formatter;
|
|
5689
|
+
resolvedConfig;
|
|
5690
|
+
currentTimeInterval;
|
|
5691
|
+
constructor(layoutService) {
|
|
5692
|
+
this.layoutService = layoutService;
|
|
5693
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5694
|
+
}
|
|
5695
|
+
ngOnInit() {
|
|
5696
|
+
this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5697
|
+
this.buildHourRows();
|
|
5698
|
+
this.updateDayInfo();
|
|
5699
|
+
this.updateCurrentTime();
|
|
5700
|
+
this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
|
|
5701
|
+
if (this.eventsChanged) {
|
|
5702
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5703
|
+
this.events = events;
|
|
5704
|
+
this.refreshEvents();
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
if (this.focusDayChanged) {
|
|
5708
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5709
|
+
this.focusDay = date;
|
|
5710
|
+
this.updateDayInfo();
|
|
5711
|
+
this.refreshEvents();
|
|
5712
|
+
this.updateCurrentTime();
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
ngOnDestroy() {
|
|
5717
|
+
this.destroy$.next();
|
|
5718
|
+
this.destroy$.complete();
|
|
5719
|
+
if (this.currentTimeInterval)
|
|
5720
|
+
clearInterval(this.currentTimeInterval);
|
|
5721
|
+
}
|
|
5722
|
+
/** Returns the CSS `grid-row` value for an event. */
|
|
5723
|
+
getEventRow(event) {
|
|
5724
|
+
const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5725
|
+
const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5726
|
+
return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
|
|
5727
|
+
}
|
|
5728
|
+
/** Returns the CSS `grid-column` value for an event within its sub-columns. */
|
|
5729
|
+
getEventColumn(event) {
|
|
5730
|
+
const col = (event.column ?? 0) + 1;
|
|
5731
|
+
const width = event.width ?? 1;
|
|
5732
|
+
return `${col} / span ${width}`;
|
|
5733
|
+
}
|
|
5734
|
+
/** Forwards event click to parent. */
|
|
5735
|
+
onEventClick(event) {
|
|
5736
|
+
this.eventClicked.emit(event);
|
|
5737
|
+
}
|
|
5738
|
+
/** trackBy for hour rows. */
|
|
5739
|
+
trackByHour(_index, row) {
|
|
5740
|
+
return row.hour;
|
|
5741
|
+
}
|
|
5742
|
+
/** trackBy for events. */
|
|
5743
|
+
trackByEvent(_index, event) {
|
|
5744
|
+
return event.id;
|
|
5745
|
+
}
|
|
5746
|
+
async buildHourRows() {
|
|
5747
|
+
this.hourRows = [];
|
|
5748
|
+
const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
|
|
5749
|
+
this.totalRows = hours * 2;
|
|
5750
|
+
for (let i = 0; i < hours; i++) {
|
|
5751
|
+
const hour = this.resolvedConfig.startHour + i;
|
|
5752
|
+
const label = await this.formatter.formatTimeI(hour, 0);
|
|
5753
|
+
this.hourRows.push({
|
|
5754
|
+
hour,
|
|
5755
|
+
topRow: i * 2 + 1,
|
|
5756
|
+
bottomRow: i * 2 + 3,
|
|
5757
|
+
hourLabel: label
|
|
5758
|
+
});
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
/** Updates the day name and isToday flag. */
|
|
5762
|
+
updateDayInfo() {
|
|
5763
|
+
if (!this.focusDay)
|
|
5764
|
+
return;
|
|
5765
|
+
const today = new Date();
|
|
5766
|
+
this.isToday = this.formatter.isSameDay(this.focusDay, today);
|
|
5767
|
+
const longNames = this.resolvedConfig.longDayNames;
|
|
5768
|
+
const dayIdx = this.focusDay.getDay();
|
|
5769
|
+
const mondayIdx = dayIdx === 0 ? 6 : dayIdx - 1;
|
|
5770
|
+
this.dayName = longNames[mondayIdx];
|
|
5771
|
+
}
|
|
5772
|
+
/** Filters, splits, and lays out events for the focus day. */
|
|
5773
|
+
refreshEvents() {
|
|
5774
|
+
if (!this.focusDay)
|
|
5775
|
+
return;
|
|
5776
|
+
const rangeStart = new Date(this.focusDay);
|
|
5777
|
+
rangeStart.setHours(0, 0, 0, 0);
|
|
5778
|
+
const rangeEnd = new Date(this.focusDay);
|
|
5779
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
5780
|
+
const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
|
|
5781
|
+
this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
|
|
5782
|
+
this.layoutService.assignColumnsToEvents(this.displayEvents);
|
|
5783
|
+
this.layoutService.assignWidthsToEvents(this.displayEvents, rangeStart, rangeEnd);
|
|
5784
|
+
const maxCol = this.displayEvents.reduce((max, e) => Math.max(max, (e.column ?? 0) + (e.width ?? 1)), 1);
|
|
5785
|
+
this.totalColumns = maxCol;
|
|
5786
|
+
}
|
|
5787
|
+
/** Updates the current-time red line position. */
|
|
5788
|
+
updateCurrentTime() {
|
|
5789
|
+
const now = new Date();
|
|
5790
|
+
if (this.focusDay && this.formatter.isSameDay(this.focusDay, now)) {
|
|
5791
|
+
this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
|
|
5792
|
+
this.isToday = true;
|
|
5793
|
+
}
|
|
5794
|
+
else {
|
|
5795
|
+
this.currentTimeRow = 0;
|
|
5796
|
+
this.isToday = false;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5800
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "app-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
5801
|
+
}
|
|
5802
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
|
|
5803
|
+
type: Component,
|
|
5804
|
+
args: [{ selector: 'app-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
|
|
5805
|
+
}], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
|
|
5806
|
+
type: Input
|
|
5807
|
+
}], eventsChanged: [{
|
|
5808
|
+
type: Input
|
|
5809
|
+
}], focusDayChanged: [{
|
|
5810
|
+
type: Input
|
|
5811
|
+
}], config: [{
|
|
5812
|
+
type: Input
|
|
5813
|
+
}], calendarEventComponent: [{
|
|
5814
|
+
type: Input
|
|
5815
|
+
}], eventClicked: [{
|
|
5816
|
+
type: Output
|
|
5817
|
+
}] } });
|
|
5818
|
+
|
|
5819
|
+
/**
|
|
5820
|
+
* Renders a single row in the upcoming-events sidebar.
|
|
5821
|
+
* Shows the event title, formatted date/time, and optional description.
|
|
5822
|
+
*/
|
|
5823
|
+
class UpcomingEventRowComponent {
|
|
5824
|
+
/** The event to display. */
|
|
5825
|
+
event;
|
|
5826
|
+
/** Emits the event when this row is clicked. */
|
|
5827
|
+
eventClicked = new EventEmitter();
|
|
5828
|
+
formattedDate = '';
|
|
5829
|
+
formatter;
|
|
5830
|
+
constructor(formatter) {
|
|
5831
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5832
|
+
}
|
|
5833
|
+
async ngOnInit() {
|
|
5834
|
+
if (this.event) {
|
|
5835
|
+
const start = await this.formatter.formatTime(this.event.startTime);
|
|
5836
|
+
const end = await this.formatter.formatTime(this.event.endTime);
|
|
5837
|
+
this.formattedDate = `${start} - ${end}`;
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
5841
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventRowComponent, isStandalone: true, selector: "app-upcoming-event-row", inputs: { event: "event" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
5842
|
+
}
|
|
5843
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, decorators: [{
|
|
5844
|
+
type: Component,
|
|
5845
|
+
args: [{ selector: 'app-upcoming-event-row', standalone: true, imports: [CommonModule], template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
5846
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5847
|
+
type: Optional
|
|
5848
|
+
}, {
|
|
5849
|
+
type: Inject,
|
|
5850
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
5851
|
+
}] }], propDecorators: { event: [{
|
|
5852
|
+
type: Input
|
|
5853
|
+
}], eventClicked: [{
|
|
5854
|
+
type: Output
|
|
5855
|
+
}] } });
|
|
5856
|
+
|
|
5857
|
+
/**
|
|
5858
|
+
* Sidebar component that lists the next 10 upcoming events
|
|
5859
|
+
* (events whose end time is in the future), sorted by start time.
|
|
5860
|
+
*/
|
|
5861
|
+
class UpcomingEventsComponent {
|
|
5862
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5863
|
+
eventsChanged;
|
|
5864
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5865
|
+
config;
|
|
5866
|
+
/** Emits when an upcoming event row is clicked. */
|
|
5867
|
+
eventClicked = new EventEmitter();
|
|
5868
|
+
upcomingEvents = [];
|
|
5869
|
+
title;
|
|
5870
|
+
destroy$ = new Subject();
|
|
5871
|
+
constructor() {
|
|
5872
|
+
this.title = DEFAULT_CALENDAR_CONFIG.upcomingEventsTitle;
|
|
5873
|
+
}
|
|
5874
|
+
ngOnInit() {
|
|
5875
|
+
const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5876
|
+
this.title = resolved.upcomingEventsTitle;
|
|
5877
|
+
if (this.eventsChanged) {
|
|
5878
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5879
|
+
const now = new Date();
|
|
5880
|
+
this.upcomingEvents = events
|
|
5881
|
+
.filter(e => e.endTime > now)
|
|
5882
|
+
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
|
5883
|
+
.slice(0, 10);
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
ngOnDestroy() {
|
|
5888
|
+
this.destroy$.next();
|
|
5889
|
+
this.destroy$.complete();
|
|
5890
|
+
}
|
|
5891
|
+
/** trackBy for upcoming event rows. */
|
|
5892
|
+
trackByEvent(_index, event) {
|
|
5893
|
+
return event.id;
|
|
5894
|
+
}
|
|
5895
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5896
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "app-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: UpcomingEventRowComponent, selector: "app-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
|
|
5897
|
+
}
|
|
5898
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
|
|
5899
|
+
type: Component,
|
|
5900
|
+
args: [{ selector: 'app-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"] }]
|
|
5901
|
+
}], ctorParameters: () => [], propDecorators: { eventsChanged: [{
|
|
5902
|
+
type: Input
|
|
5903
|
+
}], config: [{
|
|
5904
|
+
type: Input
|
|
5905
|
+
}], eventClicked: [{
|
|
5906
|
+
type: Output
|
|
5907
|
+
}] } });
|
|
5908
|
+
|
|
5909
|
+
/**
|
|
5910
|
+
* Main calendar orchestrator component.
|
|
5911
|
+
*
|
|
5912
|
+
* Provides a toolbar with view switching (month / week / day), date navigation,
|
|
5913
|
+
* and an optional action button. The active view and an upcoming-events sidebar
|
|
5914
|
+
* are rendered inside a responsive grid layout.
|
|
5915
|
+
*
|
|
5916
|
+
* All configuration (visible hours, locale, labels, mobile breakpoint) is read
|
|
5917
|
+
* from the `mn-config.json5` system via {@link MN_CALENDAR_CONFIG}, falling back
|
|
5918
|
+
* to the legacy {@link CALENDAR_CONFIG} injection token, then to built-in defaults.
|
|
5919
|
+
* Date formatting is delegated to the {@link CALENDAR_DATE_FORMATTER} token.
|
|
5920
|
+
*
|
|
5921
|
+
* @example
|
|
5922
|
+
* ```html
|
|
5923
|
+
* <app-calendar-view
|
|
5924
|
+
* [showButton]="true"
|
|
5925
|
+
* [buttonTitle]="'New Event'"
|
|
5926
|
+
* [NewCalendarItemsEvent]="eventsEmitter"
|
|
5927
|
+
* (RequestNewCalendarItemsEvent)="loadEvents($event)"
|
|
5928
|
+
* (CalendarItemClickedEvent)="onEventClick($event)"
|
|
5929
|
+
* (ButtonClickedEvent)="openModal()">
|
|
5930
|
+
* </app-calendar-view>
|
|
5931
|
+
* ```
|
|
5932
|
+
*/
|
|
5933
|
+
class CalendarViewComponent {
|
|
5934
|
+
/** Whether to show the action button in the toolbar. */
|
|
5935
|
+
showButton = false;
|
|
5936
|
+
/** Label text for the action button. */
|
|
5937
|
+
buttonTitle = '';
|
|
5938
|
+
/** Custom event renderer component type. */
|
|
5939
|
+
CalendarEventComponent;
|
|
5940
|
+
/** Observable or EventEmitter that pushes new event arrays into the calendar. */
|
|
5941
|
+
NewCalendarItemsEvent;
|
|
5942
|
+
/** Emits when the calendar needs fresh event data (e.g. after navigation). */
|
|
5943
|
+
RequestNewCalendarItemsEvent = new EventEmitter();
|
|
5944
|
+
/** Emits when a calendar event is clicked. */
|
|
5945
|
+
CalendarItemClickedEvent = new EventEmitter();
|
|
5946
|
+
/** Emits when the action button is clicked. */
|
|
5947
|
+
ButtonClickedEvent = new EventEmitter();
|
|
5948
|
+
CalendarView = CalendarView;
|
|
5949
|
+
currentView = CalendarView.WEEK;
|
|
5950
|
+
focusDay = new Date();
|
|
5951
|
+
dateInputValue = '';
|
|
5952
|
+
viewOptions = [];
|
|
5953
|
+
isMobileView = false;
|
|
5954
|
+
/** BehaviorSubject so late-subscribing child views receive the last emitted events. */
|
|
5955
|
+
internalEventsChanged = new BehaviorSubject([]);
|
|
5956
|
+
/** Subject for broadcasting focus-day changes to child views. */
|
|
5957
|
+
internalFocusDayChanged = new Subject();
|
|
5958
|
+
destroy$ = new Subject();
|
|
5959
|
+
formatter;
|
|
5960
|
+
config;
|
|
5961
|
+
destroyRef = inject(DestroyRef);
|
|
5962
|
+
lang = inject(MnLanguageService);
|
|
5963
|
+
constructor(formatter, mnConfig, legacyConfig) {
|
|
5964
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5965
|
+
// Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
|
|
5966
|
+
const raw = mnConfig ?? legacyConfig ?? undefined;
|
|
5967
|
+
this.config = resolveCalendarConfig(raw);
|
|
5968
|
+
}
|
|
5969
|
+
onResize() {
|
|
5970
|
+
this.checkMobileView();
|
|
5971
|
+
}
|
|
5972
|
+
ngOnInit() {
|
|
5973
|
+
this.rebuildFromConfig();
|
|
5974
|
+
// Re-resolve config when locale changes (supports $translate in mn-config).
|
|
5975
|
+
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
5976
|
+
this.rebuildFromConfig();
|
|
5977
|
+
});
|
|
5978
|
+
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
5979
|
+
this.checkMobileView();
|
|
5980
|
+
this.updateDateInput();
|
|
5981
|
+
this.RequestNewCalendarItemsEvent.emit(this.focusDay);
|
|
5982
|
+
if (this.NewCalendarItemsEvent) {
|
|
5983
|
+
this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5984
|
+
this.internalEventsChanged.next(events);
|
|
5985
|
+
});
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
ngOnDestroy() {
|
|
5989
|
+
this.destroy$.next();
|
|
5990
|
+
this.destroy$.complete();
|
|
5991
|
+
}
|
|
5992
|
+
/** Switches the active view. On mobile, forces day view. */
|
|
5993
|
+
switchView(view) {
|
|
5994
|
+
if (this.isMobileView) {
|
|
5995
|
+
this.currentView = CalendarView.DAY;
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
this.currentView = view;
|
|
5999
|
+
}
|
|
6000
|
+
/** Navigates to the previous period (month / week / day). */
|
|
6001
|
+
navigatePrevious() {
|
|
6002
|
+
const d = new Date(this.focusDay);
|
|
6003
|
+
switch (this.currentView) {
|
|
6004
|
+
case CalendarView.MONTH:
|
|
6005
|
+
d.setMonth(d.getMonth() - 1);
|
|
6006
|
+
break;
|
|
6007
|
+
case CalendarView.WEEK:
|
|
6008
|
+
d.setDate(d.getDate() - 7);
|
|
6009
|
+
break;
|
|
6010
|
+
case CalendarView.DAY:
|
|
6011
|
+
d.setDate(d.getDate() - 1);
|
|
6012
|
+
break;
|
|
6013
|
+
}
|
|
6014
|
+
this.setFocusDay(d);
|
|
6015
|
+
}
|
|
6016
|
+
/** Navigates to the next period (month / week / day). */
|
|
6017
|
+
navigateNext() {
|
|
6018
|
+
const d = new Date(this.focusDay);
|
|
6019
|
+
switch (this.currentView) {
|
|
6020
|
+
case CalendarView.MONTH:
|
|
6021
|
+
d.setMonth(d.getMonth() + 1);
|
|
6022
|
+
break;
|
|
6023
|
+
case CalendarView.WEEK:
|
|
6024
|
+
d.setDate(d.getDate() + 7);
|
|
6025
|
+
break;
|
|
6026
|
+
case CalendarView.DAY:
|
|
6027
|
+
d.setDate(d.getDate() + 1);
|
|
6028
|
+
break;
|
|
6029
|
+
}
|
|
6030
|
+
this.setFocusDay(d);
|
|
6031
|
+
}
|
|
6032
|
+
/** Navigates to today. */
|
|
6033
|
+
goToToday() {
|
|
6034
|
+
this.setFocusDay(new Date());
|
|
6035
|
+
}
|
|
6036
|
+
/** Handles the date-picker input change. */
|
|
6037
|
+
onDateInputChange(event) {
|
|
6038
|
+
const value = event.target.value;
|
|
6039
|
+
if (value) {
|
|
6040
|
+
this.setFocusDay(new Date(value));
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
/** Handles a day click from the month view — switches to day view. */
|
|
6044
|
+
onMonthDayClick(date) {
|
|
6045
|
+
this.currentView = CalendarView.DAY;
|
|
6046
|
+
this.setFocusDay(date);
|
|
6047
|
+
}
|
|
6048
|
+
/** Forwards a child event click to the parent output. */
|
|
6049
|
+
onEventClick(event) {
|
|
6050
|
+
this.CalendarItemClickedEvent.emit(event);
|
|
6051
|
+
}
|
|
6052
|
+
/** trackBy for view option buttons. */
|
|
6053
|
+
trackByView(_index, item) {
|
|
6054
|
+
return item.value;
|
|
6055
|
+
}
|
|
6056
|
+
/** Rebuilds view options and labels from the current config. */
|
|
6057
|
+
rebuildFromConfig() {
|
|
6058
|
+
this.viewOptions = [
|
|
6059
|
+
{ value: CalendarView.MONTH, label: this.config.viewLabels['MONTH'] ?? 'Month' },
|
|
6060
|
+
{ value: CalendarView.WEEK, label: this.config.viewLabels['WEEK'] ?? 'Week' },
|
|
6061
|
+
{ value: CalendarView.DAY, label: this.config.viewLabels['DAY'] ?? 'Day' }
|
|
6062
|
+
];
|
|
6063
|
+
}
|
|
6064
|
+
checkMobileView() {
|
|
6065
|
+
const wasMobile = this.isMobileView;
|
|
6066
|
+
this.isMobileView = window.innerWidth < this.config.mobileBreakpoint;
|
|
6067
|
+
if (this.isMobileView && !wasMobile) {
|
|
6068
|
+
this.currentView = CalendarView.DAY;
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
setFocusDay(date) {
|
|
6072
|
+
this.focusDay = date;
|
|
6073
|
+
this.updateDateInput();
|
|
6074
|
+
this.internalFocusDayChanged.next(date);
|
|
6075
|
+
this.RequestNewCalendarItemsEvent.emit(date);
|
|
6076
|
+
}
|
|
6077
|
+
updateDateInput() {
|
|
6078
|
+
this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
|
|
6079
|
+
}
|
|
6080
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }, { token: MN_CALENDAR_CONFIG, optional: true }, { token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
6081
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "app-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
|
|
6082
|
+
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
6083
|
+
], ngImport: i0, template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">‹</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">›</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarMonthComponent, selector: "app-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "app-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "app-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "app-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }] });
|
|
6084
|
+
}
|
|
6085
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
|
|
6086
|
+
type: Component,
|
|
6087
|
+
args: [{ selector: 'app-calendar-view', standalone: true, imports: [
|
|
6088
|
+
CommonModule,
|
|
6089
|
+
CalendarMonthComponent,
|
|
6090
|
+
CalendarWeekComponent,
|
|
6091
|
+
CalendarDayComponent,
|
|
6092
|
+
UpcomingEventsComponent
|
|
6093
|
+
], providers: [
|
|
6094
|
+
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
6095
|
+
], template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">‹</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">›</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"] }]
|
|
6096
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
6097
|
+
type: Optional
|
|
6098
|
+
}, {
|
|
6099
|
+
type: Inject,
|
|
6100
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
6101
|
+
}] }, { type: undefined, decorators: [{
|
|
6102
|
+
type: Optional
|
|
6103
|
+
}, {
|
|
6104
|
+
type: Inject,
|
|
6105
|
+
args: [MN_CALENDAR_CONFIG]
|
|
6106
|
+
}] }, { type: undefined, decorators: [{
|
|
6107
|
+
type: Optional
|
|
6108
|
+
}, {
|
|
6109
|
+
type: Inject,
|
|
6110
|
+
args: [CALENDAR_CONFIG]
|
|
6111
|
+
}] }], propDecorators: { showButton: [{
|
|
6112
|
+
type: Input
|
|
6113
|
+
}], buttonTitle: [{
|
|
6114
|
+
type: Input
|
|
6115
|
+
}], CalendarEventComponent: [{
|
|
6116
|
+
type: Input
|
|
6117
|
+
}], NewCalendarItemsEvent: [{
|
|
6118
|
+
type: Input
|
|
6119
|
+
}], RequestNewCalendarItemsEvent: [{
|
|
6120
|
+
type: Output
|
|
6121
|
+
}], CalendarItemClickedEvent: [{
|
|
6122
|
+
type: Output
|
|
6123
|
+
}], ButtonClickedEvent: [{
|
|
6124
|
+
type: Output
|
|
6125
|
+
}], onResize: [{
|
|
6126
|
+
type: HostListener,
|
|
6127
|
+
args: ['window:resize']
|
|
6128
|
+
}] } });
|
|
6129
|
+
|
|
6130
|
+
// Main component
|
|
6131
|
+
|
|
4864
6132
|
class MnSectionDirective {
|
|
4865
6133
|
/** Section name contributed by this DOM node to the section path */
|
|
4866
6134
|
mnSection;
|
|
@@ -5394,5 +6662,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
5394
6662
|
* Generated bundle index. Do not edit.
|
|
5395
6663
|
*/
|
|
5396
6664
|
|
|
5397
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
|
|
6665
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
5398
6666
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|