import { Iterate , IterateAction } from 'iteratez' ; import { CalendarDay } from './CalendarDay' ; import { CalendarEvent } from './CalendarEvent' ; import { Constants } from './Constants' ; import { Day , DayInput } from './Day' ; import { DaySpan } from './DaySpan' ; import { Event , EventInput } from './Event' ; import { Functions as fn } from './Functions' ; import { Op } from './Operation' ; import { Parse } from './Parse' ; import { Schedule } from './Schedule' ; import { SortEvent } from './Sort' ; import { Units } from './Units' ; param day param amount returns export type CalendarMover = ( day : Day , amount : number ) => Day ; /**
 * A definition for a given [[Units]] which informs a calendar how to setup the
 * [[Calendar.span]] and how to move with [[Calendar.move]].
 */ export interface CalendarTypeDefinition { getStart ( around , size , focus ) : Day ; getEnd ( start , size , focus ) : Day ; moveStart ( day , amount ) : Day ; moveEnd ( day , amount ) : Day ; defaultInput : any ; } /**
 * A map of [[CalendarTypeDefinition]] keyed by the [[Units]].
 */ export type CalendarTypeDefinitionMap = { [ unit ] : CalendarTypeDefinition ; } ; typeparam typeparam export interface CalendarInput < T , M > { see fill ? : boolean ; see minimumSize ? : number ; see repeatCovers ? : boolean ; see listTimes ? : boolean ; see eventsOutside ? : boolean ; see updateRows ? : boolean ; see updateColumns ? : boolean ; see eventSorter ? : SortEvent < T , M > ; see events ? : EventInput < T , M > [ ] ; see type ? : Units ; see size ? : number ; see parseMeta ? ( input ) : M ; see parseData ? ( input ) : T ; /**
     * When morphing a calendar to a fewer number of days, do we want to keep
     * today in the calendar if it is already in the calendar?
     */ preferToday ? : boolean ; /**
     * What day should the calendar be based around (contain)?
     */ around ? : DayInput ; /**
     * When morphing a calendar and `preferToday` is false OR today is not in the
     * calendar AND `around` is not specified, we will pick a day at this number
     * in the current calendar (a value between 0 and 1 signifying the start and
     * and of the current calendar).
     */ otherwiseFocus ? : number ; /**
     * When morphing or creating passing a value of `true` will avoid calling
     * [[Calendar.refresh]] as is typically done right after each of those
     * functions.
     */ delayRefresh ? : boolean ; } typeparam typeparam export class Calendar < T , M > { public static readonly DEFAULT_FOCUS = 0.4999 ; /**
     * The span of days in the calendar.
     */ public span : DaySpan ; /**
     * The full span of days represented on the calendar. This may be different
     * than the [[Calendar.span]] when [[Calendar.fill]] is `true` and the
     * calendar is representing months or years and the days need to start on
     * Sunday and end on Saturday.
     */ public filled : DaySpan ; /**
     * The number of days in the calendar specified by [[Calendar.span]].
     */ public length : number ; /**
     * The type of calendar.
     */ public type : Units ; /**
     * The size of the calendar. When the calendar type is...
     *
     * - [[Units.DAY]]: The number of days in the calendar.
     * - [[Units.WEEK]]: The number of weeks in the calendar.
     * - [[Units.MONTH]]: The number of months in the calendar.
     * - [[Units.YEAR]]: The number of years in the calendar.
     */ public size : number ; /**
     * The function used to move the start day of the calendar when functions like
     * [[Calendar.next]] or [[Calendar.prev]] are called.
     */ public moveStart : CalendarMover ; /**
     * The function used to move the end day of the calendar when functions like
     * [[Calendar.next]] or [[Calendar.prev]] are called.
     */ public moveEnd : CalendarMover ; /**
     * If the calendar should be filled in so the first day of the calendar is
     * Sunday and the last day is Saturday.
     */ public fill : boolean = false ; /**
     * The minimum number of days in the calendar no matter what the type or size
     * is. This can be used to display a month with a constant number of weeks -
     * because not all months contain the same number of weeks.
     */ public minimumSize : number = 0 ; /**
     * When `true` a [[CalendarEvent]] instance exists on each [[CalendarDay]]
     * the event covers even if the event didn't start on that day.
     */ public repeatCovers : boolean = true ; /**
     * When `true` an event instance will be created for each time specified on
     * the schedule. If the schedule specifies an all day event then only one
     * event is added to a day. This is typically done when displaying days or
     * weeks and events can be displayed on a timeline.
     */ public listTimes : boolean = false ; /**
     * When `true` events will be added to days "outside" the calendar. Days
     * outside the calendar are days filled in when [[Calendar.fill]] is `true`.
     * More specifically days that are in [[Calendar.filled]] and not in
     * [[Calendar.span]].
     */ public eventsOutside : boolean = false ; /**
     * When `true` [[CalendarEvent.row]] will be set so when visually displaying
     * the event with others multi-day events will align and not overlap.
     */ public updateRows : boolean = false ; /**
     * When `true` [[CalendarEvent.col]] will be set so when visually displaying
     * the event based on start and end time any events that overlap with each
     * other will be "indented" to see the event below it.
     */ public updateColumns : boolean = false ; /**
     * The function (if any) which sorts the events on a calendar day.
     */ public eventSorter : SortEvent < T , M > = null ; param input returns public parseMeta : ( input : any ) => M = ( x => x ) ; param input returns public parseData : ( input : any ) => T = ( x => x ) ; /**
     * A selection of days on the calendar. If no days are selected this is `null`.
     * This is merely used to keep the selection flags in [[CalendarDay]] updated
     * via [[Calendar.refreshSelection]].
     */ public selection : DaySpan = null ; /**
     * The array of days in this calendar and their events.
     */ public days : CalendarDay < T , M > [ ] = [ ] ; /**
     * The array of scheduled events added to the calendar.
     */ public events : Event < T , M > [ ] = [ ] ; /**
     * The array of visible events on the calendar. This is built based on the
     * span of the schedule in the given event and also the [[Event.visible]] flag.
     */ public visible : Event < T , M > [ ] = [ ] ; param start param end param type param size param moveStart param moveEnd param input see see see see see see public constructor ( start , end , type , size , moveStart , moveEnd , input ? ) { this . span = new DaySpan ( start , end ) ; this . filled = new DaySpan ( start , end ) ; this . type = type ; this . size = size ; this . moveStart = moveStart ; this . moveEnd = moveEnd ; if ( fn . isDefined ( input ) ) { this . set ( input ) ; } else { this . refresh ( ) ; } } param input public set ( input ) { type CTD = CalendarTypeDefinition ; const typeChange = fn . isDefined ( input . type ) && input . type !== this . type ; const sizeChange = fn . isDefined ( input . size ) && input . size !== this . size ; if ( typeChange || sizeChange ) { const focus = fn . coalesce ( input . otherwiseFocus , Calendar . DEFAULT_FOCUS ) ; const prefer = fn . coalesce ( input . preferToday , true ) ; const size = fn . coalesce ( input . size , this . size ) ; const type = fn . coalesce ( input . type , this . type ) ; const today = Day . today ( ) ; let around = fn . coalesce ( input . around , this . days [ Math . floor ( ( this . days . length - 1 ) * focus ) ] ) ; if ( ! around || ( prefer && this . span . matchesDay ( today ) ) ) { around = today ; } const meta = Calendar . TYPES [ type ] ; const start = meta . getStart ( Day . parse ( around ) , size , focus ) ; const end = meta . getEnd ( start , size , focus ) ; this . span . start = start ; this . span . end = end ; this . type = type ; this . size = size ; this . moveStart = meta . moveStart ; this . moveEnd = meta . moveEnd ; } else if ( input . around ) { const focus = fn . coalesce ( input . otherwiseFocus , Calendar . DEFAULT_FOCUS ) ; const around = Day . parse ( input . around ) ; const type = this . type ; const size = this . size ; const meta = Calendar . TYPES [ type ] ; const start = meta . getStart ( around , size , focus ) ; const end = meta . getEnd ( start , size , focus ) ; this . span . start = start ; this . span . end = end ; } this . fill = fn . coalesce ( input . fill , this . fill ) ; this . minimumSize = fn . coalesce ( input . minimumSize , this . minimumSize ) ; this . repeatCovers = fn . coalesce ( input . repeatCovers , this . repeatCovers ) ; this . listTimes = fn . coalesce ( input . listTimes , this . listTimes ) ; this . eventsOutside = fn . coalesce ( input . eventsOutside , this . eventsOutside ) ; this . updateRows = fn . coalesce ( input . updateRows , this . updateRows ) ; this . updateColumns = fn . coalesce ( input . updateColumns , this . updateColumns ) ; this . eventSorter = fn . coalesce ( input . eventSorter , this . eventSorter ) ; this . parseMeta = fn . coalesce ( input . parseMeta , this . parseMeta ) ; this . parseData = fn . coalesce ( input . parseData , this . parseData ) ; if ( fn . isArray ( input . events ) ) { this . setEvents ( input . events , true ) ; } if ( ! input . delayRefresh ) { this . refresh ( ) ; } return this ; } param minimumSize public withMinimumSize ( minimumSize ) { this . minimumSize = minimumSize ; this . refresh ( ) ; return this ; } param repeatCovers public withRepeatCovers ( repeatCovers ) { this . repeatCovers = repeatCovers ; this . refreshEvents ( ) ; return this ; } param listTimes public withListTimes ( listTimes ) { this . listTimes = listTimes ; this . refreshEvents ( ) ; return this ; } param eventsOutside public withEventsOutside ( eventsOutside ) { this . eventsOutside = eventsOutside ; this . refreshEvents ( ) ; return this ; } param updateRows param refresh public withUpdateRows ( updateRows , refresh = true ) { this . updateRows = updateRows ; if ( refresh && updateRows ) { this . refreshRows ( ) ; } return this ; } param updateColumns param refresh public withUpdateColumns ( updateColumns , refresh = true ) { this . updateColumns = updateColumns ; if ( refresh && updateColumns ) { this . refreshColumns ( ) ; } return this ; } /**
     * Returns the start day of the calendar. If this calendar is filled, this
     * may not represent the very first day in the calendar.
     */ public get start ( ) : Day { return this . span . start ; } /**
     * Returns the end day of the calendar. If this calendar is filled, this
     * may not represent the very last day in the calendar.
     */ public get end ( ) : Day { return this . span . end ; } param dayOfWeek param short param repeat param contextual param delimiter see public summary ( dayOfWeek = true , short = false , repeat = false , contextual = true , delimiter = ' - ' ) { return this . span . summary ( this . type , dayOfWeek , short , repeat , contextual , delimiter ) ; } param by returns public split ( by = 1 ) { return new Iterate < Calendar < T , M > , number , Calendar < T , M > > ( iterator => { let start = this . start ; let end = this . moveEnd ( this . end , by - this . size ) ; for ( let i = 0 ; i < this . size ; i ++ ) { const calendar = new Calendar ( start , end , this . type , by , this . moveStart , this . moveEnd , this ) ; if ( iterator . act ( calendar , i ) === IterateAction . STOP ) { return ; } start = this . moveStart ( start , by ) ; end = this . moveEnd ( end , by ) ; } } ) ; } param today public refresh ( today = Day . today ( ) ) { this . length = this . span . days ( Op . UP , true ) ; this . resetDays ( ) ; this . refreshCurrent ( today ) ; this . refreshSelection ( ) ; this . refreshVisible ( ) ; this . refreshEvents ( ) ; return this ; } /**
     * Updates the [[Calendar.filled]] span based on [[Calendar.start]],
     * [[Calendar.end]], and [[Calendar.fill]] properties.
     */ public resetFilled ( ) { this . filled . start = this . fill ? this . start . startOf ( 'week' ) : this . start ; this . filled . end = this . fill ? this . end . endOf ( 'week' ) : this . end ; return this ; } /**
     * Updates [[Calendar.days]] to match the span of days in the calendar.
     */ public resetDays ( ) { this . resetFilled ( ) ; const days = this . days ; const filled = this . filled ; const daysBetween = filled . days ( Op . UP ) ; const total = Math . max ( this . minimumSize , daysBetween ) ; let current = filled . start ; for ( let i = 0 ; i < total ; i ++ ) { let day = days [ i ] ; if ( ! day || ! day . sameDay ( current ) ) { day = new CalendarDay < T , M > ( current . date ) ; if ( i < days . length ) { days . splice ( i , 1 , day ) ; } else { days . push ( day ) ; } } day . inCalendar = this . span . contains ( day ) ; current = current . next ( ) ; } if ( days . length > total ) { days . splice ( total , days . length - total ) ; } return this ; } /**
     * Updates the list of visible schedules.
     */ public refreshVisible ( ) { const start = this . filled . start ; const end = this . filled . end ; this . visible = this . events . filter ( e => { return e . visible && e . schedule . matchesRange ( start , end ) ; } ) ; return this ; } param today public refreshCurrent ( today = Day . today ( ) ) { this . iterateDays ( ) . each ( d => d . updateCurrent ( today ) ) ; return this ; } /**
     * Updates the selection flags in [[CalendarDay]] based on the
     * [[Calendar.selection]] property.
     */ public refreshSelection ( ) { this . iterateDays ( ) . each ( d => { if ( this . selection ) { d . updateSelected ( this . selection ) ; } else { d . clearSelected ( ) ; } } ) ; return this ; } /**
     * Updates the [[CalendarDay.events]] based on the events in this calendar
     * and the following properties:
     *
     * - [[Calendar.eventsForDay]]
     * - [[Calendar.eventsOutside]]
     * - [[Calendar.listTimes]]
     * - [[Calendar.repeatCovers]]
     * - [[Calendar.updateRows]]
     * - [[Calendar.updateColumns]]
     */ public refreshEvents ( ) { this . iterateDays ( ) . each ( d => { if ( d . inCalendar || this . eventsOutside ) { d . events = this . eventsForDay ( d , this . listTimes , this . repeatCovers ) ; } } ) ; if ( this . updateRows ) { this . refreshRows ( ) ; } if ( this . updateColumns ) { this . refreshColumns ( ) ; } return this ; } /**
     * Refreshes the [[CalendarEvent.row]] property as described in the link.
     */ public refreshRows ( ) { type EventToRowMap = { [ id ] : number ; } ; type UsedMap = { [ row ] : boolean ; } ; const onlyFullDay = this . listTimes ; let eventToRow = { } ; this . iterateDays ( ) . each ( d => { if ( d . dayOfWeek === 0 ) { eventToRow = { } ; } const used = { } ; for ( const event of d . events ) { if ( onlyFullDay && ! event . fullDay ) { continue ; } if ( event . id in eventToRow ) { used [ event . row = eventToRow [ event . id ] ] = true ; } } let rowIndex = 0 ; for ( const event of d . events ) { if ( ( onlyFullDay && ! event . fullDay ) || event . id in eventToRow ) { continue ; } while ( used [ rowIndex ] ) { rowIndex ++ ; } eventToRow [ event . id ] = event . row = rowIndex ; rowIndex ++ ; } } ) ; return this ; } /**
     * Refreshes the [[CalendarEvent.col]] property as described in the link.
     */ public refreshColumns ( ) { interface Marker { time : number ; event : CalendarEvent < T , M > ; start : boolean ; parent : Marker ; } this . iterateDays ( ) . each ( d => { const markers = [ ] ; for ( const event of d . events ) { if ( ! event . fullDay ) { markers . push ( { event , time : event . time . start . time , start : true , parent : null } ) ; markers . push ( { event , time : event . time . end . time - 1 , start : false , parent : null } ) ; } } markers . sort ( ( a , b ) => { return a . time - b . time ; } ) ; let parent = null ; for ( const marker of markers ) { if ( marker . start ) { marker . parent = parent ; parent = marker ; } else if ( parent ) { parent = parent . parent ; } } for ( const marker of markers ) { if ( marker . start ) { marker . event . col = marker . parent ? marker . parent . event . col + 1 : 0 ; } } } ) ; return this ; } param input returns public getDay ( input ) { const parsed = Day . parse ( input ) ; if ( parsed ) { const dayCount = parsed . startOf ( 'day' ) . daysBetween ( this . days [ 0 ] , Op . DOWN , false ) ; return this . days [ dayCount ] ; } return null ; } param iterator public iterateDays ( ) { return new Iterate < CalendarDay < T , M > , number , Calendar < T , M > > ( iterator => { const days = this . days ; for ( let i = 0 ; i < days . length ; i ++ ) { switch ( iterator . act ( days [ i ] , i ) ) { case IterateAction . STOP : return ; } } } ) ; } param day param getTimes param covers param sorter returns public eventsForDay ( day , getTimes = true , covers = true , sorter = this . eventSorter ) { const events = [ ] ; const entries = this . visible ; for ( let entryIndex = 0 ; entryIndex < entries . length ; entryIndex ++ ) { const entry = entries [ entryIndex ] ; const schedule = entry . schedule ; const eventId = entryIndex * Constants . MAX_EVENTS_PER_DAY ; let timeIndex = 0 ; schedule . iterateSpans ( day , covers ) . each ( ( span , key , iterator ) => { events . push ( new CalendarEvent ( eventId + timeIndex ++ , entry , span , day ) ) ; if ( ! getTimes ) { iterator . stop ( ) ; } } ) ; } if ( sorter ) { events . sort ( sorter ) ; } return events ; } param input returns public findEvent ( id ) { for ( const event of this . events ) { if ( event === id || event . schedule === id || event . data === id || event . id === id ) { return event ; } } return null ; } param events param delayRefresh see see public removeEvents ( events = null , delayRefresh = false ) { if ( events ) { for ( const event of events ) { this . removeEvent ( event , true ) ; } } else { this . events = [ ] ; } this . refreshVisible ( ) ; if ( ! delayRefresh ) { this . refreshEvents ( ) ; } return this ; } param event param delayRefresh see public removeEvent ( event , delayRefresh = false ) { const found = this . findEvent ( event ) ; if ( found ) { this . events . splice ( this . events . indexOf ( found ) , 1 ) ; this . refreshVisible ( ) ; if ( ! delayRefresh ) { this . refreshEvents ( ) ; } } return this ; } param event param allowDuplicates param delayRefresh see public addEvent ( event , allowDuplicates = false , delayRefresh = false ) { const parsed = Parse . event < T , M > ( event , this . parseData , this . parseMeta ) ; if ( ! allowDuplicates ) { const existing = this . findEvent ( parsed ) ; if ( existing ) { return this ; } } this . events . push ( parsed ) ; this . refreshVisible ( ) ; if ( ! delayRefresh ) { this . refreshEvents ( ) ; } return this ; } param events param allowDuplicates param delayRefresh see public addEvents ( events , allowDuplicates = false , delayRefresh = false ) { for ( const event of events ) { this . addEvent ( event , allowDuplicates , true ) ; } if ( ! delayRefresh ) { this . refreshEvents ( ) ; } return this ; } param events param delayRefresh see public setEvents ( events , delayRefresh = false ) { const parsedEvents = [ ] ; for ( const event of events ) { const parsed = Parse . event < T , M > ( event , this . parseData , this . parseMeta ) ; if ( parsed ) { parsedEvents . push ( parsed ) ; } } this . events = parsedEvents ; this . refreshVisible ( ) ; if ( ! delayRefresh ) { this . refreshEvents ( ) ; } return this ; } param start param end see public select ( start , end = start ) { this . selection = new DaySpan ( start , end ) ; this . refreshSelection ( ) ; return this ; } see public unselect ( ) { this . selection = null ; this . refreshSelection ( ) ; return this ; } param jump param delayRefresh public move ( jump = this . size , delayRefresh = false ) { this . span . start = this . moveStart ( this . start , jump ) ; this . span . end = this . moveEnd ( this . end , jump ) ; if ( ! delayRefresh ) { this . refresh ( ) ; } return this ; } param jump param delayRefresh public next ( jump = this . size , delayRefresh = false ) { return this . move ( jump , delayRefresh ) ; } param jump param delayRefresh public prev ( jump = this . size , delayRefresh = false ) { return this . move ( - jump , delayRefresh ) ; } param plain param plainData param plainMeta returns public toInput ( plain = false , plainData = d => d , plainMeta = m => m ) { const out = { } ; out . type = this . type ; out . size = this . size ; out . fill = this . fill ; out . minimumSize = this . minimumSize ; out . repeatCovers = this . repeatCovers ; out . listTimes = this . listTimes ; out . eventsOutside = this . eventsOutside ; out . updateRows = this . updateRows ; out . updateColumns = this . updateColumns ; out . around = plain ? this . span . start . time : this . span . start ; out . events = [ ] ; for ( const event of this . events ) { if ( plain ) { const plainEvent = { } ; if ( fn . isDefined ( event . id ) ) { plainEvent . id = event . id ; } if ( fn . isDefined ( event . data ) ) { plainEvent . data = plainData ( event . data ) ; } if ( ! event . visible ) { plainEvent . visible = event . visible ; } plainEvent . schedule = event . schedule . toInput ( ) ; const meta = plainEvent . schedule . meta ; if ( meta ) { for ( const identifier in meta ) { meta [ identifier ] = plainMeta ( meta [ identifier ] ) ; } } out . events . push ( plainEvent ) ; } else { out . events . push ( event ) ; } } return out ; } param input returns public static fromInput < T , M > ( input ) { const initial = Day . today ( ) ; return new Calendar ( initial , initial , null , 1 , null , null , input ) ; } param type param days param around param focus param input returns public static forType < T , M > ( type , size = 1 , around = Day . today ( ) , focus = 0.49999 , input ? ) { const meta = this . TYPES [ type ] ; const start = meta . getStart ( around , size , focus ) ; const end = meta . getEnd ( start , size , focus ) ; return new Calendar < T , M > ( start , end , type , size , meta . moveStart , meta . moveEnd , input || meta . defaultInput ) ; } param days param around param focus param input returns see public static days < T , M > ( days = 1 , around = Day . today ( ) , focus = 0.4999 , input ? ) { return this . forType ( Units . DAY , days , around , focus , input ) ; } param days param around param focus param input returns see public static weeks < T , M > ( weeks = 1 , around = Day . today ( ) , focus = 0.4999 , input ? ) { return this . forType ( Units . WEEK , weeks , around , focus , input ) ; } param days param around param focus param input returns see public static months < T , M > ( months = 1 , around = Day . today ( ) , focus = 0.4999 , input ? ) { return this . forType ( Units . MONTH , months , around , focus , input ) ; } param days param around param focus param input returns see public static years < T , M > ( years = 1 , around = Day . today ( ) , focus = 0.4999 , input ? ) { return this . forType ( Units . YEAR , years , around , focus , input ) ; } /**
     * A map of functions and properties by [[Units]] used to create or morph
     * Calendars.
     */ public static TYPES : CalendarTypeDefinitionMap = { [ Units . DAY ] : { getStart ( around , size , focus ) { return around . startOf ( 'day' ) . add ( 'day' , - Math . floor ( size * focus ) ) ; } , getEnd ( start , size , focus ) { return start . add ( 'day' , size - 1 ) . endOf ( 'day' ) ; } , moveStart ( day , amount ) { return day . add ( 'day' , amount ) ; } , moveEnd ( day , amount ) { return day . add ( 'day' , amount ) ; } , defaultInput : undefined } , [ Units . WEEK ] : { getStart ( around , size , focus ) { return around . startOf ( 'week' ) . add ( 'week' , - Math . floor ( size * focus ) ) ; } , getEnd ( start , size , focus ) { return start . add ( 'week' , size - 1 ) . endOf ( 'week' ) ; } , moveStart ( day , amount ) { return day . add ( 'week' , amount ) ; } , moveEnd ( day , amount ) { return day . add ( 'week' , amount ) ; } , defaultInput : undefined } , [ Units . MONTH ] : { getStart ( around , size , focus ) { return around . startOf ( 'month' ) . add ( 'month' , - Math . floor ( size * focus ) ) ; } , getEnd ( start , size , focus ) { return start . add ( 'month' , size - 1 ) . endOf ( 'month' ) ; } , moveStart ( day , amount ) { return day . add ( 'month' , amount ) ; } , moveEnd ( day , amount ) { return day . startOf ( 'month' ) . add ( 'month' , amount ) . endOf ( 'month' ) ; } , defaultInput : { fill : true } } , [ Units . YEAR ] : { getStart ( around , size , focus ) { return around . startOf ( 'year' ) . add ( 'year' , - Math . floor ( size * focus ) ) ; } , getEnd ( start , size , focus ) { return start . add ( 'year' , size - 1 ) . endOf ( 'year' ) ; } , moveStart ( day , amount ) { return day . add ( 'year' , amount ) ; } , moveEnd ( day , amount ) { return day . add ( 'year' , amount ) ; } , defaultInput : { fill : true } } } ; }