// import deepEqual from 'fast-deep-equal'; import { deepCopy, deepEqual } from "./utils"; import { Component, ElementRef, Input, Output, EventEmitter, SimpleChanges, AfterViewInit, DoCheck, OnChanges, AfterContentChecked, OnDestroy, ViewEncapsulation, ChangeDetectionStrategy, Injector } from "@angular/core"; import { Calendar, BusinessHoursInput, ConstraintInput, EventApi, PluginDef } from "@fullcalendar/core"; import { ToolbarInput, CustomButtonInput, ButtonIconsInput, CellInfo, ButtonTextCompoundInput, ViewOptionsInput } from "@fullcalendar/core/types/input-types"; import { DateInput } from "@fullcalendar/core/datelib/env"; import { DurationInput } from "@fullcalendar/core/datelib/duration"; import { FormatterInput } from "@fullcalendar/core/datelib/formatting"; import { DateRangeInput } from "@fullcalendar/core/datelib/date-range"; import { RawLocale, LocaleSingularArg } from "@fullcalendar/core/datelib/locale"; import { OverlapFunc, AllowFunc } from "@fullcalendar/core/validation"; import { EventSourceInput, EventInputTransformer, EventSourceErrorResponseHandler, EventSourceSuccessResponseHandler } from "@fullcalendar/core/structs/event-source"; import { INPUT_NAMES, INPUT_IS_DEEP, OUTPUT_NAMES } from "./calendar-options"; import { DefaultConfigService } from "./calendar.config.service"; import { Observable } from "rxjs"; import { FarrisComponentInstanceService } from '@farris/ui-common'; @Component({ // tslint:disable-next-line: component-selector selector: "farris-calendar", template: "", styleUrls: ["./calendar.component.css"], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }) export class CalendarComponent implements AfterViewInit, DoCheck, OnChanges, AfterContentChecked, OnDestroy { @Input() deepChangeDetection?: boolean; // 设置头部信息,如果不想显示,可以设置header为false @Input() header?: boolean | ToolbarInput; @Input() footer?: boolean | ToolbarInput; @Input() customButtons?: { [name: string]: CustomButtonInput }; // 设置header中使用的prev, next等变量对应按钮的样式 @Input() buttonIcons?: boolean | ButtonIconsInput; @Input() themeSystem?: "standard" | string; @Input() bootstrapFontAwesome?: boolean | ButtonIconsInput; // 设置一周中显示的第一天是哪天,周日是0,周一是1,类推。 @Input() firstDay?: number; @Input() dir?: "ltr" | "rtl" | "auto"; // 设置是否显示周六和周日,设为false则不显示 @Input() weekends?: boolean; // 隐藏一周中的某一天或某几天,数组形式,如隐藏周二和周五:[2,5],默认不隐藏,除非weekends设置为false。 @Input() hiddenDays?: number[]; // Determines the number of weeks displayed in a month view. @Input() fixedWeekCount?: boolean; // 是否在日历中显示周次(一年中的第几周),如果设置为true,则会在月视图的左侧、周视图和日视图的左上角显示周数。 @Input() weekNumbers?: boolean; // Determines the styling for week numbers in Month and DayGrid views. @Input() weekNumbersWithinDays?: boolean; // 周次的显示格式。 @Input() weekNumberCalculation?: "local" | "ISO" | ((m: Date) => number); // 区分工作时间 @Input() businessHours?: BusinessHoursInput; // In month view, whether dates in the previous or next month should be rendered at all. @Input() showNonCurrentDates?: boolean; // 设置日历的高度,包括header日历头部,默认未设置,高度根据aspectRatio值自适应。 @Input() height?: number | "auto" | "parent" | (() => number); // 设置日历主体内容的高度,不包括header部分,默认未设置,高度根据aspectRatio值自适应 @Input() contentHeight?: number | "auto" | (() => number); // 月视图下日历格子宽度和高度的比例 @Input() aspectRatio?: number; // 是否随浏览器窗口大小变化而自动变化。 @Input() handleWindowResize?: boolean; // The time the calendar will wait to adjust its size after a window resize occurs, in milliseconds. @Input() windowResizeDelay?: number; // 设置为true时,如果数据过多超过日历格子显示的高度时,多出去的数据不会将格子挤开,而是显示为 +...more ,点击后才会完整显示所有的数据 @Input() eventLimit?: boolean | number; // Determines the action taken when the user clicks on a “more” link created by the eventLimit option. @Input() eventLimitClick?: | "popover" | "week" | "day" | string | ((cellinfo: CellInfo, jsevent: Event) => void); // 时区选择 默认 local @Input() timeZone?: string | boolean; // Explicitly sets the “today” date of the calendar. This is the day that is normally highlighted in yellow. @Input() now?: DateInput | (() => DateInput); // 初始化时的默认视图 @Input() defaultView?: string; // 在agenda视图模式下,是否在日历上方显示all-day(全天) @Input() allDaySlot?: boolean; // 在agenda视图模式下,定义日历上方显示全天信息的文本 @Input() allDayText?: string; // The frequency for displaying time slots. @Input() slotDuration?: DurationInput; // Determines the text that will be displayed within a time slot. @Input() slotLabelFormat?: FormatterInput; // The frequency that the time slots should be labelled with text. @Input() slotLabelInterval?: DurationInput; // The time interval at which a dragged event will snap to the time axis. Also affects the granularity at which selections can be made. @Input() snapDuration?: DurationInput; // Determines how far forward the scroll pane is initially scrolled. @Input() scrollTime?: DurationInput; // Determines the first time slot that will be displayed for each day. @Input() minTime?: DurationInput; // Determines the last time slot that will be displayed for each day. @Input() maxTime?: DurationInput; // Determines if timed events in TimeGrid view should visually overlap. @Input() slotEventOverlap?: boolean; // A Date Formatter that affects the text on the left side of the day headings in list view. @Input() listDayFormat?: FormatterInput | boolean; // A Date Formatter that affects the text on the right side of the day headings in list view. @Input() listDayAltFormat?: FormatterInput | boolean; // The text that is displayed in the middle of list view, alerting the user that there are no events within the given range. @Input() noEventsMessage?: string; // 日历初始化时显示的日期,月视图显示该月,周视图显示该周,日视图显示该天,和当前日期没有关系 @Input() defaultDate?: DateInput; // Whether or not to display a marker indicating the current time. @Input() nowIndicator?: boolean; @Input() visibleRange?: | ((currentDate: Date) => DateRangeInput) | DateRangeInput; // Limits which dates the user can navigate to and where events can go. @Input() validRange?: DateRangeInput; // How far into the future/past the calendar navigates when prev/next is executed @Input() dateIncrement?: DurationInput; // 确定自定义视图的第一个可见日。 @Input() dateAlignment?: string; @Input() duration?: DurationInput; @Input() dayCount?: number; // The locale and locales options allow you to localize calendar @Input() locales?: RawLocale[]; @Input() locale?: LocaleSingularArg; @Input() eventTimeFormat?: FormatterInput; @Input() columnHeader?: boolean; @Input() columnHeaderFormat?: FormatterInput; @Input() columnHeaderText?: string | ((date: DateInput) => string); @Input() columnHeaderHtml?: string | ((date: DateInput) => string); @Input() titleFormat?: FormatterInput; @Input() weekLabel?: string; @Input() displayEventTime?: boolean; @Input() displayEventEnd?: boolean; // Determines the text of the link created by the eventLimit setting. @Input() eventLimitText?: string | ((eventCnt: number) => string); // 确定由eventLimitClick选项创建的弹出窗口标题的日期格式 @Input() dayPopoverFormat?: FormatterInput; // 确定是否可以单击日名和周名 @Input() navLinks?: boolean; // Determines what happens upon a week-number nav-link click. @Input() navLinkWeekClick?: | string | ((weekStart: any, jsEvent: Event) => void); // 设置是否可被单击或者拖动选择 @Input() selectable?: boolean; // Whether to draw a “placeholder” event while the user is dragging. @Input() selectMirror?: boolean; // 点击或者拖动选中之后,点击日历外的空白区域是否取消选中状态 true为取消 false为不取消,只有重新选择时才会取消 @Input() unselectAuto?: boolean; // A way to specify elements that will ignore the unselectAuto option. @Input() unselectCancel?: string; @Input() defaultAllDayEventDuration?: DurationInput; @Input() defaultTimedEventDuration?: DurationInput; @Input() cmdFormatter?: string; @Input() defaultRangeSeparator?: string; // Limits user selection to certain windows of time. @Input() selectConstraint?: ConstraintInput; // Determines whether the user is allowed to select periods of time that are occupied by events. @Input() selectOverlap?: boolean | OverlapFunc; // Exact programmatic control over where the user can select. @Input() selectAllow?: AllowFunc; // The minimum distance the user’s mouse must travel after a mousedown, before a selection is allowed. @Input() selectMinDistance?: number; // Event是否可被拖动或者拖拽 @Input() editable?: boolean; // Allow events’ start times to be editable through dragging. @Input() eventStartEditable?: boolean; // Allow events’ durations to be editable through resizing. @Input() eventDurationEditable?: boolean; // Limits event dragging and resizing to certain windows of time. @Input() eventConstraint?: ConstraintInput; // Determines if events being dragged and resized are allowed to overlap each other. @Input() eventOverlap?: boolean | OverlapFunc; // Exact programmatic control over where an event can be dropped. @Input() eventAllow?: AllowFunc; @Input() eventClassName?: string[] | string; @Input() eventClassNames?: string[] | string; @Input() eventBackgroundColor?: string; @Input() eventBorderColor?: string; @Input() eventTextColor?: string; @Input() eventColor?: string; // 日程数据 @Input() set events(e: any) { if (e instanceof Observable) { e.subscribe((val: EventSourceInput) => { this._events = val; }); } else { this._events = e; } } get events(): any { return this._events; } // 指定多个事件源的方法。 @Input() eventSources?: EventSourceInput[]; @Input() allDayDefault?: boolean; // A parameter of this name will be sent to each JSON event feed. It describes the start of the interval being fetched. @Input() startParam?: string; // A parameter of this name will be sent to each JSON event feed. It describes the exclusive end of the interval being fetched. @Input() endParam?: string; // Determines when event fetching should occur. @Input() lazyFetching?: boolean; @Input() nextDayThreshold?: DurationInput; @Input() eventOrder?: | string | Array< | ((a: EventApi, b: EventApi) => number) | (string | ((a: EventApi, b: EventApi) => number)) >; @Input() rerenderDelay?: number | null; // Time it takes for an event to revert to its original position after an unsuccessful drag. @Input() dragRevertDuration?: number; // Whether to automatically scoll the scroll-containers during event drag-and-drop and date selecting. @Input() dragScroll?: boolean; @Input() longPressDelay?: number; @Input() eventLongPressDelay?: number; // Determines if external draggable elements or events from other calendars can be dropped onto the calendar. @Input() droppable?: boolean; // Provides a way to filter which external elements can be dropped onto the calendar. @Input() dropAccept?: string | ((draggable: any) => boolean); @Input() eventDataTransform?: EventInputTransformer; @Input() allDayMaintainDuration?: boolean; @Input() eventResizableFromStart?: boolean; @Input() timeGridEventMinHeight?: number; @Input() allDayHtml?: string; @Input() eventDragMinDistance?: number; // 当任何事件源失败时调用 @Input() eventSourceFailure?: EventSourceErrorResponseHandler; // A function that gets called when fetching succeeds. @Input() eventSourceSuccess?: EventSourceSuccessResponseHandler; @Input() forceEventDuration?: boolean; @Input() progressiveEventRendering?: boolean; @Input() selectLongPressDelay?: number; // A parameter of this name will be sent to each JSON event feed. It describes the timezone of the startParam and endParam values @Input() timeZoneParam?: string; @Input() titleRangeSeparator?: string; // compound OptionsInput... @Input() buttonText?: ButtonTextCompoundInput; @Input() views?: { [viewId: string]: ViewOptionsInput }; @Input() plugins?: (PluginDef | string)[]; // scheduler... @Input() schedulerLicenseKey?: string; @Input() resources?: any; @Input() resourceLabelText?: string; @Input() resourceOrder?: any; @Input() filterResourcesWithEvents?: any; @Input() resourceText?: any; @Input() resourceGroupField?: any; @Input() resourceGroupText?: any; @Input() resourceAreaWidth?: any; @Input() resourceColumns?: any; @Input() resourcesInitiallyExpanded?: any; @Input() slotWidth?: any; @Input() datesAboveResources?: any; @Input() googleCalendarApiKey?: string; @Input() refetchResourcesOnNavigate?: boolean; @Input() eventResourceEditable?: boolean; // Determines what happens upon a day heading nav-link click. @Output() navLinkDayClick = new EventEmitter(); // ?: string | ((date: Date, jsEvent: Event) => void); @Output() windowResize = new EventEmitter(); @Output() dateClick = new EventEmitter(); // 编辑事件 @Output() eventClick = new EventEmitter(); @Output() eventMouseEnter = new EventEmitter(); @Output() eventMouseLeave = new EventEmitter(); @Output() select = new EventEmitter(); @Output() unselect = new EventEmitter(); @Output() loading = new EventEmitter(); @Output() eventPositioned = new EventEmitter(); @Output() eventDragStart = new EventEmitter(); @Output() eventDragStop = new EventEmitter(); // 拖动事件 @Output() eventDrop = new EventEmitter(); @Output() eventResizeStart = new EventEmitter(); @Output() eventResizeStop = new EventEmitter(); @Output() eventResize = new EventEmitter(); @Output() drop = new EventEmitter(); @Output() eventReceive = new EventEmitter(); @Output() eventLeave = new EventEmitter(); // tslint:disable-next-line: variable-name @Output() _destroyed = new EventEmitter(); // TODO: make these inputs... @Output() viewSkeletonRender = new EventEmitter(); @Output() viewSkeletonDestroy = new EventEmitter(); @Output() datesRender = new EventEmitter(); @Output() datesDestroy = new EventEmitter(); @Output() dayRender = new EventEmitter(); @Output() eventRender = new EventEmitter(); @Output() eventDestroy = new EventEmitter(); @Output() resourceRender = new EventEmitter(); private calendar: Calendar; private dirtyProps: any = {}; private deepCopies: any = {}; // holds frozen states private _events: EventSourceInput; private farrisInstances: FarrisComponentInstanceService = null; constructor( public element: ElementRef, private configService: DefaultConfigService, public injector: Injector ) { this.farrisInstances = this.injector.get(FarrisComponentInstanceService, null); } ngAfterViewInit() { this.calendar = new Calendar( this.element.nativeElement, Object.assign( {}, this.configService.getDefaultConfig(), this.buildOptions() ) ); this.calendar.render(); if (this.element && this.farrisInstances) { this.farrisInstances.add(this.element.nativeElement, this); } } private buildOptions() { const options = {}; OUTPUT_NAMES.forEach(outputName => { options[outputName] = (...args) => { this[outputName].emit(...args); }; }); // do after outputs, so that inputs with same name override INPUT_NAMES.forEach(inputName => { let inputVal = this[inputName]; if (inputVal !== undefined) { // unfortunately FC chokes when some props are set to undefined if (inputName === "header") { inputVal = Object.assign( {}, this.configService.getDefaultConfig().header, inputVal ); } if (this.deepChangeDetection && INPUT_IS_DEEP[inputName]) { inputVal = deepCopy(inputVal); this.deepCopies[inputName] = inputVal; // side effect! } options[inputName] = inputVal; } }); return options; } /* called before ngOnChanges, allows us to manually detect input changes. called much more often than ngOnChanges. */ ngDoCheck() { if (this.calendar && this.deepChangeDetection) { // not the initial render AND we do deep-mutation checks const { deepCopies } = this; for (const inputName in INPUT_IS_DEEP) { if (INPUT_IS_DEEP.hasOwnProperty(inputName)) { let inputVal = this[inputName]; if (inputVal !== undefined) { // unfortunately FC chokes when some props are set to undefined if (!deepEqual(inputVal, deepCopies[inputName])) { const copy = deepCopy(inputVal); deepCopies[inputName] = copy; this.dirtyProps[inputName] = copy; } } } } } } /* called with confirmed changes to input references */ ngOnChanges(changes: SimpleChanges) { if (this.calendar) { // not the initial render for (const inputName in changes) { if (changes.hasOwnProperty(inputName)) { if (this.deepCopies[inputName] === undefined) { // not already handled in ngDoCheck // this.dirtyProps[inputName] = // changes[inputName].currentValue; if ( changes[inputName].currentValue instanceof Observable ) { changes[inputName].currentValue.subscribe( (val: EventSourceInput) => { this.dirtyProps[inputName] = val; } ); } else { this.dirtyProps[inputName] = changes[inputName].currentValue; } } } } } } ngAfterContentChecked() { const { dirtyProps } = this; // hold on to reference before clearing if (Object.keys(dirtyProps).length > 0) { this.dirtyProps = {}; // clear first, in case the rerender causes new dirtiness this.calendar.mutateOptions(dirtyProps, [], false, deepEqual); } } ngOnDestroy() { if (this.calendar) { this.calendar.destroy(); } this.calendar = null; } public getApi(): Calendar { return this.calendar; } public render() { this.calendar.render(); } }