fullcalendar.js 305 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878
  1. /*!
  2. * FullCalendar v2.3.2
  3. * Docs & License: http://fullcalendar.io/
  4. * (c) 2015 Adam Shaw
  5. */
  6. (function(factory) {
  7. if (typeof define === 'function' && define.amd) {
  8. define([ 'jquery', 'moment' ], factory);
  9. }
  10. else if (typeof exports === 'object') { // Node/CommonJS
  11. module.exports = factory(require('jquery'), require('moment'));
  12. }
  13. else {
  14. factory(jQuery, moment);
  15. }
  16. })(function($, moment) {
  17. ;;
  18. var fc = $.fullCalendar = { version: "2.3.2" };
  19. var fcViews = fc.views = {};
  20. $.fn.fullCalendar = function(options) {
  21. var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  22. var res = this; // what this function will return (this jQuery object by default)
  23. this.each(function(i, _element) { // loop each DOM element involved
  24. var element = $(_element);
  25. var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  26. var singleRes; // the returned value of this single method call
  27. // a method call
  28. if (typeof options === 'string') {
  29. if (calendar && $.isFunction(calendar[options])) {
  30. singleRes = calendar[options].apply(calendar, args);
  31. if (!i) {
  32. res = singleRes; // record the first method call result
  33. }
  34. if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  35. element.removeData('fullCalendar');
  36. }
  37. }
  38. }
  39. // a new calendar initialization
  40. else if (!calendar) { // don't initialize twice
  41. calendar = new Calendar(element, options);
  42. element.data('fullCalendar', calendar);
  43. calendar.render();
  44. }
  45. });
  46. return res;
  47. };
  48. var complexOptions = [ // names of options that are objects whose properties should be combined
  49. 'header',
  50. 'buttonText',
  51. 'buttonIcons',
  52. 'themeButtonIcons'
  53. ];
  54. // Merges an array of option objects into a single object
  55. function mergeOptions(optionObjs) {
  56. return mergeProps(optionObjs, complexOptions);
  57. }
  58. // Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
  59. // Converts View-Option-Hashes into the View-Specific-Options format.
  60. function massageOverrides(input) {
  61. var overrides = { views: input.views || {} }; // the output. ensure a `views` hash
  62. var subObj;
  63. // iterate through all option override properties (except `views`)
  64. $.each(input, function(name, val) {
  65. if (name != 'views') {
  66. // could the value be a legacy View-Option-Hash?
  67. if (
  68. $.isPlainObject(val) &&
  69. !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
  70. $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes
  71. ) {
  72. subObj = null;
  73. // iterate through the properties of this possible View-Option-Hash value
  74. $.each(val, function(subName, subVal) {
  75. // is the property targeting a view?
  76. if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
  77. if (!overrides.views[subName]) { // ensure the view-target entry exists
  78. overrides.views[subName] = {};
  79. }
  80. overrides.views[subName][name] = subVal; // record the value in the `views` object
  81. }
  82. else { // a non-View-Option-Hash property
  83. if (!subObj) {
  84. subObj = {};
  85. }
  86. subObj[subName] = subVal; // accumulate these unrelated values for later
  87. }
  88. });
  89. if (subObj) { // non-View-Option-Hash properties? transfer them as-is
  90. overrides[name] = subObj;
  91. }
  92. }
  93. else {
  94. overrides[name] = val; // transfer normal options as-is
  95. }
  96. }
  97. });
  98. return overrides;
  99. }
  100. ;;
  101. // exports
  102. fc.intersectionToSeg = intersectionToSeg;
  103. fc.applyAll = applyAll;
  104. fc.debounce = debounce;
  105. fc.isInt = isInt;
  106. fc.htmlEscape = htmlEscape;
  107. fc.cssToStr = cssToStr;
  108. fc.proxy = proxy;
  109. fc.capitaliseFirstLetter = capitaliseFirstLetter;
  110. /* FullCalendar-specific DOM Utilities
  111. ----------------------------------------------------------------------------------------------------------------------*/
  112. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  113. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  114. function compensateScroll(rowEls, scrollbarWidths) {
  115. if (scrollbarWidths.left) {
  116. rowEls.css({
  117. 'border-left-width': 1,
  118. 'margin-left': scrollbarWidths.left - 1
  119. });
  120. }
  121. if (scrollbarWidths.right) {
  122. rowEls.css({
  123. 'border-right-width': 1,
  124. 'margin-right': scrollbarWidths.right - 1
  125. });
  126. }
  127. }
  128. // Undoes compensateScroll and restores all borders/margins
  129. function uncompensateScroll(rowEls) {
  130. rowEls.css({
  131. 'margin-left': '',
  132. 'margin-right': '',
  133. 'border-left-width': '',
  134. 'border-right-width': ''
  135. });
  136. }
  137. // Make the mouse cursor express that an event is not allowed in the current area
  138. function disableCursor() {
  139. $('body').addClass('fc-not-allowed');
  140. }
  141. // Returns the mouse cursor to its original look
  142. function enableCursor() {
  143. $('body').removeClass('fc-not-allowed');
  144. }
  145. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  146. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  147. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  148. // reduces the available height.
  149. function distributeHeight(els, availableHeight, shouldRedistribute) {
  150. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  151. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  152. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  153. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  154. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  155. var flexOffsets = []; // amount of vertical space it takes up
  156. var flexHeights = []; // actual css height
  157. var usedHeight = 0;
  158. undistributeHeight(els); // give all elements their natural height
  159. // find elements that are below the recommended height (expandable).
  160. // important to query for heights in a single first pass (to avoid reflow oscillation).
  161. els.each(function(i, el) {
  162. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  163. var naturalOffset = $(el).outerHeight(true);
  164. if (naturalOffset < minOffset) {
  165. flexEls.push(el);
  166. flexOffsets.push(naturalOffset);
  167. flexHeights.push($(el).height());
  168. }
  169. else {
  170. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  171. usedHeight += naturalOffset;
  172. }
  173. });
  174. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  175. if (shouldRedistribute) {
  176. availableHeight -= usedHeight;
  177. minOffset1 = Math.floor(availableHeight / flexEls.length);
  178. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  179. }
  180. // assign heights to all expandable elements
  181. $(flexEls).each(function(i, el) {
  182. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  183. var naturalOffset = flexOffsets[i];
  184. var naturalHeight = flexHeights[i];
  185. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  186. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  187. $(el).height(newHeight);
  188. }
  189. });
  190. }
  191. // Undoes distrubuteHeight, restoring all els to their natural height
  192. function undistributeHeight(els) {
  193. els.height('');
  194. }
  195. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  196. // cells to be that width.
  197. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  198. function matchCellWidths(els) {
  199. var maxInnerWidth = 0;
  200. els.find('> *').each(function(i, innerEl) {
  201. var innerWidth = $(innerEl).outerWidth();
  202. if (innerWidth > maxInnerWidth) {
  203. maxInnerWidth = innerWidth;
  204. }
  205. });
  206. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  207. els.width(maxInnerWidth);
  208. return maxInnerWidth;
  209. }
  210. // Turns a container element into a scroller if its contents is taller than the allotted height.
  211. // Returns true if the element is now a scroller, false otherwise.
  212. // NOTE: this method is best because it takes weird zooming dimensions into account
  213. function setPotentialScroller(containerEl, height) {
  214. containerEl.height(height).addClass('fc-scroller');
  215. // are scrollbars needed?
  216. if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  217. return true;
  218. }
  219. unsetScroller(containerEl); // undo
  220. return false;
  221. }
  222. // Takes an element that might have been a scroller, and turns it back into a normal element.
  223. function unsetScroller(containerEl) {
  224. containerEl.height('').removeClass('fc-scroller');
  225. }
  226. /* General DOM Utilities
  227. ----------------------------------------------------------------------------------------------------------------------*/
  228. fc.getClientRect = getClientRect;
  229. fc.getContentRect = getContentRect;
  230. fc.getScrollbarWidths = getScrollbarWidths;
  231. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  232. function getScrollParent(el) {
  233. var position = el.css('position'),
  234. scrollParent = el.parents().filter(function() {
  235. var parent = $(this);
  236. return (/(auto|scroll)/).test(
  237. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  238. );
  239. }).eq(0);
  240. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  241. }
  242. // Queries the outer bounding area of a jQuery element.
  243. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  244. function getOuterRect(el) {
  245. var offset = el.offset();
  246. return {
  247. left: offset.left,
  248. right: offset.left + el.outerWidth(),
  249. top: offset.top,
  250. bottom: offset.top + el.outerHeight()
  251. };
  252. }
  253. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  254. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  255. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  256. function getClientRect(el) {
  257. var offset = el.offset();
  258. var scrollbarWidths = getScrollbarWidths(el);
  259. var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left;
  260. var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top;
  261. return {
  262. left: left,
  263. right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  264. top: top,
  265. bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  266. };
  267. }
  268. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  269. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  270. function getContentRect(el) {
  271. var offset = el.offset(); // just outside of border, margin not included
  272. var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left');
  273. var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top');
  274. return {
  275. left: left,
  276. right: left + el.width(),
  277. top: top,
  278. bottom: top + el.height()
  279. };
  280. }
  281. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  282. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  283. function getScrollbarWidths(el) {
  284. var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
  285. var widths = {
  286. left: 0,
  287. right: 0,
  288. top: 0,
  289. bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
  290. };
  291. if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  292. widths.left = leftRightWidth;
  293. }
  294. else {
  295. widths.right = leftRightWidth;
  296. }
  297. return widths;
  298. }
  299. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  300. var _isLeftRtlScrollbars = null;
  301. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  302. if (_isLeftRtlScrollbars === null) {
  303. _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  304. }
  305. return _isLeftRtlScrollbars;
  306. }
  307. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  308. var el = $('<div><div/></div>')
  309. .css({
  310. position: 'absolute',
  311. top: -1000,
  312. left: 0,
  313. border: 0,
  314. padding: 0,
  315. overflow: 'scroll',
  316. direction: 'rtl'
  317. })
  318. .appendTo('body');
  319. var innerEl = el.children();
  320. var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  321. el.remove();
  322. return res;
  323. }
  324. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  325. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  326. function getCssFloat(el, prop) {
  327. return parseFloat(el.css(prop)) || 0;
  328. }
  329. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  330. function isPrimaryMouseButton(ev) {
  331. return ev.which == 1 && !ev.ctrlKey;
  332. }
  333. /* Geometry
  334. ----------------------------------------------------------------------------------------------------------------------*/
  335. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  336. function intersectRects(rect1, rect2) {
  337. var res = {
  338. left: Math.max(rect1.left, rect2.left),
  339. right: Math.min(rect1.right, rect2.right),
  340. top: Math.max(rect1.top, rect2.top),
  341. bottom: Math.min(rect1.bottom, rect2.bottom)
  342. };
  343. if (res.left < res.right && res.top < res.bottom) {
  344. return res;
  345. }
  346. return false;
  347. }
  348. // Returns a new point that will have been moved to reside within the given rectangle
  349. function constrainPoint(point, rect) {
  350. return {
  351. left: Math.min(Math.max(point.left, rect.left), rect.right),
  352. top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  353. };
  354. }
  355. // Returns a point that is the center of the given rectangle
  356. function getRectCenter(rect) {
  357. return {
  358. left: (rect.left + rect.right) / 2,
  359. top: (rect.top + rect.bottom) / 2
  360. };
  361. }
  362. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  363. function diffPoints(point1, point2) {
  364. return {
  365. left: point1.left - point2.left,
  366. top: point1.top - point2.top
  367. };
  368. }
  369. /* FullCalendar-specific Misc Utilities
  370. ----------------------------------------------------------------------------------------------------------------------*/
  371. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  372. // Expects all dates to be normalized to the same timezone beforehand.
  373. // TODO: move to date section?
  374. function intersectionToSeg(subjectRange, constraintRange) {
  375. var subjectStart = subjectRange.start;
  376. var subjectEnd = subjectRange.end;
  377. var constraintStart = constraintRange.start;
  378. var constraintEnd = constraintRange.end;
  379. var segStart, segEnd;
  380. var isStart, isEnd;
  381. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  382. if (subjectStart >= constraintStart) {
  383. segStart = subjectStart.clone();
  384. isStart = true;
  385. }
  386. else {
  387. segStart = constraintStart.clone();
  388. isStart = false;
  389. }
  390. if (subjectEnd <= constraintEnd) {
  391. segEnd = subjectEnd.clone();
  392. isEnd = true;
  393. }
  394. else {
  395. segEnd = constraintEnd.clone();
  396. isEnd = false;
  397. }
  398. return {
  399. start: segStart,
  400. end: segEnd,
  401. isStart: isStart,
  402. isEnd: isEnd
  403. };
  404. }
  405. }
  406. /* Date Utilities
  407. ----------------------------------------------------------------------------------------------------------------------*/
  408. fc.computeIntervalUnit = computeIntervalUnit;
  409. fc.durationHasTime = durationHasTime;
  410. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  411. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  412. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  413. // Moments will have their timezones normalized.
  414. function diffDayTime(a, b) {
  415. return moment.duration({
  416. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  417. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  418. });
  419. }
  420. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  421. function diffDay(a, b) {
  422. return moment.duration({
  423. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  424. });
  425. }
  426. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  427. function diffByUnit(a, b, unit) {
  428. return moment.duration(
  429. Math.round(a.diff(b, unit, true)), // returnFloat=true
  430. unit
  431. );
  432. }
  433. // Computes the unit name of the largest whole-unit period of time.
  434. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  435. // Accepts start/end, a range object, or an original duration object.
  436. function computeIntervalUnit(start, end) {
  437. var i, unit;
  438. var val;
  439. for (i = 0; i < intervalUnits.length; i++) {
  440. unit = intervalUnits[i];
  441. val = computeRangeAs(unit, start, end);
  442. if (val >= 1 && isInt(val)) {
  443. break;
  444. }
  445. }
  446. return unit; // will be "milliseconds" if nothing else matches
  447. }
  448. // Computes the number of units (like "hours") in the given range.
  449. // Range can be a {start,end} object, separate start/end args, or a Duration.
  450. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  451. // of month-diffing logic (which tends to vary from version to version).
  452. function computeRangeAs(unit, start, end) {
  453. if (end != null) { // given start, end
  454. return end.diff(start, unit, true);
  455. }
  456. else if (moment.isDuration(start)) { // given duration
  457. return start.as(unit);
  458. }
  459. else { // given { start, end } range object
  460. return start.end.diff(start.start, unit, true);
  461. }
  462. }
  463. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  464. function durationHasTime(dur) {
  465. return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  466. }
  467. function isNativeDate(input) {
  468. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  469. }
  470. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  471. function isTimeString(str) {
  472. return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  473. }
  474. /* General Utilities
  475. ----------------------------------------------------------------------------------------------------------------------*/
  476. var hasOwnPropMethod = {}.hasOwnProperty;
  477. // Merges an array of objects into a single object.
  478. // The second argument allows for an array of property names who's object values will be merged together.
  479. function mergeProps(propObjs, complexProps) {
  480. var dest = {};
  481. var i, name;
  482. var complexObjs;
  483. var j, val;
  484. var props;
  485. if (complexProps) {
  486. for (i = 0; i < complexProps.length; i++) {
  487. name = complexProps[i];
  488. complexObjs = [];
  489. // collect the trailing object values, stopping when a non-object is discovered
  490. for (j = propObjs.length - 1; j >= 0; j--) {
  491. val = propObjs[j][name];
  492. if (typeof val === 'object') {
  493. complexObjs.unshift(val);
  494. }
  495. else if (val !== undefined) {
  496. dest[name] = val; // if there were no objects, this value will be used
  497. break;
  498. }
  499. }
  500. // if the trailing values were objects, use the merged value
  501. if (complexObjs.length) {
  502. dest[name] = mergeProps(complexObjs);
  503. }
  504. }
  505. }
  506. // copy values into the destination, going from last to first
  507. for (i = propObjs.length - 1; i >= 0; i--) {
  508. props = propObjs[i];
  509. for (name in props) {
  510. if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  511. dest[name] = props[name];
  512. }
  513. }
  514. }
  515. return dest;
  516. }
  517. // Create an object that has the given prototype. Just like Object.create
  518. function createObject(proto) {
  519. var f = function() {};
  520. f.prototype = proto;
  521. return new f();
  522. }
  523. function copyOwnProps(src, dest) {
  524. for (var name in src) {
  525. if (hasOwnProp(src, name)) {
  526. dest[name] = src[name];
  527. }
  528. }
  529. }
  530. // Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
  531. // https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
  532. function copyNativeMethods(src, dest) {
  533. var names = [ 'constructor', 'toString', 'valueOf' ];
  534. var i, name;
  535. for (i = 0; i < names.length; i++) {
  536. name = names[i];
  537. if (src[name] !== Object.prototype[name]) {
  538. dest[name] = src[name];
  539. }
  540. }
  541. }
  542. function hasOwnProp(obj, name) {
  543. return hasOwnPropMethod.call(obj, name);
  544. }
  545. // Is the given value a non-object non-function value?
  546. function isAtomic(val) {
  547. return /undefined|null|boolean|number|string/.test($.type(val));
  548. }
  549. function applyAll(functions, thisObj, args) {
  550. if ($.isFunction(functions)) {
  551. functions = [ functions ];
  552. }
  553. if (functions) {
  554. var i;
  555. var ret;
  556. for (i=0; i<functions.length; i++) {
  557. ret = functions[i].apply(thisObj, args) || ret;
  558. }
  559. return ret;
  560. }
  561. }
  562. function firstDefined() {
  563. for (var i=0; i<arguments.length; i++) {
  564. if (arguments[i] !== undefined) {
  565. return arguments[i];
  566. }
  567. }
  568. }
  569. function htmlEscape(s) {
  570. return (s + '').replace(/&/g, '&amp;')
  571. .replace(/</g, '&lt;')
  572. .replace(/>/g, '&gt;')
  573. .replace(/'/g, '&#039;')
  574. .replace(/"/g, '&quot;')
  575. .replace(/\n/g, '<br />');
  576. }
  577. function stripHtmlEntities(text) {
  578. return text.replace(/&.*?;/g, '');
  579. }
  580. // Given a hash of CSS properties, returns a string of CSS.
  581. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  582. function cssToStr(cssProps) {
  583. var statements = [];
  584. $.each(cssProps, function(name, val) {
  585. if (val != null) {
  586. statements.push(name + ':' + val);
  587. }
  588. });
  589. return statements.join(';');
  590. }
  591. function capitaliseFirstLetter(str) {
  592. return str.charAt(0).toUpperCase() + str.slice(1);
  593. }
  594. function compareNumbers(a, b) { // for .sort()
  595. return a - b;
  596. }
  597. function isInt(n) {
  598. return n % 1 === 0;
  599. }
  600. // Returns a method bound to the given object context.
  601. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  602. // different contexts as identical when binding/unbinding events.
  603. function proxy(obj, methodName) {
  604. var method = obj[methodName];
  605. return function() {
  606. return method.apply(obj, arguments);
  607. };
  608. }
  609. // Returns a function, that, as long as it continues to be invoked, will not
  610. // be triggered. The function will be called after it stops being called for
  611. // N milliseconds.
  612. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  613. function debounce(func, wait) {
  614. var timeoutId;
  615. var args;
  616. var context;
  617. var timestamp; // of most recent call
  618. var later = function() {
  619. var last = +new Date() - timestamp;
  620. if (last < wait && last > 0) {
  621. timeoutId = setTimeout(later, wait - last);
  622. }
  623. else {
  624. timeoutId = null;
  625. func.apply(context, args);
  626. if (!timeoutId) {
  627. context = args = null;
  628. }
  629. }
  630. };
  631. return function() {
  632. context = this;
  633. args = arguments;
  634. timestamp = +new Date();
  635. if (!timeoutId) {
  636. timeoutId = setTimeout(later, wait);
  637. }
  638. };
  639. }
  640. ;;
  641. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  642. var ambigTimeOrZoneRegex =
  643. /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
  644. var newMomentProto = moment.fn; // where we will attach our new methods
  645. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  646. var allowValueOptimization;
  647. var setUTCValues; // function defined below
  648. var setLocalValues; // function defined below
  649. // Creating
  650. // -------------------------------------------------------------------------------------------------
  651. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  652. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  653. // it will function as a clone (and retain the zone of the moment). Anything else will
  654. // result in a moment in the local zone.
  655. fc.moment = function() {
  656. return makeMoment(arguments);
  657. };
  658. // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
  659. fc.moment.utc = function() {
  660. var mom = makeMoment(arguments, true);
  661. // Force it into UTC because makeMoment doesn't guarantee it
  662. // (if given a pre-existing moment for example)
  663. if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  664. mom.utc();
  665. }
  666. return mom;
  667. };
  668. // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
  669. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  670. fc.moment.parseZone = function() {
  671. return makeMoment(arguments, true, true);
  672. };
  673. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  674. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  675. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  676. // parseAsUTC - if there is no zone information, should we parse the input in UTC?
  677. // parseZone - if there is zone information, should we force the zone of the moment?
  678. function makeMoment(args, parseAsUTC, parseZone) {
  679. var input = args[0];
  680. var isSingleString = args.length == 1 && typeof input === 'string';
  681. var isAmbigTime;
  682. var isAmbigZone;
  683. var ambigMatch;
  684. var mom;
  685. if (moment.isMoment(input)) {
  686. mom = moment.apply(null, args); // clone it
  687. transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
  688. }
  689. else if (isNativeDate(input) || input === undefined) {
  690. mom = moment.apply(null, args); // will be local
  691. }
  692. else { // "parsing" is required
  693. isAmbigTime = false;
  694. isAmbigZone = false;
  695. if (isSingleString) {
  696. if (ambigDateOfMonthRegex.test(input)) {
  697. // accept strings like '2014-05', but convert to the first of the month
  698. input += '-01';
  699. args = [ input ]; // for when we pass it on to moment's constructor
  700. isAmbigTime = true;
  701. isAmbigZone = true;
  702. }
  703. else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  704. isAmbigTime = !ambigMatch[5]; // no time part?
  705. isAmbigZone = true;
  706. }
  707. }
  708. else if ($.isArray(input)) {
  709. // arrays have no timezone information, so assume ambiguous zone
  710. isAmbigZone = true;
  711. }
  712. // otherwise, probably a string with a format
  713. if (parseAsUTC || isAmbigTime) {
  714. mom = moment.utc.apply(moment, args);
  715. }
  716. else {
  717. mom = moment.apply(null, args);
  718. }
  719. if (isAmbigTime) {
  720. mom._ambigTime = true;
  721. mom._ambigZone = true; // ambiguous time always means ambiguous zone
  722. }
  723. else if (parseZone) { // let's record the inputted zone somehow
  724. if (isAmbigZone) {
  725. mom._ambigZone = true;
  726. }
  727. else if (isSingleString) {
  728. if (mom.utcOffset) {
  729. mom.utcOffset(input); // if not a valid zone, will assign UTC
  730. }
  731. else {
  732. mom.zone(input); // for moment-pre-2.9
  733. }
  734. }
  735. }
  736. }
  737. mom._fullCalendar = true; // flag for extended functionality
  738. return mom;
  739. }
  740. // A clone method that works with the flags related to our enhanced functionality.
  741. // In the future, use moment.momentProperties
  742. newMomentProto.clone = function() {
  743. var mom = oldMomentProto.clone.apply(this, arguments);
  744. // these flags weren't transfered with the clone
  745. transferAmbigs(this, mom);
  746. if (this._fullCalendar) {
  747. mom._fullCalendar = true;
  748. }
  749. return mom;
  750. };
  751. // Week Number
  752. // -------------------------------------------------------------------------------------------------
  753. // Returns the week number, considering the locale's custom week number calcuation
  754. // `weeks` is an alias for `week`
  755. newMomentProto.week = newMomentProto.weeks = function(input) {
  756. var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
  757. ._fullCalendar_weekCalc;
  758. if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
  759. return weekCalc(this);
  760. }
  761. else if (weekCalc === 'ISO') {
  762. return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
  763. }
  764. return oldMomentProto.week.apply(this, arguments); // local getter/setter
  765. };
  766. // Time-of-day
  767. // -------------------------------------------------------------------------------------------------
  768. // GETTER
  769. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  770. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  771. //
  772. // SETTER
  773. // You can supply a Duration, a Moment, or a Duration-like argument.
  774. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  775. newMomentProto.time = function(time) {
  776. // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  777. // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  778. if (!this._fullCalendar) {
  779. return oldMomentProto.time.apply(this, arguments);
  780. }
  781. if (time == null) { // getter
  782. return moment.duration({
  783. hours: this.hours(),
  784. minutes: this.minutes(),
  785. seconds: this.seconds(),
  786. milliseconds: this.milliseconds()
  787. });
  788. }
  789. else { // setter
  790. this._ambigTime = false; // mark that the moment now has a time
  791. if (!moment.isDuration(time) && !moment.isMoment(time)) {
  792. time = moment.duration(time);
  793. }
  794. // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  795. // Only for Duration times, not Moment times.
  796. var dayHours = 0;
  797. if (moment.isDuration(time)) {
  798. dayHours = Math.floor(time.asDays()) * 24;
  799. }
  800. // We need to set the individual fields.
  801. // Can't use startOf('day') then add duration. In case of DST at start of day.
  802. return this.hours(dayHours + time.hours())
  803. .minutes(time.minutes())
  804. .seconds(time.seconds())
  805. .milliseconds(time.milliseconds());
  806. }
  807. };
  808. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  809. // but preserving its YMD. A moment with a stripped time will display no time
  810. // nor timezone offset when .format() is called.
  811. newMomentProto.stripTime = function() {
  812. var a;
  813. if (!this._ambigTime) {
  814. // get the values before any conversion happens
  815. a = this.toArray(); // array of y/m/d/h/m/s/ms
  816. // TODO: use keepLocalTime in the future
  817. this.utc(); // set the internal UTC flag (will clear the ambig flags)
  818. setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
  819. // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  820. // which clears all ambig flags. Same with setUTCValues with moment-timezone.
  821. this._ambigTime = true;
  822. this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  823. }
  824. return this; // for chaining
  825. };
  826. // Returns if the moment has a non-ambiguous time (boolean)
  827. newMomentProto.hasTime = function() {
  828. return !this._ambigTime;
  829. };
  830. // Timezone
  831. // -------------------------------------------------------------------------------------------------
  832. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  833. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  834. // timezone offset when .format() is called.
  835. // TODO: look into Moment's keepLocalTime functionality
  836. newMomentProto.stripZone = function() {
  837. var a, wasAmbigTime;
  838. if (!this._ambigZone) {
  839. // get the values before any conversion happens
  840. a = this.toArray(); // array of y/m/d/h/m/s/ms
  841. wasAmbigTime = this._ambigTime;
  842. this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
  843. setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
  844. // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
  845. this._ambigTime = wasAmbigTime || false;
  846. // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  847. // which clears the ambig flags. Same with setUTCValues with moment-timezone.
  848. this._ambigZone = true;
  849. }
  850. return this; // for chaining
  851. };
  852. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  853. newMomentProto.hasZone = function() {
  854. return !this._ambigZone;
  855. };
  856. // this method implicitly marks a zone
  857. newMomentProto.local = function() {
  858. var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
  859. var wasAmbigZone = this._ambigZone;
  860. oldMomentProto.local.apply(this, arguments);
  861. // ensure non-ambiguous
  862. // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
  863. this._ambigTime = false;
  864. this._ambigZone = false;
  865. if (wasAmbigZone) {
  866. // If the moment was ambiguously zoned, the date fields were stored as UTC.
  867. // We want to preserve these, but in local time.
  868. // TODO: look into Moment's keepLocalTime functionality
  869. setLocalValues(this, a);
  870. }
  871. return this; // for chaining
  872. };
  873. // implicitly marks a zone
  874. newMomentProto.utc = function() {
  875. oldMomentProto.utc.apply(this, arguments);
  876. // ensure non-ambiguous
  877. // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
  878. this._ambigTime = false;
  879. this._ambigZone = false;
  880. return this;
  881. };
  882. // methods for arbitrarily manipulating timezone offset.
  883. // should clear time/zone ambiguity when called.
  884. $.each([
  885. 'zone', // only in moment-pre-2.9. deprecated afterwards
  886. 'utcOffset'
  887. ], function(i, name) {
  888. if (oldMomentProto[name]) { // original method exists?
  889. // this method implicitly marks a zone (will probably get called upon .utc() and .local())
  890. newMomentProto[name] = function(tzo) {
  891. if (tzo != null) { // setter
  892. // these assignments needs to happen before the original zone method is called.
  893. // I forget why, something to do with a browser crash.
  894. this._ambigTime = false;
  895. this._ambigZone = false;
  896. }
  897. return oldMomentProto[name].apply(this, arguments);
  898. };
  899. }
  900. });
  901. // Formatting
  902. // -------------------------------------------------------------------------------------------------
  903. newMomentProto.format = function() {
  904. if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  905. return formatDate(this, arguments[0]); // our extended formatting
  906. }
  907. if (this._ambigTime) {
  908. return oldMomentFormat(this, 'YYYY-MM-DD');
  909. }
  910. if (this._ambigZone) {
  911. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  912. }
  913. return oldMomentProto.format.apply(this, arguments);
  914. };
  915. newMomentProto.toISOString = function() {
  916. if (this._ambigTime) {
  917. return oldMomentFormat(this, 'YYYY-MM-DD');
  918. }
  919. if (this._ambigZone) {
  920. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  921. }
  922. return oldMomentProto.toISOString.apply(this, arguments);
  923. };
  924. // Querying
  925. // -------------------------------------------------------------------------------------------------
  926. // Is the moment within the specified range? `end` is exclusive.
  927. // FYI, this method is not a standard Moment method, so always do our enhanced logic.
  928. newMomentProto.isWithin = function(start, end) {
  929. var a = commonlyAmbiguate([ this, start, end ]);
  930. return a[0] >= a[1] && a[0] < a[2];
  931. };
  932. // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
  933. // If no units specified, the two moments must be identically the same, with matching ambig flags.
  934. newMomentProto.isSame = function(input, units) {
  935. var a;
  936. // only do custom logic if this is an enhanced moment
  937. if (!this._fullCalendar) {
  938. return oldMomentProto.isSame.apply(this, arguments);
  939. }
  940. if (units) {
  941. a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
  942. return oldMomentProto.isSame.call(a[0], a[1], units);
  943. }
  944. else {
  945. input = fc.moment.parseZone(input); // normalize input
  946. return oldMomentProto.isSame.call(this, input) &&
  947. Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
  948. Boolean(this._ambigZone) === Boolean(input._ambigZone);
  949. }
  950. };
  951. // Make these query methods work with ambiguous moments
  952. $.each([
  953. 'isBefore',
  954. 'isAfter'
  955. ], function(i, methodName) {
  956. newMomentProto[methodName] = function(input, units) {
  957. var a;
  958. // only do custom logic if this is an enhanced moment
  959. if (!this._fullCalendar) {
  960. return oldMomentProto[methodName].apply(this, arguments);
  961. }
  962. a = commonlyAmbiguate([ this, input ]);
  963. return oldMomentProto[methodName].call(a[0], a[1], units);
  964. };
  965. });
  966. // Misc Internals
  967. // -------------------------------------------------------------------------------------------------
  968. // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
  969. // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
  970. // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
  971. // returns the original moments if no modifications are necessary.
  972. function commonlyAmbiguate(inputs, preserveTime) {
  973. var anyAmbigTime = false;
  974. var anyAmbigZone = false;
  975. var len = inputs.length;
  976. var moms = [];
  977. var i, mom;
  978. // parse inputs into real moments and query their ambig flags
  979. for (i = 0; i < len; i++) {
  980. mom = inputs[i];
  981. if (!moment.isMoment(mom)) {
  982. mom = fc.moment.parseZone(mom);
  983. }
  984. anyAmbigTime = anyAmbigTime || mom._ambigTime;
  985. anyAmbigZone = anyAmbigZone || mom._ambigZone;
  986. moms.push(mom);
  987. }
  988. // strip each moment down to lowest common ambiguity
  989. // use clones to avoid modifying the original moments
  990. for (i = 0; i < len; i++) {
  991. mom = moms[i];
  992. if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
  993. moms[i] = mom.clone().stripTime();
  994. }
  995. else if (anyAmbigZone && !mom._ambigZone) {
  996. moms[i] = mom.clone().stripZone();
  997. }
  998. }
  999. return moms;
  1000. }
  1001. // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
  1002. // TODO: look into moment.momentProperties for this.
  1003. function transferAmbigs(src, dest) {
  1004. if (src._ambigTime) {
  1005. dest._ambigTime = true;
  1006. }
  1007. else if (dest._ambigTime) {
  1008. dest._ambigTime = false;
  1009. }
  1010. if (src._ambigZone) {
  1011. dest._ambigZone = true;
  1012. }
  1013. else if (dest._ambigZone) {
  1014. dest._ambigZone = false;
  1015. }
  1016. }
  1017. // Sets the year/month/date/etc values of the moment from the given array.
  1018. // Inefficient because it calls each individual setter.
  1019. function setMomentValues(mom, a) {
  1020. mom.year(a[0] || 0)
  1021. .month(a[1] || 0)
  1022. .date(a[2] || 0)
  1023. .hours(a[3] || 0)
  1024. .minutes(a[4] || 0)
  1025. .seconds(a[5] || 0)
  1026. .milliseconds(a[6] || 0);
  1027. }
  1028. // Can we set the moment's internal date directly?
  1029. allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
  1030. // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
  1031. // Assumes the given moment is already in UTC mode.
  1032. setUTCValues = allowValueOptimization ? function(mom, a) {
  1033. // simlate what moment's accessors do
  1034. mom._d.setTime(Date.UTC.apply(Date, a));
  1035. moment.updateOffset(mom, false); // keepTime=false
  1036. } : setMomentValues;
  1037. // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
  1038. // Assumes the given moment is already in local mode.
  1039. setLocalValues = allowValueOptimization ? function(mom, a) {
  1040. // simlate what moment's accessors do
  1041. mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
  1042. a[0] || 0,
  1043. a[1] || 0,
  1044. a[2] || 0,
  1045. a[3] || 0,
  1046. a[4] || 0,
  1047. a[5] || 0,
  1048. a[6] || 0
  1049. ));
  1050. moment.updateOffset(mom, false); // keepTime=false
  1051. } : setMomentValues;
  1052. ;;
  1053. // Single Date Formatting
  1054. // -------------------------------------------------------------------------------------------------
  1055. // call this if you want Moment's original format method to be used
  1056. function oldMomentFormat(mom, formatStr) {
  1057. return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  1058. }
  1059. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1060. // additional token.
  1061. function formatDate(date, formatStr) {
  1062. return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1063. }
  1064. function formatDateWithChunks(date, chunks) {
  1065. var s = '';
  1066. var i;
  1067. for (i=0; i<chunks.length; i++) {
  1068. s += formatDateWithChunk(date, chunks[i]);
  1069. }
  1070. return s;
  1071. }
  1072. // addition formatting tokens we want recognized
  1073. var tokenOverrides = {
  1074. t: function(date) { // "a" or "p"
  1075. return oldMomentFormat(date, 'a').charAt(0);
  1076. },
  1077. T: function(date) { // "A" or "P"
  1078. return oldMomentFormat(date, 'A').charAt(0);
  1079. }
  1080. };
  1081. function formatDateWithChunk(date, chunk) {
  1082. var token;
  1083. var maybeStr;
  1084. if (typeof chunk === 'string') { // a literal string
  1085. return chunk;
  1086. }
  1087. else if ((token = chunk.token)) { // a token, like "YYYY"
  1088. if (tokenOverrides[token]) {
  1089. return tokenOverrides[token](date); // use our custom token
  1090. }
  1091. return oldMomentFormat(date, token);
  1092. }
  1093. else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  1094. maybeStr = formatDateWithChunks(date, chunk.maybe);
  1095. if (maybeStr.match(/[1-9]/)) {
  1096. return maybeStr;
  1097. }
  1098. }
  1099. return '';
  1100. }
  1101. // Date Range Formatting
  1102. // -------------------------------------------------------------------------------------------------
  1103. // TODO: make it work with timezone offset
  1104. // Using a formatting string meant for a single date, generate a range string, like
  1105. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  1106. // If the dates are the same as far as the format string is concerned, just return a single
  1107. // rendering of one date, without any separator.
  1108. function formatRange(date1, date2, formatStr, separator, isRTL) {
  1109. var localeData;
  1110. date1 = fc.moment.parseZone(date1);
  1111. date2 = fc.moment.parseZone(date2);
  1112. localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
  1113. // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  1114. formatStr = localeData.longDateFormat(formatStr) || formatStr;
  1115. // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  1116. // or non-zero areas in Moment's localized format strings.
  1117. separator = separator || ' - ';
  1118. return formatRangeWithChunks(
  1119. date1,
  1120. date2,
  1121. getFormatStringChunks(formatStr),
  1122. separator,
  1123. isRTL
  1124. );
  1125. }
  1126. fc.formatRange = formatRange; // expose
  1127. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  1128. var chunkStr; // the rendering of the chunk
  1129. var leftI;
  1130. var leftStr = '';
  1131. var rightI;
  1132. var rightStr = '';
  1133. var middleI;
  1134. var middleStr1 = '';
  1135. var middleStr2 = '';
  1136. var middleStr = '';
  1137. // Start at the leftmost side of the formatting string and continue until you hit a token
  1138. // that is not the same between dates.
  1139. for (leftI=0; leftI<chunks.length; leftI++) {
  1140. chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
  1141. if (chunkStr === false) {
  1142. break;
  1143. }
  1144. leftStr += chunkStr;
  1145. }
  1146. // Similarly, start at the rightmost side of the formatting string and move left
  1147. for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  1148. chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
  1149. if (chunkStr === false) {
  1150. break;
  1151. }
  1152. rightStr = chunkStr + rightStr;
  1153. }
  1154. // The area in the middle is different for both of the dates.
  1155. // Collect them distinctly so we can jam them together later.
  1156. for (middleI=leftI; middleI<=rightI; middleI++) {
  1157. middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  1158. middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  1159. }
  1160. if (middleStr1 || middleStr2) {
  1161. if (isRTL) {
  1162. middleStr = middleStr2 + separator + middleStr1;
  1163. }
  1164. else {
  1165. middleStr = middleStr1 + separator + middleStr2;
  1166. }
  1167. }
  1168. return leftStr + middleStr + rightStr;
  1169. }
  1170. var similarUnitMap = {
  1171. Y: 'year',
  1172. M: 'month',
  1173. D: 'day', // day of month
  1174. d: 'day', // day of week
  1175. // prevents a separator between anything time-related...
  1176. A: 'second', // AM/PM
  1177. a: 'second', // am/pm
  1178. T: 'second', // A/P
  1179. t: 'second', // a/p
  1180. H: 'second', // hour (24)
  1181. h: 'second', // hour (12)
  1182. m: 'second', // minute
  1183. s: 'second' // second
  1184. };
  1185. // TODO: week maybe?
  1186. // Given a formatting chunk, and given that both dates are similar in the regard the
  1187. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  1188. function formatSimilarChunk(date1, date2, chunk) {
  1189. var token;
  1190. var unit;
  1191. if (typeof chunk === 'string') { // a literal string
  1192. return chunk;
  1193. }
  1194. else if ((token = chunk.token)) {
  1195. unit = similarUnitMap[token.charAt(0)];
  1196. // are the dates the same for this unit of measurement?
  1197. if (unit && date1.isSame(date2, unit)) {
  1198. return oldMomentFormat(date1, token); // would be the same if we used `date2`
  1199. // BTW, don't support custom tokens
  1200. }
  1201. }
  1202. return false; // the chunk is NOT the same for the two dates
  1203. // BTW, don't support splitting on non-zero areas
  1204. }
  1205. // Chunking Utils
  1206. // -------------------------------------------------------------------------------------------------
  1207. var formatStringChunkCache = {};
  1208. function getFormatStringChunks(formatStr) {
  1209. if (formatStr in formatStringChunkCache) {
  1210. return formatStringChunkCache[formatStr];
  1211. }
  1212. return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  1213. }
  1214. // Break the formatting string into an array of chunks
  1215. function chunkFormatString(formatStr) {
  1216. var chunks = [];
  1217. var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  1218. var match;
  1219. while ((match = chunker.exec(formatStr))) {
  1220. if (match[1]) { // a literal string inside [ ... ]
  1221. chunks.push(match[1]);
  1222. }
  1223. else if (match[2]) { // non-zero formatting inside ( ... )
  1224. chunks.push({ maybe: chunkFormatString(match[2]) });
  1225. }
  1226. else if (match[3]) { // a formatting token
  1227. chunks.push({ token: match[3] });
  1228. }
  1229. else if (match[5]) { // an unenclosed literal string
  1230. chunks.push(match[5]);
  1231. }
  1232. }
  1233. return chunks;
  1234. }
  1235. ;;
  1236. fc.Class = Class; // export
  1237. // class that all other classes will inherit from
  1238. function Class() { }
  1239. // called upon a class to create a subclass
  1240. Class.extend = function(members) {
  1241. var superClass = this;
  1242. var subClass;
  1243. members = members || {};
  1244. // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
  1245. if (hasOwnProp(members, 'constructor')) {
  1246. subClass = members.constructor;
  1247. }
  1248. if (typeof subClass !== 'function') {
  1249. subClass = members.constructor = function() {
  1250. superClass.apply(this, arguments);
  1251. };
  1252. }
  1253. // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
  1254. subClass.prototype = createObject(superClass.prototype);
  1255. // copy each member variable/method onto the the subclass's prototype
  1256. copyOwnProps(members, subClass.prototype);
  1257. copyNativeMethods(members, subClass.prototype); // hack for IE8
  1258. // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
  1259. copyOwnProps(superClass, subClass);
  1260. return subClass;
  1261. };
  1262. // adds new member variables/methods to the class's prototype.
  1263. // can be called with another class, or a plain object hash containing new members.
  1264. Class.mixin = function(members) {
  1265. copyOwnProps(members.prototype || members, this.prototype); // TODO: copyNativeMethods?
  1266. };
  1267. ;;
  1268. /* A rectangular panel that is absolutely positioned over other content
  1269. ------------------------------------------------------------------------------------------------------------------------
  1270. Options:
  1271. - className (string)
  1272. - content (HTML string or jQuery element set)
  1273. - parentEl
  1274. - top
  1275. - left
  1276. - right (the x coord of where the right edge should be. not a "CSS" right)
  1277. - autoHide (boolean)
  1278. - show (callback)
  1279. - hide (callback)
  1280. */
  1281. var Popover = Class.extend({
  1282. isHidden: true,
  1283. options: null,
  1284. el: null, // the container element for the popover. generated by this object
  1285. documentMousedownProxy: null, // document mousedown handler bound to `this`
  1286. margin: 10, // the space required between the popover and the edges of the scroll container
  1287. constructor: function(options) {
  1288. this.options = options || {};
  1289. },
  1290. // Shows the popover on the specified position. Renders it if not already
  1291. show: function() {
  1292. if (this.isHidden) {
  1293. if (!this.el) {
  1294. this.render();
  1295. }
  1296. this.el.show();
  1297. this.position();
  1298. this.isHidden = false;
  1299. this.trigger('show');
  1300. }
  1301. },
  1302. // Hides the popover, through CSS, but does not remove it from the DOM
  1303. hide: function() {
  1304. if (!this.isHidden) {
  1305. this.el.hide();
  1306. this.isHidden = true;
  1307. this.trigger('hide');
  1308. }
  1309. },
  1310. // Creates `this.el` and renders content inside of it
  1311. render: function() {
  1312. var _this = this;
  1313. var options = this.options;
  1314. this.el = $('<div class="fc-popover"/>')
  1315. .addClass(options.className || '')
  1316. .css({
  1317. // position initially to the top left to avoid creating scrollbars
  1318. top: 0,
  1319. left: 0
  1320. })
  1321. .append(options.content)
  1322. .appendTo(options.parentEl);
  1323. // when a click happens on anything inside with a 'fc-close' className, hide the popover
  1324. this.el.on('click', '.fc-close', function() {
  1325. _this.hide();
  1326. });
  1327. if (options.autoHide) {
  1328. $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
  1329. }
  1330. },
  1331. // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  1332. documentMousedown: function(ev) {
  1333. // only hide the popover if the click happened outside the popover
  1334. if (this.el && !$(ev.target).closest(this.el).length) {
  1335. this.hide();
  1336. }
  1337. },
  1338. // Hides and unregisters any handlers
  1339. removeElement: function() {
  1340. this.hide();
  1341. if (this.el) {
  1342. this.el.remove();
  1343. this.el = null;
  1344. }
  1345. $(document).off('mousedown', this.documentMousedownProxy);
  1346. },
  1347. // Positions the popover optimally, using the top/left/right options
  1348. position: function() {
  1349. var options = this.options;
  1350. var origin = this.el.offsetParent().offset();
  1351. var width = this.el.outerWidth();
  1352. var height = this.el.outerHeight();
  1353. var windowEl = $(window);
  1354. var viewportEl = getScrollParent(this.el);
  1355. var viewportTop;
  1356. var viewportLeft;
  1357. var viewportOffset;
  1358. var top; // the "position" (not "offset") values for the popover
  1359. var left; //
  1360. // compute top and left
  1361. top = options.top || 0;
  1362. if (options.left !== undefined) {
  1363. left = options.left;
  1364. }
  1365. else if (options.right !== undefined) {
  1366. left = options.right - width; // derive the left value from the right value
  1367. }
  1368. else {
  1369. left = 0;
  1370. }
  1371. if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  1372. viewportEl = windowEl;
  1373. viewportTop = 0; // the window is always at the top left
  1374. viewportLeft = 0; // (and .offset() won't work if called here)
  1375. }
  1376. else {
  1377. viewportOffset = viewportEl.offset();
  1378. viewportTop = viewportOffset.top;
  1379. viewportLeft = viewportOffset.left;
  1380. }
  1381. // if the window is scrolled, it causes the visible area to be further down
  1382. viewportTop += windowEl.scrollTop();
  1383. viewportLeft += windowEl.scrollLeft();
  1384. // constrain to the view port. if constrained by two edges, give precedence to top/left
  1385. if (options.viewportConstrain !== false) {
  1386. top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  1387. top = Math.max(top, viewportTop + this.margin);
  1388. left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  1389. left = Math.max(left, viewportLeft + this.margin);
  1390. }
  1391. this.el.css({
  1392. top: top - origin.top,
  1393. left: left - origin.left
  1394. });
  1395. },
  1396. // Triggers a callback. Calls a function in the option hash of the same name.
  1397. // Arguments beyond the first `name` are forwarded on.
  1398. // TODO: better code reuse for this. Repeat code
  1399. trigger: function(name) {
  1400. if (this.options[name]) {
  1401. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1402. }
  1403. }
  1404. });
  1405. ;;
  1406. /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
  1407. ------------------------------------------------------------------------------------------------------------------------
  1408. Common interface:
  1409. CoordMap.prototype = {
  1410. build: function() {},
  1411. getCell: function(x, y) {}
  1412. };
  1413. */
  1414. /* Coordinate map for a grid component
  1415. ----------------------------------------------------------------------------------------------------------------------*/
  1416. var GridCoordMap = Class.extend({
  1417. grid: null, // reference to the Grid
  1418. rowCoords: null, // array of {top,bottom} objects
  1419. colCoords: null, // array of {left,right} objects
  1420. containerEl: null, // container element that all coordinates are constrained to. optionally assigned
  1421. bounds: null,
  1422. constructor: function(grid) {
  1423. this.grid = grid;
  1424. },
  1425. // Queries the grid for the coordinates of all the cells
  1426. build: function() {
  1427. this.grid.build();
  1428. this.rowCoords = this.grid.computeRowCoords();
  1429. this.colCoords = this.grid.computeColCoords();
  1430. this.computeBounds();
  1431. },
  1432. // Clears the coordinates data to free up memory
  1433. clear: function() {
  1434. this.grid.clear();
  1435. this.rowCoords = null;
  1436. this.colCoords = null;
  1437. },
  1438. // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
  1439. getCell: function(x, y) {
  1440. var rowCoords = this.rowCoords;
  1441. var rowCnt = rowCoords.length;
  1442. var colCoords = this.colCoords;
  1443. var colCnt = colCoords.length;
  1444. var hitRow = null;
  1445. var hitCol = null;
  1446. var i, coords;
  1447. var cell;
  1448. if (this.inBounds(x, y)) {
  1449. for (i = 0; i < rowCnt; i++) {
  1450. coords = rowCoords[i];
  1451. if (y >= coords.top && y < coords.bottom) {
  1452. hitRow = i;
  1453. break;
  1454. }
  1455. }
  1456. for (i = 0; i < colCnt; i++) {
  1457. coords = colCoords[i];
  1458. if (x >= coords.left && x < coords.right) {
  1459. hitCol = i;
  1460. break;
  1461. }
  1462. }
  1463. if (hitRow !== null && hitCol !== null) {
  1464. cell = this.grid.getCell(hitRow, hitCol); // expected to return a fresh object we can modify
  1465. cell.grid = this.grid; // for CellDragListener's isCellsEqual. dragging between grids
  1466. // make the coordinates available on the cell object
  1467. $.extend(cell, rowCoords[hitRow], colCoords[hitCol]);
  1468. return cell;
  1469. }
  1470. }
  1471. return null;
  1472. },
  1473. // If there is a containerEl, compute the bounds into min/max values
  1474. computeBounds: function() {
  1475. this.bounds = this.containerEl ?
  1476. getClientRect(this.containerEl) : // area within scrollbars
  1477. null;
  1478. },
  1479. // Determines if the given coordinates are in bounds. If no `containerEl`, always true
  1480. inBounds: function(x, y) {
  1481. var bounds = this.bounds;
  1482. if (bounds) {
  1483. return x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom;
  1484. }
  1485. return true;
  1486. }
  1487. });
  1488. /* Coordinate map that is a combination of multiple other coordinate maps
  1489. ----------------------------------------------------------------------------------------------------------------------*/
  1490. var ComboCoordMap = Class.extend({
  1491. coordMaps: null, // an array of CoordMaps
  1492. constructor: function(coordMaps) {
  1493. this.coordMaps = coordMaps;
  1494. },
  1495. // Builds all coordMaps
  1496. build: function() {
  1497. var coordMaps = this.coordMaps;
  1498. var i;
  1499. for (i = 0; i < coordMaps.length; i++) {
  1500. coordMaps[i].build();
  1501. }
  1502. },
  1503. // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
  1504. getCell: function(x, y) {
  1505. var coordMaps = this.coordMaps;
  1506. var cell = null;
  1507. var i;
  1508. for (i = 0; i < coordMaps.length && !cell; i++) {
  1509. cell = coordMaps[i].getCell(x, y);
  1510. }
  1511. return cell;
  1512. },
  1513. // Clears all coordMaps
  1514. clear: function() {
  1515. var coordMaps = this.coordMaps;
  1516. var i;
  1517. for (i = 0; i < coordMaps.length; i++) {
  1518. coordMaps[i].clear();
  1519. }
  1520. }
  1521. });
  1522. ;;
  1523. /* Tracks a drag's mouse movement, firing various handlers
  1524. ----------------------------------------------------------------------------------------------------------------------*/
  1525. var DragListener = fc.DragListener = Class.extend({
  1526. options: null,
  1527. isListening: false,
  1528. isDragging: false,
  1529. // coordinates of the initial mousedown
  1530. originX: null,
  1531. originY: null,
  1532. // handler attached to the document, bound to the DragListener's `this`
  1533. mousemoveProxy: null,
  1534. mouseupProxy: null,
  1535. // for IE8 bug-fighting behavior, for now
  1536. subjectEl: null, // the element being draged. optional
  1537. subjectHref: null,
  1538. scrollEl: null,
  1539. scrollBounds: null, // { top, bottom, left, right }
  1540. scrollTopVel: null, // pixels per second
  1541. scrollLeftVel: null, // pixels per second
  1542. scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  1543. scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
  1544. scrollSensitivity: 30, // pixels from edge for scrolling to start
  1545. scrollSpeed: 200, // pixels per second, at maximum speed
  1546. scrollIntervalMs: 50, // millisecond wait between scroll increment
  1547. constructor: function(options) {
  1548. options = options || {};
  1549. this.options = options;
  1550. this.subjectEl = options.subjectEl;
  1551. },
  1552. // Call this when the user does a mousedown. Will probably lead to startListening
  1553. mousedown: function(ev) {
  1554. if (isPrimaryMouseButton(ev)) {
  1555. ev.preventDefault(); // prevents native selection in most browsers
  1556. this.startListening(ev);
  1557. // start the drag immediately if there is no minimum distance for a drag start
  1558. if (!this.options.distance) {
  1559. this.startDrag(ev);
  1560. }
  1561. }
  1562. },
  1563. // Call this to start tracking mouse movements
  1564. startListening: function(ev) {
  1565. var scrollParent;
  1566. if (!this.isListening) {
  1567. // grab scroll container and attach handler
  1568. if (ev && this.options.scroll) {
  1569. scrollParent = getScrollParent($(ev.target));
  1570. if (!scrollParent.is(window) && !scrollParent.is(document)) {
  1571. this.scrollEl = scrollParent;
  1572. // scope to `this`, and use `debounce` to make sure rapid calls don't happen
  1573. this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
  1574. this.scrollEl.on('scroll', this.scrollHandlerProxy);
  1575. }
  1576. }
  1577. $(document)
  1578. .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
  1579. .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
  1580. .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
  1581. if (ev) {
  1582. this.originX = ev.pageX;
  1583. this.originY = ev.pageY;
  1584. }
  1585. else {
  1586. // if no starting information was given, origin will be the topleft corner of the screen.
  1587. // if so, dx/dy in the future will be the absolute coordinates.
  1588. this.originX = 0;
  1589. this.originY = 0;
  1590. }
  1591. this.isListening = true;
  1592. this.listenStart(ev);
  1593. }
  1594. },
  1595. // Called when drag listening has started (but a real drag has not necessarily began)
  1596. listenStart: function(ev) {
  1597. this.trigger('listenStart', ev);
  1598. },
  1599. // Called when the user moves the mouse
  1600. mousemove: function(ev) {
  1601. var dx = ev.pageX - this.originX;
  1602. var dy = ev.pageY - this.originY;
  1603. var minDistance;
  1604. var distanceSq; // current distance from the origin, squared
  1605. if (!this.isDragging) { // if not already dragging...
  1606. // then start the drag if the minimum distance criteria is met
  1607. minDistance = this.options.distance || 1;
  1608. distanceSq = dx * dx + dy * dy;
  1609. if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  1610. this.startDrag(ev);
  1611. }
  1612. }
  1613. if (this.isDragging) {
  1614. this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
  1615. }
  1616. },
  1617. // Call this to initiate a legitimate drag.
  1618. // This function is called internally from this class, but can also be called explicitly from outside
  1619. startDrag: function(ev) {
  1620. if (!this.isListening) { // startDrag must have manually initiated
  1621. this.startListening();
  1622. }
  1623. if (!this.isDragging) {
  1624. this.isDragging = true;
  1625. this.dragStart(ev);
  1626. }
  1627. },
  1628. // Called when the actual drag has started (went beyond minDistance)
  1629. dragStart: function(ev) {
  1630. var subjectEl = this.subjectEl;
  1631. this.trigger('dragStart', ev);
  1632. // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
  1633. if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
  1634. subjectEl.removeAttr('href');
  1635. }
  1636. },
  1637. // Called while the mouse is being moved and when we know a legitimate drag is taking place
  1638. drag: function(dx, dy, ev) {
  1639. this.trigger('drag', dx, dy, ev);
  1640. this.updateScroll(ev); // will possibly cause scrolling
  1641. },
  1642. // Called when the user does a mouseup
  1643. mouseup: function(ev) {
  1644. this.stopListening(ev);
  1645. },
  1646. // Called when the drag is over. Will not cause listening to stop however.
  1647. // A concluding 'cellOut' event will NOT be triggered.
  1648. stopDrag: function(ev) {
  1649. if (this.isDragging) {
  1650. this.stopScrolling();
  1651. this.dragStop(ev);
  1652. this.isDragging = false;
  1653. }
  1654. },
  1655. // Called when dragging has been stopped
  1656. dragStop: function(ev) {
  1657. var _this = this;
  1658. this.trigger('dragStop', ev);
  1659. // restore a mousedown'd <a>'s href (for IE8 bug)
  1660. setTimeout(function() { // must be outside of the click's execution
  1661. if (_this.subjectHref) {
  1662. _this.subjectEl.attr('href', _this.subjectHref);
  1663. }
  1664. }, 0);
  1665. },
  1666. // Call this to stop listening to the user's mouse events
  1667. stopListening: function(ev) {
  1668. this.stopDrag(ev); // if there's a current drag, kill it
  1669. if (this.isListening) {
  1670. // remove the scroll handler if there is a scrollEl
  1671. if (this.scrollEl) {
  1672. this.scrollEl.off('scroll', this.scrollHandlerProxy);
  1673. this.scrollHandlerProxy = null;
  1674. }
  1675. $(document)
  1676. .off('mousemove', this.mousemoveProxy)
  1677. .off('mouseup', this.mouseupProxy)
  1678. .off('selectstart', this.preventDefault);
  1679. this.mousemoveProxy = null;
  1680. this.mouseupProxy = null;
  1681. this.isListening = false;
  1682. this.listenStop(ev);
  1683. }
  1684. },
  1685. // Called when drag listening has stopped
  1686. listenStop: function(ev) {
  1687. this.trigger('listenStop', ev);
  1688. },
  1689. // Triggers a callback. Calls a function in the option hash of the same name.
  1690. // Arguments beyond the first `name` are forwarded on.
  1691. trigger: function(name) {
  1692. if (this.options[name]) {
  1693. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1694. }
  1695. },
  1696. // Stops a given mouse event from doing it's native browser action. In our case, text selection.
  1697. preventDefault: function(ev) {
  1698. ev.preventDefault();
  1699. },
  1700. /* Scrolling
  1701. ------------------------------------------------------------------------------------------------------------------*/
  1702. // Computes and stores the bounding rectangle of scrollEl
  1703. computeScrollBounds: function() {
  1704. var el = this.scrollEl;
  1705. this.scrollBounds = el ? getOuterRect(el) : null;
  1706. // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  1707. },
  1708. // Called when the dragging is in progress and scrolling should be updated
  1709. updateScroll: function(ev) {
  1710. var sensitivity = this.scrollSensitivity;
  1711. var bounds = this.scrollBounds;
  1712. var topCloseness, bottomCloseness;
  1713. var leftCloseness, rightCloseness;
  1714. var topVel = 0;
  1715. var leftVel = 0;
  1716. if (bounds) { // only scroll if scrollEl exists
  1717. // compute closeness to edges. valid range is from 0.0 - 1.0
  1718. topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
  1719. bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
  1720. leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
  1721. rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
  1722. // translate vertical closeness into velocity.
  1723. // mouse must be completely in bounds for velocity to happen.
  1724. if (topCloseness >= 0 && topCloseness <= 1) {
  1725. topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  1726. }
  1727. else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  1728. topVel = bottomCloseness * this.scrollSpeed;
  1729. }
  1730. // translate horizontal closeness into velocity
  1731. if (leftCloseness >= 0 && leftCloseness <= 1) {
  1732. leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  1733. }
  1734. else if (rightCloseness >= 0 && rightCloseness <= 1) {
  1735. leftVel = rightCloseness * this.scrollSpeed;
  1736. }
  1737. }
  1738. this.setScrollVel(topVel, leftVel);
  1739. },
  1740. // Sets the speed-of-scrolling for the scrollEl
  1741. setScrollVel: function(topVel, leftVel) {
  1742. this.scrollTopVel = topVel;
  1743. this.scrollLeftVel = leftVel;
  1744. this.constrainScrollVel(); // massages into realistic values
  1745. // if there is non-zero velocity, and an animation loop hasn't already started, then START
  1746. if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  1747. this.scrollIntervalId = setInterval(
  1748. proxy(this, 'scrollIntervalFunc'), // scope to `this`
  1749. this.scrollIntervalMs
  1750. );
  1751. }
  1752. },
  1753. // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  1754. constrainScrollVel: function() {
  1755. var el = this.scrollEl;
  1756. if (this.scrollTopVel < 0) { // scrolling up?
  1757. if (el.scrollTop() <= 0) { // already scrolled all the way up?
  1758. this.scrollTopVel = 0;
  1759. }
  1760. }
  1761. else if (this.scrollTopVel > 0) { // scrolling down?
  1762. if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  1763. this.scrollTopVel = 0;
  1764. }
  1765. }
  1766. if (this.scrollLeftVel < 0) { // scrolling left?
  1767. if (el.scrollLeft() <= 0) { // already scrolled all the left?
  1768. this.scrollLeftVel = 0;
  1769. }
  1770. }
  1771. else if (this.scrollLeftVel > 0) { // scrolling right?
  1772. if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  1773. this.scrollLeftVel = 0;
  1774. }
  1775. }
  1776. },
  1777. // This function gets called during every iteration of the scrolling animation loop
  1778. scrollIntervalFunc: function() {
  1779. var el = this.scrollEl;
  1780. var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  1781. // change the value of scrollEl's scroll
  1782. if (this.scrollTopVel) {
  1783. el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  1784. }
  1785. if (this.scrollLeftVel) {
  1786. el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  1787. }
  1788. this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  1789. // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  1790. if (!this.scrollTopVel && !this.scrollLeftVel) {
  1791. this.stopScrolling();
  1792. }
  1793. },
  1794. // Kills any existing scrolling animation loop
  1795. stopScrolling: function() {
  1796. if (this.scrollIntervalId) {
  1797. clearInterval(this.scrollIntervalId);
  1798. this.scrollIntervalId = null;
  1799. // when all done with scrolling, recompute positions since they probably changed
  1800. this.scrollStop();
  1801. }
  1802. },
  1803. // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  1804. scrollHandler: function() {
  1805. // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  1806. if (!this.scrollIntervalId) {
  1807. this.scrollStop();
  1808. }
  1809. },
  1810. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  1811. scrollStop: function() {
  1812. }
  1813. });
  1814. ;;
  1815. /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
  1816. ------------------------------------------------------------------------------------------------------------------------
  1817. options:
  1818. - subjectEl
  1819. - subjectCenter
  1820. */
  1821. var CellDragListener = DragListener.extend({
  1822. coordMap: null, // converts coordinates to date cells
  1823. origCell: null, // the cell the mouse was over when listening started
  1824. cell: null, // the cell the mouse is over
  1825. coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
  1826. constructor: function(coordMap, options) {
  1827. DragListener.prototype.constructor.call(this, options); // call the super-constructor
  1828. this.coordMap = coordMap;
  1829. },
  1830. // Called when drag listening starts (but a real drag has not necessarily began).
  1831. // ev might be undefined if dragging was started manually.
  1832. listenStart: function(ev) {
  1833. var subjectEl = this.subjectEl;
  1834. var subjectRect;
  1835. var origPoint;
  1836. var point;
  1837. DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
  1838. this.computeCoords();
  1839. if (ev) {
  1840. origPoint = { left: ev.pageX, top: ev.pageY };
  1841. point = origPoint;
  1842. // constrain the point to bounds of the element being dragged
  1843. if (subjectEl) {
  1844. subjectRect = getOuterRect(subjectEl); // used for centering as well
  1845. point = constrainPoint(point, subjectRect);
  1846. }
  1847. this.origCell = this.getCell(point.left, point.top);
  1848. // treat the center of the subject as the collision point?
  1849. if (subjectEl && this.options.subjectCenter) {
  1850. // only consider the area the subject overlaps the cell. best for large subjects
  1851. if (this.origCell) {
  1852. subjectRect = intersectRects(this.origCell, subjectRect) ||
  1853. subjectRect; // in case there is no intersection
  1854. }
  1855. point = getRectCenter(subjectRect);
  1856. }
  1857. this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
  1858. }
  1859. else {
  1860. this.origCell = null;
  1861. this.coordAdjust = null;
  1862. }
  1863. },
  1864. // Recomputes the drag-critical positions of elements
  1865. computeCoords: function() {
  1866. this.coordMap.build();
  1867. this.computeScrollBounds();
  1868. },
  1869. // Called when the actual drag has started
  1870. dragStart: function(ev) {
  1871. var cell;
  1872. DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
  1873. cell = this.getCell(ev.pageX, ev.pageY); // might be different from this.origCell if the min-distance is large
  1874. // report the initial cell the mouse is over
  1875. // especially important if no min-distance and drag starts immediately
  1876. if (cell) {
  1877. this.cellOver(cell);
  1878. }
  1879. },
  1880. // Called when the drag moves
  1881. drag: function(dx, dy, ev) {
  1882. var cell;
  1883. DragListener.prototype.drag.apply(this, arguments); // call the super-method
  1884. cell = this.getCell(ev.pageX, ev.pageY);
  1885. if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
  1886. if (this.cell) {
  1887. this.cellOut();
  1888. }
  1889. if (cell) {
  1890. this.cellOver(cell);
  1891. }
  1892. }
  1893. },
  1894. // Called when dragging has been stopped
  1895. dragStop: function() {
  1896. this.cellDone();
  1897. DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
  1898. },
  1899. // Called when a the mouse has just moved over a new cell
  1900. cellOver: function(cell) {
  1901. this.cell = cell;
  1902. this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell), this.origCell);
  1903. },
  1904. // Called when the mouse has just moved out of a cell
  1905. cellOut: function() {
  1906. if (this.cell) {
  1907. this.trigger('cellOut', this.cell);
  1908. this.cellDone();
  1909. this.cell = null;
  1910. }
  1911. },
  1912. // Called after a cellOut. Also called before a dragStop
  1913. cellDone: function() {
  1914. if (this.cell) {
  1915. this.trigger('cellDone', this.cell);
  1916. }
  1917. },
  1918. // Called when drag listening has stopped
  1919. listenStop: function() {
  1920. DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
  1921. this.origCell = this.cell = null;
  1922. this.coordMap.clear();
  1923. },
  1924. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  1925. scrollStop: function() {
  1926. DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method
  1927. this.computeCoords(); // cells' absolute positions will be in new places. recompute
  1928. },
  1929. // Gets the cell underneath the coordinates for the given mouse event
  1930. getCell: function(left, top) {
  1931. if (this.coordAdjust) {
  1932. left += this.coordAdjust.left;
  1933. top += this.coordAdjust.top;
  1934. }
  1935. return this.coordMap.getCell(left, top);
  1936. }
  1937. });
  1938. // Returns `true` if the cells are identically equal. `false` otherwise.
  1939. // They must have the same row, col, and be from the same grid.
  1940. // Two null values will be considered equal, as two "out of the grid" states are the same.
  1941. function isCellsEqual(cell1, cell2) {
  1942. if (!cell1 && !cell2) {
  1943. return true;
  1944. }
  1945. if (cell1 && cell2) {
  1946. return cell1.grid === cell2.grid &&
  1947. cell1.row === cell2.row &&
  1948. cell1.col === cell2.col;
  1949. }
  1950. return false;
  1951. }
  1952. ;;
  1953. /* Creates a clone of an element and lets it track the mouse as it moves
  1954. ----------------------------------------------------------------------------------------------------------------------*/
  1955. var MouseFollower = Class.extend({
  1956. options: null,
  1957. sourceEl: null, // the element that will be cloned and made to look like it is dragging
  1958. el: null, // the clone of `sourceEl` that will track the mouse
  1959. parentEl: null, // the element that `el` (the clone) will be attached to
  1960. // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  1961. top0: null,
  1962. left0: null,
  1963. // the initial position of the mouse
  1964. mouseY0: null,
  1965. mouseX0: null,
  1966. // the number of pixels the mouse has moved from its initial position
  1967. topDelta: null,
  1968. leftDelta: null,
  1969. mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
  1970. isFollowing: false,
  1971. isHidden: false,
  1972. isAnimating: false, // doing the revert animation?
  1973. constructor: function(sourceEl, options) {
  1974. this.options = options = options || {};
  1975. this.sourceEl = sourceEl;
  1976. this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  1977. },
  1978. // Causes the element to start following the mouse
  1979. start: function(ev) {
  1980. if (!this.isFollowing) {
  1981. this.isFollowing = true;
  1982. this.mouseY0 = ev.pageY;
  1983. this.mouseX0 = ev.pageX;
  1984. this.topDelta = 0;
  1985. this.leftDelta = 0;
  1986. if (!this.isHidden) {
  1987. this.updatePosition();
  1988. }
  1989. $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
  1990. }
  1991. },
  1992. // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  1993. // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  1994. stop: function(shouldRevert, callback) {
  1995. var _this = this;
  1996. var revertDuration = this.options.revertDuration;
  1997. function complete() {
  1998. this.isAnimating = false;
  1999. _this.removeElement();
  2000. this.top0 = this.left0 = null; // reset state for future updatePosition calls
  2001. if (callback) {
  2002. callback();
  2003. }
  2004. }
  2005. if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  2006. this.isFollowing = false;
  2007. $(document).off('mousemove', this.mousemoveProxy);
  2008. if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  2009. this.isAnimating = true;
  2010. this.el.animate({
  2011. top: this.top0,
  2012. left: this.left0
  2013. }, {
  2014. duration: revertDuration,
  2015. complete: complete
  2016. });
  2017. }
  2018. else {
  2019. complete();
  2020. }
  2021. }
  2022. },
  2023. // Gets the tracking element. Create it if necessary
  2024. getEl: function() {
  2025. var el = this.el;
  2026. if (!el) {
  2027. this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2028. el = this.el = this.sourceEl.clone()
  2029. .css({
  2030. position: 'absolute',
  2031. visibility: '', // in case original element was hidden (commonly through hideEvents())
  2032. display: this.isHidden ? 'none' : '', // for when initially hidden
  2033. margin: 0,
  2034. right: 'auto', // erase and set width instead
  2035. bottom: 'auto', // erase and set height instead
  2036. width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  2037. height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  2038. opacity: this.options.opacity || '',
  2039. zIndex: this.options.zIndex
  2040. })
  2041. .appendTo(this.parentEl);
  2042. }
  2043. return el;
  2044. },
  2045. // Removes the tracking element if it has already been created
  2046. removeElement: function() {
  2047. if (this.el) {
  2048. this.el.remove();
  2049. this.el = null;
  2050. }
  2051. },
  2052. // Update the CSS position of the tracking element
  2053. updatePosition: function() {
  2054. var sourceOffset;
  2055. var origin;
  2056. this.getEl(); // ensure this.el
  2057. // make sure origin info was computed
  2058. if (this.top0 === null) {
  2059. this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2060. sourceOffset = this.sourceEl.offset();
  2061. origin = this.el.offsetParent().offset();
  2062. this.top0 = sourceOffset.top - origin.top;
  2063. this.left0 = sourceOffset.left - origin.left;
  2064. }
  2065. this.el.css({
  2066. top: this.top0 + this.topDelta,
  2067. left: this.left0 + this.leftDelta
  2068. });
  2069. },
  2070. // Gets called when the user moves the mouse
  2071. mousemove: function(ev) {
  2072. this.topDelta = ev.pageY - this.mouseY0;
  2073. this.leftDelta = ev.pageX - this.mouseX0;
  2074. if (!this.isHidden) {
  2075. this.updatePosition();
  2076. }
  2077. },
  2078. // Temporarily makes the tracking element invisible. Can be called before following starts
  2079. hide: function() {
  2080. if (!this.isHidden) {
  2081. this.isHidden = true;
  2082. if (this.el) {
  2083. this.el.hide();
  2084. }
  2085. }
  2086. },
  2087. // Show the tracking element after it has been temporarily hidden
  2088. show: function() {
  2089. if (this.isHidden) {
  2090. this.isHidden = false;
  2091. this.updatePosition();
  2092. this.getEl().show();
  2093. }
  2094. }
  2095. });
  2096. ;;
  2097. /* A utility class for rendering <tr> rows.
  2098. ----------------------------------------------------------------------------------------------------------------------*/
  2099. // It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
  2100. // (such as highlight rows, day rows, helper rows, etc).
  2101. var RowRenderer = Class.extend({
  2102. view: null, // a View object
  2103. isRTL: null, // shortcut to the view's isRTL option
  2104. cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
  2105. constructor: function(view) {
  2106. this.view = view;
  2107. this.isRTL = view.opt('isRTL');
  2108. },
  2109. // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
  2110. // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
  2111. // `row` is an optional row number.
  2112. rowHtml: function(rowType, row) {
  2113. var renderCell = this.getHtmlRenderer('cell', rowType);
  2114. var rowCellHtml = '';
  2115. var col;
  2116. var cell;
  2117. row = row || 0;
  2118. for (col = 0; col < this.colCnt; col++) {
  2119. cell = this.getCell(row, col);
  2120. rowCellHtml += renderCell(cell);
  2121. }
  2122. rowCellHtml = this.bookendCells(rowCellHtml, rowType, row); // apply intro and outro
  2123. return '<tr>' + rowCellHtml + '</tr>';
  2124. },
  2125. // Applies the "intro" and "outro" HTML to the given cells.
  2126. // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  2127. // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
  2128. // `row` is an optional row number.
  2129. bookendCells: function(cells, rowType, row) {
  2130. var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
  2131. var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
  2132. var prependHtml = this.isRTL ? outro : intro;
  2133. var appendHtml = this.isRTL ? intro : outro;
  2134. if (typeof cells === 'string') {
  2135. return prependHtml + cells + appendHtml;
  2136. }
  2137. else { // a jQuery <tr> element
  2138. return cells.prepend(prependHtml).append(appendHtml);
  2139. }
  2140. },
  2141. // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
  2142. // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
  2143. // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
  2144. // We will query the View object first for any custom rendering functions, then the methods of the subclass.
  2145. getHtmlRenderer: function(rendererName, rowType) {
  2146. var view = this.view;
  2147. var generalName; // like "cellHtml"
  2148. var specificName; // like "dayCellHtml". based on rowType
  2149. var provider; // either the View or the RowRenderer subclass, whichever provided the method
  2150. var renderer;
  2151. generalName = rendererName + 'Html';
  2152. if (rowType) {
  2153. specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
  2154. }
  2155. if (specificName && (renderer = view[specificName])) {
  2156. provider = view;
  2157. }
  2158. else if (specificName && (renderer = this[specificName])) {
  2159. provider = this;
  2160. }
  2161. else if ((renderer = view[generalName])) {
  2162. provider = view;
  2163. }
  2164. else if ((renderer = this[generalName])) {
  2165. provider = this;
  2166. }
  2167. if (typeof renderer === 'function') {
  2168. return function() {
  2169. return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
  2170. };
  2171. }
  2172. // the rendered can be a plain string as well. if not specified, always an empty string.
  2173. return function() {
  2174. return renderer || '';
  2175. };
  2176. }
  2177. });
  2178. ;;
  2179. /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
  2180. ----------------------------------------------------------------------------------------------------------------------*/
  2181. var Grid = fc.Grid = RowRenderer.extend({
  2182. start: null, // the date of the first cell
  2183. end: null, // the date after the last cell
  2184. rowCnt: 0, // number of rows
  2185. colCnt: 0, // number of cols
  2186. el: null, // the containing element
  2187. coordMap: null, // a GridCoordMap that converts pixel values to datetimes
  2188. elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  2189. externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
  2190. // derived from options
  2191. colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
  2192. eventTimeFormat: null,
  2193. displayEventTime: null,
  2194. displayEventEnd: null,
  2195. // if all cells are the same length of time, the duration they all share. optional.
  2196. // when defined, allows the computeCellRange shortcut, as well as improved resizing behavior.
  2197. cellDuration: null,
  2198. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  2199. // of the date cells. if not defined, assumes to be day and time granularity.
  2200. largeUnit: null,
  2201. constructor: function() {
  2202. RowRenderer.apply(this, arguments); // call the super-constructor
  2203. this.coordMap = new GridCoordMap(this);
  2204. this.elsByFill = {};
  2205. this.externalDragStartProxy = proxy(this, 'externalDragStart');
  2206. },
  2207. /* Options
  2208. ------------------------------------------------------------------------------------------------------------------*/
  2209. // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
  2210. // TODO: move to another class. not applicable to all Grids
  2211. computeColHeadFormat: function() {
  2212. // subclasses must implement if they want to use headHtml()
  2213. },
  2214. // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  2215. computeEventTimeFormat: function() {
  2216. return this.view.opt('smallTimeFormat');
  2217. },
  2218. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
  2219. // Only applies to non-all-day events.
  2220. computeDisplayEventTime: function() {
  2221. return true;
  2222. },
  2223. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  2224. computeDisplayEventEnd: function() {
  2225. return true;
  2226. },
  2227. /* Dates
  2228. ------------------------------------------------------------------------------------------------------------------*/
  2229. // Tells the grid about what period of time to display.
  2230. // Any date-related cell system internal data should be generated.
  2231. setRange: function(range) {
  2232. this.start = range.start.clone();
  2233. this.end = range.end.clone();
  2234. this.rangeUpdated();
  2235. this.processRangeOptions();
  2236. },
  2237. // Called when internal variables that rely on the range should be updated
  2238. rangeUpdated: function() {
  2239. },
  2240. // Updates values that rely on options and also relate to range
  2241. processRangeOptions: function() {
  2242. var view = this.view;
  2243. var displayEventTime;
  2244. var displayEventEnd;
  2245. // Populate option-derived settings. Look for override first, then compute if necessary.
  2246. this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();
  2247. this.eventTimeFormat =
  2248. view.opt('eventTimeFormat') ||
  2249. view.opt('timeFormat') || // deprecated
  2250. this.computeEventTimeFormat();
  2251. displayEventTime = view.opt('displayEventTime');
  2252. if (displayEventTime == null) {
  2253. displayEventTime = this.computeDisplayEventTime(); // might be based off of range
  2254. }
  2255. displayEventEnd = view.opt('displayEventEnd');
  2256. if (displayEventEnd == null) {
  2257. displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
  2258. }
  2259. this.displayEventTime = displayEventTime;
  2260. this.displayEventEnd = displayEventEnd;
  2261. },
  2262. // Called before the grid's coordinates will need to be queried for cells.
  2263. // Any non-date-related cell system internal data should be built.
  2264. build: function() {
  2265. },
  2266. // Called after the grid's coordinates are done being relied upon.
  2267. // Any non-date-related cell system internal data should be cleared.
  2268. clear: function() {
  2269. },
  2270. // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  2271. rangeToSegs: function(range) {
  2272. // subclasses must implement
  2273. },
  2274. // Diffs the two dates, returning a duration, based on granularity of the grid
  2275. diffDates: function(a, b) {
  2276. if (this.largeUnit) {
  2277. return diffByUnit(a, b, this.largeUnit);
  2278. }
  2279. else {
  2280. return diffDayTime(a, b);
  2281. }
  2282. },
  2283. /* Cells
  2284. ------------------------------------------------------------------------------------------------------------------*/
  2285. // NOTE: columns are ordered left-to-right
  2286. // Gets an object containing row/col number, misc data, and range information about the cell.
  2287. // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
  2288. getCell: function(row, col) {
  2289. var cell;
  2290. if (col == null) {
  2291. if (typeof row === 'number') { // a single-number offset
  2292. col = row % this.colCnt;
  2293. row = Math.floor(row / this.colCnt);
  2294. }
  2295. else { // an object with row/col properties
  2296. col = row.col;
  2297. row = row.row;
  2298. }
  2299. }
  2300. cell = { row: row, col: col };
  2301. $.extend(cell, this.getRowData(row), this.getColData(col));
  2302. $.extend(cell, this.computeCellRange(cell));
  2303. return cell;
  2304. },
  2305. // Given a cell object with index and misc data, generates a range object
  2306. // If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does.
  2307. // If being overridden, should return a range with reference-free date copies.
  2308. computeCellRange: function(cell) {
  2309. var date = this.computeCellDate(cell);
  2310. return {
  2311. start: date,
  2312. end: date.clone().add(this.cellDuration)
  2313. };
  2314. },
  2315. // Given a cell, returns its start date. Should return a reference-free date copy.
  2316. computeCellDate: function(cell) {
  2317. // subclasses can implement
  2318. },
  2319. // Retrieves misc data about the given row
  2320. getRowData: function(row) {
  2321. return {};
  2322. },
  2323. // Retrieves misc data baout the given column
  2324. getColData: function(col) {
  2325. return {};
  2326. },
  2327. // Retrieves the element representing the given row
  2328. getRowEl: function(row) {
  2329. // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
  2330. },
  2331. // Retrieves the element representing the given column
  2332. getColEl: function(col) {
  2333. // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
  2334. },
  2335. // Given a cell object, returns the element that represents the cell's whole-day
  2336. getCellDayEl: function(cell) {
  2337. return this.getColEl(cell.col) || this.getRowEl(cell.row);
  2338. },
  2339. /* Cell Coordinates
  2340. ------------------------------------------------------------------------------------------------------------------*/
  2341. // Computes the top/bottom coordinates of all rows.
  2342. // By default, queries the dimensions of the element provided by getRowEl().
  2343. computeRowCoords: function() {
  2344. var items = [];
  2345. var i, el;
  2346. var top;
  2347. for (i = 0; i < this.rowCnt; i++) {
  2348. el = this.getRowEl(i);
  2349. top = el.offset().top;
  2350. items.push({
  2351. top: top,
  2352. bottom: top + el.outerHeight()
  2353. });
  2354. }
  2355. return items;
  2356. },
  2357. // Computes the left/right coordinates of all rows.
  2358. // By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL.
  2359. computeColCoords: function() {
  2360. var items = [];
  2361. var i, el;
  2362. var left;
  2363. for (i = 0; i < this.colCnt; i++) {
  2364. el = this.getColEl(i);
  2365. left = el.offset().left;
  2366. items.push({
  2367. left: left,
  2368. right: left + el.outerWidth()
  2369. });
  2370. }
  2371. return items;
  2372. },
  2373. /* Rendering
  2374. ------------------------------------------------------------------------------------------------------------------*/
  2375. // Sets the container element that the grid should render inside of.
  2376. // Does other DOM-related initializations.
  2377. setElement: function(el) {
  2378. var _this = this;
  2379. this.el = el;
  2380. // attach a handler to the grid's root element.
  2381. // jQuery will take care of unregistering them when removeElement gets called.
  2382. el.on('mousedown', function(ev) {
  2383. if (
  2384. !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
  2385. !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
  2386. ) {
  2387. _this.dayMousedown(ev);
  2388. }
  2389. });
  2390. // attach event-element-related handlers. in Grid.events
  2391. // same garbage collection note as above.
  2392. this.bindSegHandlers();
  2393. this.bindGlobalHandlers();
  2394. },
  2395. // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
  2396. // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
  2397. removeElement: function() {
  2398. this.unbindGlobalHandlers();
  2399. this.el.remove();
  2400. // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
  2401. },
  2402. // Renders the basic structure of grid view before any content is rendered
  2403. renderSkeleton: function() {
  2404. // subclasses should implement
  2405. },
  2406. // Renders the grid's date-related content (like cells that represent days/times).
  2407. // Assumes setRange has already been called and the skeleton has already been rendered.
  2408. renderDates: function() {
  2409. // subclasses should implement
  2410. },
  2411. // Unrenders the grid's date-related content
  2412. unrenderDates: function() {
  2413. // subclasses should implement
  2414. },
  2415. /* Handlers
  2416. ------------------------------------------------------------------------------------------------------------------*/
  2417. // Binds DOM handlers to elements that reside outside the grid, such as the document
  2418. bindGlobalHandlers: function() {
  2419. $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
  2420. },
  2421. // Unbinds DOM handlers from elements that reside outside the grid
  2422. unbindGlobalHandlers: function() {
  2423. $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
  2424. },
  2425. // Process a mousedown on an element that represents a day. For day clicking and selecting.
  2426. dayMousedown: function(ev) {
  2427. var _this = this;
  2428. var view = this.view;
  2429. var isSelectable = view.opt('selectable');
  2430. var dayClickCell; // null if invalid dayClick
  2431. var selectionRange; // null if invalid selection
  2432. // this listener tracks a mousedown on a day element, and a subsequent drag.
  2433. // if the drag ends on the same day, it is a 'dayClick'.
  2434. // if 'selectable' is enabled, this listener also detects selections.
  2435. var dragListener = new CellDragListener(this.coordMap, {
  2436. //distance: 5, // needs more work if we want dayClick to fire correctly
  2437. scroll: view.opt('dragScroll'),
  2438. dragStart: function() {
  2439. view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  2440. },
  2441. cellOver: function(cell, isOrig, origCell) {
  2442. if (origCell) { // click needs to have started on a cell
  2443. dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
  2444. if (isSelectable) {
  2445. selectionRange = _this.computeSelection(origCell, cell);
  2446. if (selectionRange) {
  2447. _this.renderSelection(selectionRange);
  2448. }
  2449. else {
  2450. disableCursor();
  2451. }
  2452. }
  2453. }
  2454. },
  2455. cellOut: function(cell) {
  2456. dayClickCell = null;
  2457. selectionRange = null;
  2458. _this.unrenderSelection();
  2459. enableCursor();
  2460. },
  2461. listenStop: function(ev) {
  2462. if (dayClickCell) {
  2463. view.triggerDayClick(dayClickCell, _this.getCellDayEl(dayClickCell), ev);
  2464. }
  2465. if (selectionRange) {
  2466. // the selection will already have been rendered. just report it
  2467. view.reportSelection(selectionRange, ev);
  2468. }
  2469. enableCursor();
  2470. }
  2471. });
  2472. dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
  2473. },
  2474. /* Event Helper
  2475. ------------------------------------------------------------------------------------------------------------------*/
  2476. // TODO: should probably move this to Grid.events, like we did event dragging / resizing
  2477. // Renders a mock event over the given range
  2478. renderRangeHelper: function(range, sourceSeg) {
  2479. var fakeEvent = this.fabricateHelperEvent(range, sourceSeg);
  2480. this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  2481. },
  2482. // Builds a fake event given a date range it should cover, and a segment is should be inspired from.
  2483. // The range's end can be null, in which case the mock event that is rendered will have a null end time.
  2484. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  2485. fabricateHelperEvent: function(range, sourceSeg) {
  2486. var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  2487. fakeEvent.start = range.start.clone();
  2488. fakeEvent.end = range.end ? range.end.clone() : null;
  2489. fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange
  2490. this.view.calendar.normalizeEventRange(fakeEvent);
  2491. // this extra className will be useful for differentiating real events from mock events in CSS
  2492. fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  2493. // if something external is being dragged in, don't render a resizer
  2494. if (!sourceSeg) {
  2495. fakeEvent.editable = false;
  2496. }
  2497. return fakeEvent;
  2498. },
  2499. // Renders a mock event
  2500. renderHelper: function(event, sourceSeg) {
  2501. // subclasses must implement
  2502. },
  2503. // Unrenders a mock event
  2504. unrenderHelper: function() {
  2505. // subclasses must implement
  2506. },
  2507. /* Selection
  2508. ------------------------------------------------------------------------------------------------------------------*/
  2509. // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  2510. renderSelection: function(range) {
  2511. this.renderHighlight(this.selectionRangeToSegs(range));
  2512. },
  2513. // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  2514. unrenderSelection: function() {
  2515. this.unrenderHighlight();
  2516. },
  2517. // Given the first and last cells of a selection, returns a range object.
  2518. // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
  2519. // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
  2520. computeSelection: function(firstCell, lastCell) {
  2521. var dates = [
  2522. firstCell.start,
  2523. firstCell.end,
  2524. lastCell.start,
  2525. lastCell.end
  2526. ];
  2527. var range;
  2528. dates.sort(compareNumbers); // sorts chronologically. works with Moments
  2529. range = {
  2530. start: dates[0].clone(),
  2531. end: dates[3].clone()
  2532. };
  2533. if (!this.view.calendar.isSelectionRangeAllowed(range)) {
  2534. return null;
  2535. }
  2536. return range;
  2537. },
  2538. selectionRangeToSegs: function(range) {
  2539. return this.rangeToSegs(range);
  2540. },
  2541. /* Highlight
  2542. ------------------------------------------------------------------------------------------------------------------*/
  2543. // Renders an emphasis on the given date range. Given an array of segments.
  2544. renderHighlight: function(segs) {
  2545. this.renderFill('highlight', segs);
  2546. },
  2547. // Unrenders the emphasis on a date range
  2548. unrenderHighlight: function() {
  2549. this.unrenderFill('highlight');
  2550. },
  2551. // Generates an array of classNames for rendering the highlight. Used by the fill system.
  2552. highlightSegClasses: function() {
  2553. return [ 'fc-highlight' ];
  2554. },
  2555. /* Fill System (highlight, background events, business hours)
  2556. ------------------------------------------------------------------------------------------------------------------*/
  2557. // Renders a set of rectangles over the given segments of time.
  2558. // MUST RETURN a subset of segs, the segs that were actually rendered.
  2559. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  2560. renderFill: function(type, segs) {
  2561. // subclasses must implement
  2562. },
  2563. // Unrenders a specific type of fill that is currently rendered on the grid
  2564. unrenderFill: function(type) {
  2565. var el = this.elsByFill[type];
  2566. if (el) {
  2567. el.remove();
  2568. delete this.elsByFill[type];
  2569. }
  2570. },
  2571. // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  2572. // Only returns segments that successfully rendered.
  2573. // To be harnessed by renderFill (implemented by subclasses).
  2574. // Analagous to renderFgSegEls.
  2575. renderFillSegEls: function(type, segs) {
  2576. var _this = this;
  2577. var segElMethod = this[type + 'SegEl'];
  2578. var html = '';
  2579. var renderedSegs = [];
  2580. var i;
  2581. if (segs.length) {
  2582. // build a large concatenation of segment HTML
  2583. for (i = 0; i < segs.length; i++) {
  2584. html += this.fillSegHtml(type, segs[i]);
  2585. }
  2586. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  2587. // Then, compute the 'el' for each segment.
  2588. $(html).each(function(i, node) {
  2589. var seg = segs[i];
  2590. var el = $(node);
  2591. // allow custom filter methods per-type
  2592. if (segElMethod) {
  2593. el = segElMethod.call(_this, seg, el);
  2594. }
  2595. if (el) { // custom filters did not cancel the render
  2596. el = $(el); // allow custom filter to return raw DOM node
  2597. // correct element type? (would be bad if a non-TD were inserted into a table for example)
  2598. if (el.is(_this.fillSegTag)) {
  2599. seg.el = el;
  2600. renderedSegs.push(seg);
  2601. }
  2602. }
  2603. });
  2604. }
  2605. return renderedSegs;
  2606. },
  2607. fillSegTag: 'div', // subclasses can override
  2608. // Builds the HTML needed for one fill segment. Generic enought o work with different types.
  2609. fillSegHtml: function(type, seg) {
  2610. // custom hooks per-type
  2611. var classesMethod = this[type + 'SegClasses'];
  2612. var cssMethod = this[type + 'SegCss'];
  2613. var classes = classesMethod ? classesMethod.call(this, seg) : [];
  2614. var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
  2615. return '<' + this.fillSegTag +
  2616. (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  2617. (css ? ' style="' + css + '"' : '') +
  2618. ' />';
  2619. },
  2620. /* Generic rendering utilities for subclasses
  2621. ------------------------------------------------------------------------------------------------------------------*/
  2622. // Renders a day-of-week header row.
  2623. // TODO: move to another class. not applicable to all Grids
  2624. headHtml: function() {
  2625. return '' +
  2626. '<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
  2627. '<table>' +
  2628. '<thead>' +
  2629. this.rowHtml('head') + // leverages RowRenderer
  2630. '</thead>' +
  2631. '</table>' +
  2632. '</div>';
  2633. },
  2634. // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
  2635. // TODO: move to another class. not applicable to all Grids
  2636. headCellHtml: function(cell) {
  2637. var view = this.view;
  2638. var date = cell.start;
  2639. return '' +
  2640. '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
  2641. htmlEscape(date.format(this.colHeadFormat)) +
  2642. '</th>';
  2643. },
  2644. // Renders the HTML for a single-day background cell
  2645. bgCellHtml: function(cell) {
  2646. var view = this.view;
  2647. var date = cell.start;
  2648. var classes = this.getDayClasses(date);
  2649. classes.unshift('fc-day', view.widgetContentClass);
  2650. return '<td class="' + classes.join(' ') + '"' +
  2651. ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  2652. '></td>';
  2653. },
  2654. // Computes HTML classNames for a single-day cell
  2655. getDayClasses: function(date) {
  2656. var view = this.view;
  2657. var today = view.calendar.getNow().stripTime();
  2658. var classes = [ 'fc-' + dayIDs[date.day()] ];
  2659. if (
  2660. view.intervalDuration.as('months') == 1 &&
  2661. date.month() != view.intervalStart.month()
  2662. ) {
  2663. classes.push('fc-other-month');
  2664. }
  2665. if (date.isSame(today, 'day')) {
  2666. classes.push(
  2667. 'fc-today',
  2668. view.highlightStateClass
  2669. );
  2670. }
  2671. else if (date < today) {
  2672. classes.push('fc-past');
  2673. }
  2674. else {
  2675. classes.push('fc-future');
  2676. }
  2677. return classes;
  2678. }
  2679. });
  2680. ;;
  2681. /* Event-rendering and event-interaction methods for the abstract Grid class
  2682. ----------------------------------------------------------------------------------------------------------------------*/
  2683. Grid.mixin({
  2684. mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  2685. isDraggingSeg: false, // is a segment being dragged? boolean
  2686. isResizingSeg: false, // is a segment being resized? boolean
  2687. isDraggingExternal: false, // jqui-dragging an external element? boolean
  2688. segs: null, // the event segments currently rendered in the grid
  2689. // Renders the given events onto the grid
  2690. renderEvents: function(events) {
  2691. var segs = this.eventsToSegs(events);
  2692. var bgSegs = [];
  2693. var fgSegs = [];
  2694. var i, seg;
  2695. for (i = 0; i < segs.length; i++) {
  2696. seg = segs[i];
  2697. if (isBgEvent(seg.event)) {
  2698. bgSegs.push(seg);
  2699. }
  2700. else {
  2701. fgSegs.push(seg);
  2702. }
  2703. }
  2704. // Render each different type of segment.
  2705. // Each function may return a subset of the segs, segs that were actually rendered.
  2706. bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
  2707. fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
  2708. this.segs = bgSegs.concat(fgSegs);
  2709. },
  2710. // Unrenders all events currently rendered on the grid
  2711. unrenderEvents: function() {
  2712. this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  2713. this.unrenderFgSegs();
  2714. this.unrenderBgSegs();
  2715. this.segs = null;
  2716. },
  2717. // Retrieves all rendered segment objects currently rendered on the grid
  2718. getEventSegs: function() {
  2719. return this.segs || [];
  2720. },
  2721. /* Foreground Segment Rendering
  2722. ------------------------------------------------------------------------------------------------------------------*/
  2723. // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  2724. renderFgSegs: function(segs) {
  2725. // subclasses must implement
  2726. },
  2727. // Unrenders all currently rendered foreground segments
  2728. unrenderFgSegs: function() {
  2729. // subclasses must implement
  2730. },
  2731. // Renders and assigns an `el` property for each foreground event segment.
  2732. // Only returns segments that successfully rendered.
  2733. // A utility that subclasses may use.
  2734. renderFgSegEls: function(segs, disableResizing) {
  2735. var view = this.view;
  2736. var html = '';
  2737. var renderedSegs = [];
  2738. var i;
  2739. if (segs.length) { // don't build an empty html string
  2740. // build a large concatenation of event segment HTML
  2741. for (i = 0; i < segs.length; i++) {
  2742. html += this.fgSegHtml(segs[i], disableResizing);
  2743. }
  2744. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  2745. // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  2746. $(html).each(function(i, node) {
  2747. var seg = segs[i];
  2748. var el = view.resolveEventEl(seg.event, $(node));
  2749. if (el) {
  2750. el.data('fc-seg', seg); // used by handlers
  2751. seg.el = el;
  2752. renderedSegs.push(seg);
  2753. }
  2754. });
  2755. }
  2756. return renderedSegs;
  2757. },
  2758. // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  2759. fgSegHtml: function(seg, disableResizing) {
  2760. // subclasses should implement
  2761. },
  2762. /* Background Segment Rendering
  2763. ------------------------------------------------------------------------------------------------------------------*/
  2764. // Renders the given background event segments onto the grid.
  2765. // Returns a subset of the segs that were actually rendered.
  2766. renderBgSegs: function(segs) {
  2767. return this.renderFill('bgEvent', segs);
  2768. },
  2769. // Unrenders all the currently rendered background event segments
  2770. unrenderBgSegs: function() {
  2771. this.unrenderFill('bgEvent');
  2772. },
  2773. // Renders a background event element, given the default rendering. Called by the fill system.
  2774. bgEventSegEl: function(seg, el) {
  2775. return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  2776. },
  2777. // Generates an array of classNames to be used for the default rendering of a background event.
  2778. // Called by the fill system.
  2779. bgEventSegClasses: function(seg) {
  2780. var event = seg.event;
  2781. var source = event.source || {};
  2782. return [ 'fc-bgevent' ].concat(
  2783. event.className,
  2784. source.className || []
  2785. );
  2786. },
  2787. // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  2788. // Called by the fill system.
  2789. // TODO: consolidate with getEventSkinCss?
  2790. bgEventSegCss: function(seg) {
  2791. var view = this.view;
  2792. var event = seg.event;
  2793. var source = event.source || {};
  2794. return {
  2795. 'background-color':
  2796. event.backgroundColor ||
  2797. event.color ||
  2798. source.backgroundColor ||
  2799. source.color ||
  2800. view.opt('eventBackgroundColor') ||
  2801. view.opt('eventColor')
  2802. };
  2803. },
  2804. // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  2805. businessHoursSegClasses: function(seg) {
  2806. return [ 'fc-nonbusiness', 'fc-bgevent' ];
  2807. },
  2808. /* Handlers
  2809. ------------------------------------------------------------------------------------------------------------------*/
  2810. // Attaches event-element-related handlers to the container element and leverage bubbling
  2811. bindSegHandlers: function() {
  2812. var _this = this;
  2813. var view = this.view;
  2814. $.each(
  2815. {
  2816. mouseenter: function(seg, ev) {
  2817. _this.triggerSegMouseover(seg, ev);
  2818. },
  2819. mouseleave: function(seg, ev) {
  2820. _this.triggerSegMouseout(seg, ev);
  2821. },
  2822. click: function(seg, ev) {
  2823. return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
  2824. },
  2825. mousedown: function(seg, ev) {
  2826. if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
  2827. _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer'));
  2828. }
  2829. else if (view.isEventDraggable(seg.event)) {
  2830. _this.segDragMousedown(seg, ev);
  2831. }
  2832. }
  2833. },
  2834. function(name, func) {
  2835. // attach the handler to the container element and only listen for real event elements via bubbling
  2836. _this.el.on(name, '.fc-event-container > *', function(ev) {
  2837. var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  2838. // only call the handlers if there is not a drag/resize in progress
  2839. if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  2840. return func.call(this, seg, ev); // `this` will be the event element
  2841. }
  2842. });
  2843. }
  2844. );
  2845. },
  2846. // Updates internal state and triggers handlers for when an event element is moused over
  2847. triggerSegMouseover: function(seg, ev) {
  2848. if (!this.mousedOverSeg) {
  2849. this.mousedOverSeg = seg;
  2850. this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
  2851. }
  2852. },
  2853. // Updates internal state and triggers handlers for when an event element is moused out.
  2854. // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  2855. triggerSegMouseout: function(seg, ev) {
  2856. ev = ev || {}; // if given no args, make a mock mouse event
  2857. if (this.mousedOverSeg) {
  2858. seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  2859. this.mousedOverSeg = null;
  2860. this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
  2861. }
  2862. },
  2863. /* Event Dragging
  2864. ------------------------------------------------------------------------------------------------------------------*/
  2865. // Called when the user does a mousedown on an event, which might lead to dragging.
  2866. // Generic enough to work with any type of Grid.
  2867. segDragMousedown: function(seg, ev) {
  2868. var _this = this;
  2869. var view = this.view;
  2870. var calendar = view.calendar;
  2871. var el = seg.el;
  2872. var event = seg.event;
  2873. var dropLocation;
  2874. // A clone of the original element that will move with the mouse
  2875. var mouseFollower = new MouseFollower(seg.el, {
  2876. parentEl: view.el,
  2877. opacity: view.opt('dragOpacity'),
  2878. revertDuration: view.opt('dragRevertDuration'),
  2879. zIndex: 2 // one above the .fc-view
  2880. });
  2881. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  2882. // of the view.
  2883. var dragListener = new CellDragListener(view.coordMap, {
  2884. distance: 5,
  2885. scroll: view.opt('dragScroll'),
  2886. subjectEl: el,
  2887. subjectCenter: true,
  2888. listenStart: function(ev) {
  2889. mouseFollower.hide(); // don't show until we know this is a real drag
  2890. mouseFollower.start(ev);
  2891. },
  2892. dragStart: function(ev) {
  2893. _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  2894. _this.segDragStart(seg, ev);
  2895. view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  2896. },
  2897. cellOver: function(cell, isOrig, origCell) {
  2898. // starting cell could be forced (DayGrid.limit)
  2899. if (seg.cell) {
  2900. origCell = seg.cell;
  2901. }
  2902. dropLocation = _this.computeEventDrop(origCell, cell, event);
  2903. if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) {
  2904. disableCursor();
  2905. dropLocation = null;
  2906. }
  2907. // if a valid drop location, have the subclass render a visual indication
  2908. if (dropLocation && view.renderDrag(dropLocation, seg)) {
  2909. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  2910. }
  2911. else {
  2912. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  2913. }
  2914. if (isOrig) {
  2915. dropLocation = null; // needs to have moved cells to be a valid drop
  2916. }
  2917. },
  2918. cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  2919. view.unrenderDrag(); // unrender whatever was done in renderDrag
  2920. mouseFollower.show(); // show in case we are moving out of all cells
  2921. dropLocation = null;
  2922. },
  2923. cellDone: function() { // Called after a cellOut OR before a dragStop
  2924. enableCursor();
  2925. },
  2926. dragStop: function(ev) {
  2927. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  2928. mouseFollower.stop(!dropLocation, function() {
  2929. view.unrenderDrag();
  2930. view.showEvent(event);
  2931. _this.segDragStop(seg, ev);
  2932. if (dropLocation) {
  2933. view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
  2934. }
  2935. });
  2936. },
  2937. listenStop: function() {
  2938. mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
  2939. }
  2940. });
  2941. dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  2942. },
  2943. // Called before event segment dragging starts
  2944. segDragStart: function(seg, ev) {
  2945. this.isDraggingSeg = true;
  2946. this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  2947. },
  2948. // Called after event segment dragging stops
  2949. segDragStop: function(seg, ev) {
  2950. this.isDraggingSeg = false;
  2951. this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  2952. },
  2953. // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay
  2954. // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
  2955. // A falsy returned value indicates an invalid drop.
  2956. computeEventDrop: function(startCell, endCell, event) {
  2957. var calendar = this.view.calendar;
  2958. var dragStart = startCell.start;
  2959. var dragEnd = endCell.start;
  2960. var delta;
  2961. var dropLocation;
  2962. if (dragStart.hasTime() === dragEnd.hasTime()) {
  2963. delta = this.diffDates(dragEnd, dragStart);
  2964. // if an all-day event was in a timed area and it was dragged to a different time,
  2965. // guarantee an end and adjust start/end to have times
  2966. if (event.allDay && durationHasTime(delta)) {
  2967. dropLocation = {
  2968. start: event.start.clone(),
  2969. end: calendar.getEventEnd(event), // will be an ambig day
  2970. allDay: false // for normalizeEventRangeTimes
  2971. };
  2972. calendar.normalizeEventRangeTimes(dropLocation);
  2973. }
  2974. // othewise, work off existing values
  2975. else {
  2976. dropLocation = {
  2977. start: event.start.clone(),
  2978. end: event.end ? event.end.clone() : null,
  2979. allDay: event.allDay // keep it the same
  2980. };
  2981. }
  2982. dropLocation.start.add(delta);
  2983. if (dropLocation.end) {
  2984. dropLocation.end.add(delta);
  2985. }
  2986. }
  2987. else {
  2988. // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  2989. dropLocation = {
  2990. start: dragEnd.clone(),
  2991. end: null, // end should be cleared
  2992. allDay: !dragEnd.hasTime()
  2993. };
  2994. }
  2995. return dropLocation;
  2996. },
  2997. // Utility for apply dragOpacity to a jQuery set
  2998. applyDragOpacity: function(els) {
  2999. var opacity = this.view.opt('dragOpacity');
  3000. if (opacity != null) {
  3001. els.each(function(i, node) {
  3002. // Don't use jQuery (will set an IE filter), do it the old fashioned way.
  3003. // In IE8, a helper element will disappears if there's a filter.
  3004. node.style.opacity = opacity;
  3005. });
  3006. }
  3007. },
  3008. /* External Element Dragging
  3009. ------------------------------------------------------------------------------------------------------------------*/
  3010. // Called when a jQuery UI drag is initiated anywhere in the DOM
  3011. externalDragStart: function(ev, ui) {
  3012. var view = this.view;
  3013. var el;
  3014. var accept;
  3015. if (view.opt('droppable')) { // only listen if this setting is on
  3016. el = $((ui ? ui.item : null) || ev.target);
  3017. // Test that the dragged element passes the dropAccept selector or filter function.
  3018. // FYI, the default is "*" (matches all)
  3019. accept = view.opt('dropAccept');
  3020. if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  3021. if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  3022. this.listenToExternalDrag(el, ev, ui);
  3023. }
  3024. }
  3025. }
  3026. },
  3027. // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping
  3028. listenToExternalDrag: function(el, ev, ui) {
  3029. var _this = this;
  3030. var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  3031. var dragListener;
  3032. var dropLocation; // a null value signals an unsuccessful drag
  3033. // listener that tracks mouse movement over date-associated pixel regions
  3034. dragListener = new CellDragListener(this.coordMap, {
  3035. listenStart: function() {
  3036. _this.isDraggingExternal = true;
  3037. },
  3038. cellOver: function(cell) {
  3039. dropLocation = _this.computeExternalDrop(cell, meta);
  3040. if (dropLocation) {
  3041. _this.renderDrag(dropLocation); // called without a seg parameter
  3042. }
  3043. else { // invalid drop cell
  3044. disableCursor();
  3045. }
  3046. },
  3047. cellOut: function() {
  3048. dropLocation = null; // signal unsuccessful
  3049. _this.unrenderDrag();
  3050. enableCursor();
  3051. },
  3052. dragStop: function() {
  3053. _this.unrenderDrag();
  3054. enableCursor();
  3055. if (dropLocation) { // element was dropped on a valid date/time cell
  3056. _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
  3057. }
  3058. },
  3059. listenStop: function() {
  3060. _this.isDraggingExternal = false;
  3061. }
  3062. });
  3063. dragListener.startDrag(ev); // start listening immediately
  3064. },
  3065. // Given a cell to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  3066. // returns start/end dates for the event that would result from the hypothetical drop. end might be null.
  3067. // Returning a null value signals an invalid drop cell.
  3068. computeExternalDrop: function(cell, meta) {
  3069. var dropLocation = {
  3070. start: cell.start.clone(),
  3071. end: null
  3072. };
  3073. // if dropped on an all-day cell, and element's metadata specified a time, set it
  3074. if (meta.startTime && !dropLocation.start.hasTime()) {
  3075. dropLocation.start.time(meta.startTime);
  3076. }
  3077. if (meta.duration) {
  3078. dropLocation.end = dropLocation.start.clone().add(meta.duration);
  3079. }
  3080. if (!this.view.calendar.isExternalDropRangeAllowed(dropLocation, meta.eventProps)) {
  3081. return null;
  3082. }
  3083. return dropLocation;
  3084. },
  3085. /* Drag Rendering (for both events and an external elements)
  3086. ------------------------------------------------------------------------------------------------------------------*/
  3087. // Renders a visual indication of an event or external element being dragged.
  3088. // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
  3089. // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
  3090. // A truthy returned value indicates this method has rendered a helper element.
  3091. renderDrag: function(dropLocation, seg) {
  3092. // subclasses must implement
  3093. },
  3094. // Unrenders a visual indication of an event or external element being dragged
  3095. unrenderDrag: function() {
  3096. // subclasses must implement
  3097. },
  3098. /* Resizing
  3099. ------------------------------------------------------------------------------------------------------------------*/
  3100. // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
  3101. // Generic enough to work with any type of Grid.
  3102. segResizeMousedown: function(seg, ev, isStart) {
  3103. var _this = this;
  3104. var view = this.view;
  3105. var calendar = view.calendar;
  3106. var el = seg.el;
  3107. var event = seg.event;
  3108. var eventEnd = calendar.getEventEnd(event);
  3109. var dragListener;
  3110. var resizeLocation; // falsy if invalid resize
  3111. // Tracks mouse movement over the *grid's* coordinate map
  3112. dragListener = new CellDragListener(this.coordMap, {
  3113. distance: 5,
  3114. scroll: view.opt('dragScroll'),
  3115. subjectEl: el,
  3116. dragStart: function(ev) {
  3117. _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3118. _this.segResizeStart(seg, ev);
  3119. },
  3120. cellOver: function(cell, isOrig, origCell) {
  3121. resizeLocation = isStart ?
  3122. _this.computeEventStartResize(origCell, cell, event) :
  3123. _this.computeEventEndResize(origCell, cell, event);
  3124. if (resizeLocation) {
  3125. if (!calendar.isEventRangeAllowed(resizeLocation, event)) {
  3126. disableCursor();
  3127. resizeLocation = null;
  3128. }
  3129. // no change? (TODO: how does this work with timezones?)
  3130. else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
  3131. resizeLocation = null;
  3132. }
  3133. }
  3134. if (resizeLocation) {
  3135. view.hideEvent(event);
  3136. _this.renderEventResize(resizeLocation, seg);
  3137. }
  3138. },
  3139. cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  3140. resizeLocation = null;
  3141. },
  3142. cellDone: function() { // resets the rendering to show the original event
  3143. _this.unrenderEventResize();
  3144. view.showEvent(event);
  3145. enableCursor();
  3146. },
  3147. dragStop: function(ev) {
  3148. _this.segResizeStop(seg, ev);
  3149. if (resizeLocation) { // valid date to resize to?
  3150. view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
  3151. }
  3152. }
  3153. });
  3154. dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  3155. },
  3156. // Called before event segment resizing starts
  3157. segResizeStart: function(seg, ev) {
  3158. this.isResizingSeg = true;
  3159. this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3160. },
  3161. // Called after event segment resizing stops
  3162. segResizeStop: function(seg, ev) {
  3163. this.isResizingSeg = false;
  3164. this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3165. },
  3166. // Returns new date-information for an event segment being resized from its start
  3167. computeEventStartResize: function(startCell, endCell, event) {
  3168. return this.computeEventResize('start', startCell, endCell, event);
  3169. },
  3170. // Returns new date-information for an event segment being resized from its end
  3171. computeEventEndResize: function(startCell, endCell, event) {
  3172. return this.computeEventResize('end', startCell, endCell, event);
  3173. },
  3174. // Returns new date-information for an event segment being resized from its start OR end
  3175. // `type` is either 'start' or 'end'
  3176. computeEventResize: function(type, startCell, endCell, event) {
  3177. var calendar = this.view.calendar;
  3178. var delta = this.diffDates(endCell[type], startCell[type]);
  3179. var range;
  3180. var defaultDuration;
  3181. // build original values to work from, guaranteeing a start and end
  3182. range = {
  3183. start: event.start.clone(),
  3184. end: calendar.getEventEnd(event),
  3185. allDay: event.allDay
  3186. };
  3187. // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
  3188. if (range.allDay && durationHasTime(delta)) {
  3189. range.allDay = false;
  3190. calendar.normalizeEventRangeTimes(range);
  3191. }
  3192. range[type].add(delta); // apply delta to start or end
  3193. // if the event was compressed too small, find a new reasonable duration for it
  3194. if (!range.start.isBefore(range.end)) {
  3195. defaultDuration = event.allDay ?
  3196. calendar.defaultAllDayEventDuration :
  3197. calendar.defaultTimedEventDuration;
  3198. // between the cell's duration and the event's default duration, use the smaller of the two.
  3199. // example: if year-length slots, and compressed to one slot, we don't want the event to be a year long
  3200. if (this.cellDuration && this.cellDuration < defaultDuration) {
  3201. defaultDuration = this.cellDuration;
  3202. }
  3203. if (type == 'start') { // resizing the start?
  3204. range.start = range.end.clone().subtract(defaultDuration);
  3205. }
  3206. else { // resizing the end?
  3207. range.end = range.start.clone().add(defaultDuration);
  3208. }
  3209. }
  3210. return range;
  3211. },
  3212. // Renders a visual indication of an event being resized.
  3213. // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
  3214. renderEventResize: function(range, seg) {
  3215. // subclasses must implement
  3216. },
  3217. // Unrenders a visual indication of an event being resized.
  3218. unrenderEventResize: function() {
  3219. // subclasses must implement
  3220. },
  3221. /* Rendering Utils
  3222. ------------------------------------------------------------------------------------------------------------------*/
  3223. // Compute the text that should be displayed on an event's element.
  3224. // `range` can be the Event object itself, or something range-like, with at least a `start`.
  3225. // If event times are disabled, or the event has no time, will return a blank string.
  3226. // If not specified, formatStr will default to the eventTimeFormat setting,
  3227. // and displayEnd will default to the displayEventEnd setting.
  3228. getEventTimeText: function(range, formatStr, displayEnd) {
  3229. if (formatStr == null) {
  3230. formatStr = this.eventTimeFormat;
  3231. }
  3232. if (displayEnd == null) {
  3233. displayEnd = this.displayEventEnd;
  3234. }
  3235. if (this.displayEventTime && range.start.hasTime()) {
  3236. if (displayEnd && range.end) {
  3237. return this.view.formatRange(range, formatStr);
  3238. }
  3239. else {
  3240. return range.start.format(formatStr);
  3241. }
  3242. }
  3243. return '';
  3244. },
  3245. // Generic utility for generating the HTML classNames for an event segment's element
  3246. getSegClasses: function(seg, isDraggable, isResizable) {
  3247. var event = seg.event;
  3248. var classes = [
  3249. 'fc-event',
  3250. seg.isStart ? 'fc-start' : 'fc-not-start',
  3251. seg.isEnd ? 'fc-end' : 'fc-not-end'
  3252. ].concat(
  3253. event.className,
  3254. event.source ? event.source.className : []
  3255. );
  3256. if (isDraggable) {
  3257. classes.push('fc-draggable');
  3258. }
  3259. if (isResizable) {
  3260. classes.push('fc-resizable');
  3261. }
  3262. return classes;
  3263. },
  3264. // Utility for generating event skin-related CSS properties
  3265. getEventSkinCss: function(event) {
  3266. var view = this.view;
  3267. var source = event.source || {};
  3268. var eventColor = event.color;
  3269. var sourceColor = source.color;
  3270. var optionColor = view.opt('eventColor');
  3271. return {
  3272. 'background-color':
  3273. event.backgroundColor ||
  3274. eventColor ||
  3275. source.backgroundColor ||
  3276. sourceColor ||
  3277. view.opt('eventBackgroundColor') ||
  3278. optionColor,
  3279. 'border-color':
  3280. event.borderColor ||
  3281. eventColor ||
  3282. source.borderColor ||
  3283. sourceColor ||
  3284. view.opt('eventBorderColor') ||
  3285. optionColor,
  3286. color:
  3287. event.textColor ||
  3288. source.textColor ||
  3289. view.opt('eventTextColor')
  3290. };
  3291. },
  3292. /* Converting events -> ranges -> segs
  3293. ------------------------------------------------------------------------------------------------------------------*/
  3294. // Converts an array of event objects into an array of event segment objects.
  3295. // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
  3296. // Doesn't guarantee an order for the resulting array.
  3297. eventsToSegs: function(events, rangeToSegsFunc) {
  3298. var eventRanges = this.eventsToRanges(events);
  3299. var segs = [];
  3300. var i;
  3301. for (i = 0; i < eventRanges.length; i++) {
  3302. segs.push.apply(
  3303. segs,
  3304. this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
  3305. );
  3306. }
  3307. return segs;
  3308. },
  3309. // Converts an array of events into an array of "range" objects.
  3310. // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
  3311. // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
  3312. // will create an array of ranges that span the time *not* covered by the given event.
  3313. // Doesn't guarantee an order for the resulting array.
  3314. eventsToRanges: function(events) {
  3315. var _this = this;
  3316. var eventsById = groupEventsById(events);
  3317. var ranges = [];
  3318. // group by ID so that related inverse-background events can be rendered together
  3319. $.each(eventsById, function(id, eventGroup) {
  3320. if (eventGroup.length) {
  3321. ranges.push.apply(
  3322. ranges,
  3323. isInverseBgEvent(eventGroup[0]) ?
  3324. _this.eventsToInverseRanges(eventGroup) :
  3325. _this.eventsToNormalRanges(eventGroup)
  3326. );
  3327. }
  3328. });
  3329. return ranges;
  3330. },
  3331. // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
  3332. eventsToNormalRanges: function(events) {
  3333. var calendar = this.view.calendar;
  3334. var ranges = [];
  3335. var i, event;
  3336. var eventStart, eventEnd;
  3337. for (i = 0; i < events.length; i++) {
  3338. event = events[i];
  3339. // make copies and normalize by stripping timezone
  3340. eventStart = event.start.clone().stripZone();
  3341. eventEnd = calendar.getEventEnd(event).stripZone();
  3342. ranges.push({
  3343. event: event,
  3344. start: eventStart,
  3345. end: eventEnd,
  3346. eventStartMS: +eventStart,
  3347. eventDurationMS: eventEnd - eventStart
  3348. });
  3349. }
  3350. return ranges;
  3351. },
  3352. // Converts an array of events, with inverse-background rendering, into an array of range objects.
  3353. // The range objects will cover all the time NOT covered by the events.
  3354. eventsToInverseRanges: function(events) {
  3355. var view = this.view;
  3356. var viewStart = view.start.clone().stripZone(); // normalize timezone
  3357. var viewEnd = view.end.clone().stripZone(); // normalize timezone
  3358. var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
  3359. var inverseRanges = [];
  3360. var event0 = events[0]; // assign this to each range's `.event`
  3361. var start = viewStart; // the end of the previous range. the start of the new range
  3362. var i, normalRange;
  3363. // ranges need to be in order. required for our date-walking algorithm
  3364. normalRanges.sort(compareNormalRanges);
  3365. for (i = 0; i < normalRanges.length; i++) {
  3366. normalRange = normalRanges[i];
  3367. // add the span of time before the event (if there is any)
  3368. if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
  3369. inverseRanges.push({
  3370. event: event0,
  3371. start: start,
  3372. end: normalRange.start
  3373. });
  3374. }
  3375. start = normalRange.end;
  3376. }
  3377. // add the span of time after the last event (if there is any)
  3378. if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  3379. inverseRanges.push({
  3380. event: event0,
  3381. start: start,
  3382. end: viewEnd
  3383. });
  3384. }
  3385. return inverseRanges;
  3386. },
  3387. // Slices the given event range into one or more segment objects.
  3388. // A `rangeToSegsFunc` custom slicing function can be given.
  3389. eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
  3390. var segs;
  3391. var i, seg;
  3392. eventRange = this.view.calendar.ensureVisibleEventRange(eventRange);
  3393. if (rangeToSegsFunc) {
  3394. segs = rangeToSegsFunc(eventRange);
  3395. }
  3396. else {
  3397. segs = this.rangeToSegs(eventRange); // defined by the subclass
  3398. }
  3399. for (i = 0; i < segs.length; i++) {
  3400. seg = segs[i];
  3401. seg.event = eventRange.event;
  3402. seg.eventStartMS = eventRange.eventStartMS;
  3403. seg.eventDurationMS = eventRange.eventDurationMS;
  3404. }
  3405. return segs;
  3406. }
  3407. });
  3408. /* Utilities
  3409. ----------------------------------------------------------------------------------------------------------------------*/
  3410. function isBgEvent(event) { // returns true if background OR inverse-background
  3411. var rendering = getEventRendering(event);
  3412. return rendering === 'background' || rendering === 'inverse-background';
  3413. }
  3414. function isInverseBgEvent(event) {
  3415. return getEventRendering(event) === 'inverse-background';
  3416. }
  3417. function getEventRendering(event) {
  3418. return firstDefined((event.source || {}).rendering, event.rendering);
  3419. }
  3420. function groupEventsById(events) {
  3421. var eventsById = {};
  3422. var i, event;
  3423. for (i = 0; i < events.length; i++) {
  3424. event = events[i];
  3425. (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  3426. }
  3427. return eventsById;
  3428. }
  3429. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  3430. function compareNormalRanges(range1, range2) {
  3431. return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
  3432. }
  3433. // A cmp function for determining which segments should take visual priority
  3434. // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
  3435. function compareSegs(seg1, seg2) {
  3436. return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  3437. seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  3438. seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  3439. (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
  3440. }
  3441. fc.compareSegs = compareSegs; // export
  3442. /* External-Dragging-Element Data
  3443. ----------------------------------------------------------------------------------------------------------------------*/
  3444. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  3445. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  3446. fc.dataAttrPrefix = '';
  3447. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  3448. // to be used for Event Object creation.
  3449. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  3450. function getDraggedElMeta(el) {
  3451. var prefix = fc.dataAttrPrefix;
  3452. var eventProps; // properties for creating the event, not related to date/time
  3453. var startTime; // a Duration
  3454. var duration;
  3455. var stick;
  3456. if (prefix) { prefix += '-'; }
  3457. eventProps = el.data(prefix + 'event') || null;
  3458. if (eventProps) {
  3459. if (typeof eventProps === 'object') {
  3460. eventProps = $.extend({}, eventProps); // make a copy
  3461. }
  3462. else { // something like 1 or true. still signal event creation
  3463. eventProps = {};
  3464. }
  3465. // pluck special-cased date/time properties
  3466. startTime = eventProps.start;
  3467. if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  3468. duration = eventProps.duration;
  3469. stick = eventProps.stick;
  3470. delete eventProps.start;
  3471. delete eventProps.time;
  3472. delete eventProps.duration;
  3473. delete eventProps.stick;
  3474. }
  3475. // fallback to standalone attribute values for each of the date/time properties
  3476. if (startTime == null) { startTime = el.data(prefix + 'start'); }
  3477. if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  3478. if (duration == null) { duration = el.data(prefix + 'duration'); }
  3479. if (stick == null) { stick = el.data(prefix + 'stick'); }
  3480. // massage into correct data types
  3481. startTime = startTime != null ? moment.duration(startTime) : null;
  3482. duration = duration != null ? moment.duration(duration) : null;
  3483. stick = Boolean(stick);
  3484. return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  3485. }
  3486. ;;
  3487. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  3488. ----------------------------------------------------------------------------------------------------------------------*/
  3489. var DayGrid = Grid.extend({
  3490. numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  3491. bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  3492. breakOnWeeks: null, // should create a new row for each week? set by outside view
  3493. cellDates: null, // flat chronological array of each cell's dates
  3494. dayToCellOffsets: null, // maps days offsets from grid's start date, to cell offsets
  3495. rowEls: null, // set of fake row elements
  3496. dayEls: null, // set of whole-day elements comprising the row's background
  3497. helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  3498. constructor: function() {
  3499. Grid.apply(this, arguments);
  3500. this.cellDuration = moment.duration(1, 'day'); // for Grid system
  3501. },
  3502. // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  3503. // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  3504. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  3505. renderDates: function(isRigid) {
  3506. var view = this.view;
  3507. var rowCnt = this.rowCnt;
  3508. var colCnt = this.colCnt;
  3509. var cellCnt = rowCnt * colCnt;
  3510. var html = '';
  3511. var row;
  3512. var i, cell;
  3513. for (row = 0; row < rowCnt; row++) {
  3514. html += this.dayRowHtml(row, isRigid);
  3515. }
  3516. this.el.html(html);
  3517. this.rowEls = this.el.find('.fc-row');
  3518. this.dayEls = this.el.find('.fc-day');
  3519. // trigger dayRender with each cell's element
  3520. for (i = 0; i < cellCnt; i++) {
  3521. cell = this.getCell(i);
  3522. view.trigger('dayRender', null, cell.start, this.dayEls.eq(i));
  3523. }
  3524. },
  3525. unrenderDates: function() {
  3526. this.removeSegPopover();
  3527. },
  3528. renderBusinessHours: function() {
  3529. var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
  3530. var segs = this.eventsToSegs(events);
  3531. this.renderFill('businessHours', segs, 'bgevent');
  3532. },
  3533. // Generates the HTML for a single row. `row` is the row number.
  3534. dayRowHtml: function(row, isRigid) {
  3535. var view = this.view;
  3536. var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  3537. if (isRigid) {
  3538. classes.push('fc-rigid');
  3539. }
  3540. return '' +
  3541. '<div class="' + classes.join(' ') + '">' +
  3542. '<div class="fc-bg">' +
  3543. '<table>' +
  3544. this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()
  3545. '</table>' +
  3546. '</div>' +
  3547. '<div class="fc-content-skeleton">' +
  3548. '<table>' +
  3549. (this.numbersVisible ?
  3550. '<thead>' +
  3551. this.rowHtml('number', row) + // leverages RowRenderer. View will define render method
  3552. '</thead>' :
  3553. ''
  3554. ) +
  3555. '</table>' +
  3556. '</div>' +
  3557. '</div>';
  3558. },
  3559. // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
  3560. // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
  3561. // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
  3562. dayCellHtml: function(cell) {
  3563. return this.bgCellHtml(cell);
  3564. },
  3565. /* Options
  3566. ------------------------------------------------------------------------------------------------------------------*/
  3567. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  3568. computeColHeadFormat: function() {
  3569. if (this.rowCnt > 1) { // more than one week row. day numbers will be in each cell
  3570. return 'ddd'; // "Sat"
  3571. }
  3572. else if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  3573. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  3574. }
  3575. else { // single day, so full single date string will probably be in title text
  3576. return 'dddd'; // "Saturday"
  3577. }
  3578. },
  3579. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  3580. computeEventTimeFormat: function() {
  3581. return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
  3582. },
  3583. // Computes a default `displayEventEnd` value if one is not expliclty defined
  3584. computeDisplayEventEnd: function() {
  3585. return this.colCnt == 1; // we'll likely have space if there's only one day
  3586. },
  3587. /* Cell System
  3588. ------------------------------------------------------------------------------------------------------------------*/
  3589. rangeUpdated: function() {
  3590. var cellDates;
  3591. var firstDay;
  3592. var rowCnt;
  3593. var colCnt;
  3594. this.updateCellDates(); // populates cellDates and dayToCellOffsets
  3595. cellDates = this.cellDates;
  3596. if (this.breakOnWeeks) {
  3597. // count columns until the day-of-week repeats
  3598. firstDay = cellDates[0].day();
  3599. for (colCnt = 1; colCnt < cellDates.length; colCnt++) {
  3600. if (cellDates[colCnt].day() == firstDay) {
  3601. break;
  3602. }
  3603. }
  3604. rowCnt = Math.ceil(cellDates.length / colCnt);
  3605. }
  3606. else {
  3607. rowCnt = 1;
  3608. colCnt = cellDates.length;
  3609. }
  3610. this.rowCnt = rowCnt;
  3611. this.colCnt = colCnt;
  3612. },
  3613. // Populates cellDates and dayToCellOffsets
  3614. updateCellDates: function() {
  3615. var view = this.view;
  3616. var date = this.start.clone();
  3617. var dates = [];
  3618. var offset = -1;
  3619. var offsets = [];
  3620. while (date.isBefore(this.end)) { // loop each day from start to end
  3621. if (view.isHiddenDay(date)) {
  3622. offsets.push(offset + 0.5); // mark that it's between offsets
  3623. }
  3624. else {
  3625. offset++;
  3626. offsets.push(offset);
  3627. dates.push(date.clone());
  3628. }
  3629. date.add(1, 'days');
  3630. }
  3631. this.cellDates = dates;
  3632. this.dayToCellOffsets = offsets;
  3633. },
  3634. // Given a cell object, generates its start date. Returns a reference-free copy.
  3635. computeCellDate: function(cell) {
  3636. var colCnt = this.colCnt;
  3637. var index = cell.row * colCnt + (this.isRTL ? colCnt - cell.col - 1 : cell.col);
  3638. return this.cellDates[index].clone();
  3639. },
  3640. // Retrieves the element representing the given row
  3641. getRowEl: function(row) {
  3642. return this.rowEls.eq(row);
  3643. },
  3644. // Retrieves the element representing the given column
  3645. getColEl: function(col) {
  3646. return this.dayEls.eq(col);
  3647. },
  3648. // Gets the whole-day element associated with the cell
  3649. getCellDayEl: function(cell) {
  3650. return this.dayEls.eq(cell.row * this.colCnt + cell.col);
  3651. },
  3652. // Overrides Grid's method for when row coordinates are computed
  3653. computeRowCoords: function() {
  3654. var rowCoords = Grid.prototype.computeRowCoords.call(this); // call the super-method
  3655. // hack for extending last row (used by AgendaView)
  3656. rowCoords[rowCoords.length - 1].bottom += this.bottomCoordPadding;
  3657. return rowCoords;
  3658. },
  3659. /* Dates
  3660. ------------------------------------------------------------------------------------------------------------------*/
  3661. // Slices up a date range by row into an array of segments
  3662. rangeToSegs: function(range) {
  3663. var isRTL = this.isRTL;
  3664. var rowCnt = this.rowCnt;
  3665. var colCnt = this.colCnt;
  3666. var segs = [];
  3667. var first, last; // inclusive cell-offset range for given range
  3668. var row;
  3669. var rowFirst, rowLast; // inclusive cell-offset range for current row
  3670. var isStart, isEnd;
  3671. var segFirst, segLast; // inclusive cell-offset range for segment
  3672. var seg;
  3673. range = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  3674. first = this.dateToCellOffset(range.start);
  3675. last = this.dateToCellOffset(range.end.subtract(1, 'days')); // offset of inclusive end date
  3676. for (row = 0; row < rowCnt; row++) {
  3677. rowFirst = row * colCnt;
  3678. rowLast = rowFirst + colCnt - 1;
  3679. // intersect segment's offset range with the row's
  3680. segFirst = Math.max(rowFirst, first);
  3681. segLast = Math.min(rowLast, last);
  3682. // deal with in-between indices
  3683. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  3684. segLast = Math.floor(segLast); // in-between ends round to prev cell
  3685. if (segFirst <= segLast) { // was there any intersection with the current row?
  3686. // must be matching integers to be the segment's start/end
  3687. isStart = segFirst === first;
  3688. isEnd = segLast === last;
  3689. // translate offsets to be relative to start-of-row
  3690. segFirst -= rowFirst;
  3691. segLast -= rowFirst;
  3692. seg = { row: row, isStart: isStart, isEnd: isEnd };
  3693. if (isRTL) {
  3694. seg.leftCol = colCnt - segLast - 1;
  3695. seg.rightCol = colCnt - segFirst - 1;
  3696. }
  3697. else {
  3698. seg.leftCol = segFirst;
  3699. seg.rightCol = segLast;
  3700. }
  3701. segs.push(seg);
  3702. }
  3703. }
  3704. return segs;
  3705. },
  3706. // Given a date, returns its chronolocial cell-offset from the first cell of the grid.
  3707. // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
  3708. // If before the first offset, returns a negative number.
  3709. // If after the last offset, returns an offset past the last cell offset.
  3710. // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
  3711. dateToCellOffset: function(date) {
  3712. var offsets = this.dayToCellOffsets;
  3713. var day = date.diff(this.start, 'days');
  3714. if (day < 0) {
  3715. return offsets[0] - 1;
  3716. }
  3717. else if (day >= offsets.length) {
  3718. return offsets[offsets.length - 1] + 1;
  3719. }
  3720. else {
  3721. return offsets[day];
  3722. }
  3723. },
  3724. /* Event Drag Visualization
  3725. ------------------------------------------------------------------------------------------------------------------*/
  3726. // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
  3727. // Renders a visual indication of an event or external element being dragged.
  3728. // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info.
  3729. renderDrag: function(dropLocation, seg) {
  3730. // always render a highlight underneath
  3731. this.renderHighlight(this.eventRangeToSegs(dropLocation));
  3732. // if a segment from the same calendar but another component is being dragged, render a helper event
  3733. if (seg && !seg.el.closest(this.el).length) {
  3734. this.renderRangeHelper(dropLocation, seg);
  3735. this.applyDragOpacity(this.helperEls);
  3736. return true; // a helper has been rendered
  3737. }
  3738. },
  3739. // Unrenders any visual indication of a hovering event
  3740. unrenderDrag: function() {
  3741. this.unrenderHighlight();
  3742. this.unrenderHelper();
  3743. },
  3744. /* Event Resize Visualization
  3745. ------------------------------------------------------------------------------------------------------------------*/
  3746. // Renders a visual indication of an event being resized
  3747. renderEventResize: function(range, seg) {
  3748. this.renderHighlight(this.eventRangeToSegs(range));
  3749. this.renderRangeHelper(range, seg);
  3750. },
  3751. // Unrenders a visual indication of an event being resized
  3752. unrenderEventResize: function() {
  3753. this.unrenderHighlight();
  3754. this.unrenderHelper();
  3755. },
  3756. /* Event Helper
  3757. ------------------------------------------------------------------------------------------------------------------*/
  3758. // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  3759. renderHelper: function(event, sourceSeg) {
  3760. var helperNodes = [];
  3761. var segs = this.eventsToSegs([ event ]);
  3762. var rowStructs;
  3763. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  3764. rowStructs = this.renderSegRows(segs);
  3765. // inject each new event skeleton into each associated row
  3766. this.rowEls.each(function(row, rowNode) {
  3767. var rowEl = $(rowNode); // the .fc-row
  3768. var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  3769. var skeletonTop;
  3770. // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  3771. if (sourceSeg && sourceSeg.row === row) {
  3772. skeletonTop = sourceSeg.el.position().top;
  3773. }
  3774. else {
  3775. skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  3776. }
  3777. skeletonEl.css('top', skeletonTop)
  3778. .find('table')
  3779. .append(rowStructs[row].tbodyEl);
  3780. rowEl.append(skeletonEl);
  3781. helperNodes.push(skeletonEl[0]);
  3782. });
  3783. this.helperEls = $(helperNodes); // array -> jQuery set
  3784. },
  3785. // Unrenders any visual indication of a mock helper event
  3786. unrenderHelper: function() {
  3787. if (this.helperEls) {
  3788. this.helperEls.remove();
  3789. this.helperEls = null;
  3790. }
  3791. },
  3792. /* Fill System (highlight, background events, business hours)
  3793. ------------------------------------------------------------------------------------------------------------------*/
  3794. fillSegTag: 'td', // override the default tag name
  3795. // Renders a set of rectangles over the given segments of days.
  3796. // Only returns segments that successfully rendered.
  3797. renderFill: function(type, segs, className) {
  3798. var nodes = [];
  3799. var i, seg;
  3800. var skeletonEl;
  3801. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  3802. for (i = 0; i < segs.length; i++) {
  3803. seg = segs[i];
  3804. skeletonEl = this.renderFillRow(type, seg, className);
  3805. this.rowEls.eq(seg.row).append(skeletonEl);
  3806. nodes.push(skeletonEl[0]);
  3807. }
  3808. this.elsByFill[type] = $(nodes);
  3809. return segs;
  3810. },
  3811. // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  3812. renderFillRow: function(type, seg, className) {
  3813. var colCnt = this.colCnt;
  3814. var startCol = seg.leftCol;
  3815. var endCol = seg.rightCol + 1;
  3816. var skeletonEl;
  3817. var trEl;
  3818. className = className || type.toLowerCase();
  3819. skeletonEl = $(
  3820. '<div class="fc-' + className + '-skeleton">' +
  3821. '<table><tr/></table>' +
  3822. '</div>'
  3823. );
  3824. trEl = skeletonEl.find('tr');
  3825. if (startCol > 0) {
  3826. trEl.append('<td colspan="' + startCol + '"/>');
  3827. }
  3828. trEl.append(
  3829. seg.el.attr('colspan', endCol - startCol)
  3830. );
  3831. if (endCol < colCnt) {
  3832. trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  3833. }
  3834. this.bookendCells(trEl, type);
  3835. return skeletonEl;
  3836. }
  3837. });
  3838. ;;
  3839. /* Event-rendering methods for the DayGrid class
  3840. ----------------------------------------------------------------------------------------------------------------------*/
  3841. DayGrid.mixin({
  3842. rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  3843. // Unrenders all events currently rendered on the grid
  3844. unrenderEvents: function() {
  3845. this.removeSegPopover(); // removes the "more.." events popover
  3846. Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
  3847. },
  3848. // Retrieves all rendered segment objects currently rendered on the grid
  3849. getEventSegs: function() {
  3850. return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
  3851. .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  3852. },
  3853. // Renders the given background event segments onto the grid
  3854. renderBgSegs: function(segs) {
  3855. // don't render timed background events
  3856. var allDaySegs = $.grep(segs, function(seg) {
  3857. return seg.event.allDay;
  3858. });
  3859. return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  3860. },
  3861. // Renders the given foreground event segments onto the grid
  3862. renderFgSegs: function(segs) {
  3863. var rowStructs;
  3864. // render an `.el` on each seg
  3865. // returns a subset of the segs. segs that were actually rendered
  3866. segs = this.renderFgSegEls(segs);
  3867. rowStructs = this.rowStructs = this.renderSegRows(segs);
  3868. // append to each row's content skeleton
  3869. this.rowEls.each(function(i, rowNode) {
  3870. $(rowNode).find('.fc-content-skeleton > table').append(
  3871. rowStructs[i].tbodyEl
  3872. );
  3873. });
  3874. return segs; // return only the segs that were actually rendered
  3875. },
  3876. // Unrenders all currently rendered foreground event segments
  3877. unrenderFgSegs: function() {
  3878. var rowStructs = this.rowStructs || [];
  3879. var rowStruct;
  3880. while ((rowStruct = rowStructs.pop())) {
  3881. rowStruct.tbodyEl.remove();
  3882. }
  3883. this.rowStructs = null;
  3884. },
  3885. // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  3886. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  3887. // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  3888. renderSegRows: function(segs) {
  3889. var rowStructs = [];
  3890. var segRows;
  3891. var row;
  3892. segRows = this.groupSegRows(segs); // group into nested arrays
  3893. // iterate each row of segment groupings
  3894. for (row = 0; row < segRows.length; row++) {
  3895. rowStructs.push(
  3896. this.renderSegRow(row, segRows[row])
  3897. );
  3898. }
  3899. return rowStructs;
  3900. },
  3901. // Builds the HTML to be used for the default element for an individual segment
  3902. fgSegHtml: function(seg, disableResizing) {
  3903. var view = this.view;
  3904. var event = seg.event;
  3905. var isDraggable = view.isEventDraggable(event);
  3906. var isResizableFromStart = !disableResizing && event.allDay &&
  3907. seg.isStart && view.isEventResizableFromStart(event);
  3908. var isResizableFromEnd = !disableResizing && event.allDay &&
  3909. seg.isEnd && view.isEventResizableFromEnd(event);
  3910. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  3911. var skinCss = cssToStr(this.getEventSkinCss(event));
  3912. var timeHtml = '';
  3913. var timeText;
  3914. var titleHtml;
  3915. classes.unshift('fc-day-grid-event', 'fc-h-event');
  3916. // Only display a timed events time if it is the starting segment
  3917. if (seg.isStart) {
  3918. timeText = this.getEventTimeText(event);
  3919. if (timeText) {
  3920. timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
  3921. }
  3922. }
  3923. titleHtml =
  3924. '<span class="fc-title">' +
  3925. (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  3926. '</span>';
  3927. return '<a class="' + classes.join(' ') + '"' +
  3928. (event.url ?
  3929. ' href="' + htmlEscape(event.url) + '"' :
  3930. ''
  3931. ) +
  3932. (skinCss ?
  3933. ' style="' + skinCss + '"' :
  3934. ''
  3935. ) +
  3936. '>' +
  3937. '<div class="fc-content">' +
  3938. (this.isRTL ?
  3939. titleHtml + ' ' + timeHtml : // put a natural space in between
  3940. timeHtml + ' ' + titleHtml //
  3941. ) +
  3942. '</div>' +
  3943. (isResizableFromStart ?
  3944. '<div class="fc-resizer fc-start-resizer" />' :
  3945. ''
  3946. ) +
  3947. (isResizableFromEnd ?
  3948. '<div class="fc-resizer fc-end-resizer" />' :
  3949. ''
  3950. ) +
  3951. '</a>';
  3952. },
  3953. // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  3954. // the segments. Returns object with a bunch of internal data about how the render was calculated.
  3955. // NOTE: modifies rowSegs
  3956. renderSegRow: function(row, rowSegs) {
  3957. var colCnt = this.colCnt;
  3958. var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  3959. var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  3960. var tbody = $('<tbody/>');
  3961. var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  3962. var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  3963. var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  3964. var i, levelSegs;
  3965. var col;
  3966. var tr;
  3967. var j, seg;
  3968. var td;
  3969. // populates empty cells from the current column (`col`) to `endCol`
  3970. function emptyCellsUntil(endCol) {
  3971. while (col < endCol) {
  3972. // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  3973. td = (loneCellMatrix[i - 1] || [])[col];
  3974. if (td) {
  3975. td.attr(
  3976. 'rowspan',
  3977. parseInt(td.attr('rowspan') || 1, 10) + 1
  3978. );
  3979. }
  3980. else {
  3981. td = $('<td/>');
  3982. tr.append(td);
  3983. }
  3984. cellMatrix[i][col] = td;
  3985. loneCellMatrix[i][col] = td;
  3986. col++;
  3987. }
  3988. }
  3989. for (i = 0; i < levelCnt; i++) { // iterate through all levels
  3990. levelSegs = segLevels[i];
  3991. col = 0;
  3992. tr = $('<tr/>');
  3993. segMatrix.push([]);
  3994. cellMatrix.push([]);
  3995. loneCellMatrix.push([]);
  3996. // levelCnt might be 1 even though there are no actual levels. protect against this.
  3997. // this single empty row is useful for styling.
  3998. if (levelSegs) {
  3999. for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  4000. seg = levelSegs[j];
  4001. emptyCellsUntil(seg.leftCol);
  4002. // create a container that occupies or more columns. append the event element.
  4003. td = $('<td class="fc-event-container"/>').append(seg.el);
  4004. if (seg.leftCol != seg.rightCol) {
  4005. td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  4006. }
  4007. else { // a single-column segment
  4008. loneCellMatrix[i][col] = td;
  4009. }
  4010. while (col <= seg.rightCol) {
  4011. cellMatrix[i][col] = td;
  4012. segMatrix[i][col] = seg;
  4013. col++;
  4014. }
  4015. tr.append(td);
  4016. }
  4017. }
  4018. emptyCellsUntil(colCnt); // finish off the row
  4019. this.bookendCells(tr, 'eventSkeleton');
  4020. tbody.append(tr);
  4021. }
  4022. return { // a "rowStruct"
  4023. row: row, // the row number
  4024. tbodyEl: tbody,
  4025. cellMatrix: cellMatrix,
  4026. segMatrix: segMatrix,
  4027. segLevels: segLevels,
  4028. segs: rowSegs
  4029. };
  4030. },
  4031. // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  4032. // NOTE: modifies segs
  4033. buildSegLevels: function(segs) {
  4034. var levels = [];
  4035. var i, seg;
  4036. var j;
  4037. // Give preference to elements with certain criteria, so they have
  4038. // a chance to be closer to the top.
  4039. segs.sort(compareSegs);
  4040. for (i = 0; i < segs.length; i++) {
  4041. seg = segs[i];
  4042. // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  4043. for (j = 0; j < levels.length; j++) {
  4044. if (!isDaySegCollision(seg, levels[j])) {
  4045. break;
  4046. }
  4047. }
  4048. // `j` now holds the desired subrow index
  4049. seg.level = j;
  4050. // create new level array if needed and append segment
  4051. (levels[j] || (levels[j] = [])).push(seg);
  4052. }
  4053. // order segments left-to-right. very important if calendar is RTL
  4054. for (j = 0; j < levels.length; j++) {
  4055. levels[j].sort(compareDaySegCols);
  4056. }
  4057. return levels;
  4058. },
  4059. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  4060. groupSegRows: function(segs) {
  4061. var segRows = [];
  4062. var i;
  4063. for (i = 0; i < this.rowCnt; i++) {
  4064. segRows.push([]);
  4065. }
  4066. for (i = 0; i < segs.length; i++) {
  4067. segRows[segs[i].row].push(segs[i]);
  4068. }
  4069. return segRows;
  4070. }
  4071. });
  4072. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  4073. function isDaySegCollision(seg, otherSegs) {
  4074. var i, otherSeg;
  4075. for (i = 0; i < otherSegs.length; i++) {
  4076. otherSeg = otherSegs[i];
  4077. if (
  4078. otherSeg.leftCol <= seg.rightCol &&
  4079. otherSeg.rightCol >= seg.leftCol
  4080. ) {
  4081. return true;
  4082. }
  4083. }
  4084. return false;
  4085. }
  4086. // A cmp function for determining the leftmost event
  4087. function compareDaySegCols(a, b) {
  4088. return a.leftCol - b.leftCol;
  4089. }
  4090. ;;
  4091. /* Methods relate to limiting the number events for a given day on a DayGrid
  4092. ----------------------------------------------------------------------------------------------------------------------*/
  4093. // NOTE: all the segs being passed around in here are foreground segs
  4094. DayGrid.mixin({
  4095. segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  4096. popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  4097. removeSegPopover: function() {
  4098. if (this.segPopover) {
  4099. this.segPopover.hide(); // in handler, will call segPopover's removeElement
  4100. }
  4101. },
  4102. // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  4103. // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  4104. limitRows: function(levelLimit) {
  4105. var rowStructs = this.rowStructs || [];
  4106. var row; // row #
  4107. var rowLevelLimit;
  4108. for (row = 0; row < rowStructs.length; row++) {
  4109. this.unlimitRow(row);
  4110. if (!levelLimit) {
  4111. rowLevelLimit = false;
  4112. }
  4113. else if (typeof levelLimit === 'number') {
  4114. rowLevelLimit = levelLimit;
  4115. }
  4116. else {
  4117. rowLevelLimit = this.computeRowLevelLimit(row);
  4118. }
  4119. if (rowLevelLimit !== false) {
  4120. this.limitRow(row, rowLevelLimit);
  4121. }
  4122. }
  4123. },
  4124. // Computes the number of levels a row will accomodate without going outside its bounds.
  4125. // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  4126. // `row` is the row number.
  4127. computeRowLevelLimit: function(row) {
  4128. var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  4129. var rowHeight = rowEl.height(); // TODO: cache somehow?
  4130. var trEls = this.rowStructs[row].tbodyEl.children();
  4131. var i, trEl;
  4132. var trHeight;
  4133. function iterInnerHeights(i, childNode) {
  4134. trHeight = Math.max(trHeight, $(childNode).outerHeight());
  4135. }
  4136. // Reveal one level <tr> at a time and stop when we find one out of bounds
  4137. for (i = 0; i < trEls.length; i++) {
  4138. trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
  4139. // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
  4140. // so instead, find the tallest inner content element.
  4141. trHeight = 0;
  4142. trEl.find('> td > :first-child').each(iterInnerHeights);
  4143. if (trEl.position().top + trHeight > rowHeight) {
  4144. return i;
  4145. }
  4146. }
  4147. return false; // should not limit at all
  4148. },
  4149. // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  4150. // `row` is the row number.
  4151. // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  4152. limitRow: function(row, levelLimit) {
  4153. var _this = this;
  4154. var rowStruct = this.rowStructs[row];
  4155. var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  4156. var col = 0; // col #, left-to-right (not chronologically)
  4157. var cell;
  4158. var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  4159. var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  4160. var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  4161. var i, seg;
  4162. var segsBelow; // array of segment objects below `seg` in the current `col`
  4163. var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  4164. var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  4165. var td, rowspan;
  4166. var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  4167. var j;
  4168. var moreTd, moreWrap, moreLink;
  4169. // Iterates through empty level cells and places "more" links inside if need be
  4170. function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  4171. while (col < endCol) {
  4172. cell = _this.getCell(row, col);
  4173. segsBelow = _this.getCellSegs(cell, levelLimit);
  4174. if (segsBelow.length) {
  4175. td = cellMatrix[levelLimit - 1][col];
  4176. moreLink = _this.renderMoreLink(cell, segsBelow);
  4177. moreWrap = $('<div/>').append(moreLink);
  4178. td.append(moreWrap);
  4179. moreNodes.push(moreWrap[0]);
  4180. }
  4181. col++;
  4182. }
  4183. }
  4184. if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  4185. levelSegs = rowStruct.segLevels[levelLimit - 1];
  4186. cellMatrix = rowStruct.cellMatrix;
  4187. limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  4188. .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  4189. // iterate though segments in the last allowable level
  4190. for (i = 0; i < levelSegs.length; i++) {
  4191. seg = levelSegs[i];
  4192. emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  4193. // determine *all* segments below `seg` that occupy the same columns
  4194. colSegsBelow = [];
  4195. totalSegsBelow = 0;
  4196. while (col <= seg.rightCol) {
  4197. cell = this.getCell(row, col);
  4198. segsBelow = this.getCellSegs(cell, levelLimit);
  4199. colSegsBelow.push(segsBelow);
  4200. totalSegsBelow += segsBelow.length;
  4201. col++;
  4202. }
  4203. if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  4204. td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  4205. rowspan = td.attr('rowspan') || 1;
  4206. segMoreNodes = [];
  4207. // make a replacement <td> for each column the segment occupies. will be one for each colspan
  4208. for (j = 0; j < colSegsBelow.length; j++) {
  4209. moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  4210. segsBelow = colSegsBelow[j];
  4211. cell = this.getCell(row, seg.leftCol + j);
  4212. moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
  4213. moreWrap = $('<div/>').append(moreLink);
  4214. moreTd.append(moreWrap);
  4215. segMoreNodes.push(moreTd[0]);
  4216. moreNodes.push(moreTd[0]);
  4217. }
  4218. td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  4219. limitedNodes.push(td[0]);
  4220. }
  4221. }
  4222. emptyCellsUntil(this.colCnt); // finish off the level
  4223. rowStruct.moreEls = $(moreNodes); // for easy undoing later
  4224. rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  4225. }
  4226. },
  4227. // Reveals all levels and removes all "more"-related elements for a grid's row.
  4228. // `row` is a row number.
  4229. unlimitRow: function(row) {
  4230. var rowStruct = this.rowStructs[row];
  4231. if (rowStruct.moreEls) {
  4232. rowStruct.moreEls.remove();
  4233. rowStruct.moreEls = null;
  4234. }
  4235. if (rowStruct.limitedEls) {
  4236. rowStruct.limitedEls.removeClass('fc-limited');
  4237. rowStruct.limitedEls = null;
  4238. }
  4239. },
  4240. // Renders an <a> element that represents hidden event element for a cell.
  4241. // Responsible for attaching click handler as well.
  4242. renderMoreLink: function(cell, hiddenSegs) {
  4243. var _this = this;
  4244. var view = this.view;
  4245. return $('<a class="fc-more"/>')
  4246. .text(
  4247. this.getMoreLinkText(hiddenSegs.length)
  4248. )
  4249. .on('click', function(ev) {
  4250. var clickOption = view.opt('eventLimitClick');
  4251. var date = cell.start;
  4252. var moreEl = $(this);
  4253. var dayEl = _this.getCellDayEl(cell);
  4254. var allSegs = _this.getCellSegs(cell);
  4255. // rescope the segments to be within the cell's date
  4256. var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  4257. var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  4258. if (typeof clickOption === 'function') {
  4259. // the returned value can be an atomic option
  4260. clickOption = view.trigger('eventLimitClick', null, {
  4261. date: date,
  4262. dayEl: dayEl,
  4263. moreEl: moreEl,
  4264. segs: reslicedAllSegs,
  4265. hiddenSegs: reslicedHiddenSegs
  4266. }, ev);
  4267. }
  4268. if (clickOption === 'popover') {
  4269. _this.showSegPopover(cell, moreEl, reslicedAllSegs);
  4270. }
  4271. else if (typeof clickOption === 'string') { // a view name
  4272. view.calendar.zoomTo(date, clickOption);
  4273. }
  4274. });
  4275. },
  4276. // Reveals the popover that displays all events within a cell
  4277. showSegPopover: function(cell, moreLink, segs) {
  4278. var _this = this;
  4279. var view = this.view;
  4280. var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  4281. var topEl; // the element we want to match the top coordinate of
  4282. var options;
  4283. if (this.rowCnt == 1) {
  4284. topEl = view.el; // will cause the popover to cover any sort of header
  4285. }
  4286. else {
  4287. topEl = this.rowEls.eq(cell.row); // will align with top of row
  4288. }
  4289. options = {
  4290. className: 'fc-more-popover',
  4291. content: this.renderSegPopoverContent(cell, segs),
  4292. parentEl: this.el,
  4293. top: topEl.offset().top,
  4294. autoHide: true, // when the user clicks elsewhere, hide the popover
  4295. viewportConstrain: view.opt('popoverViewportConstrain'),
  4296. hide: function() {
  4297. // kill everything when the popover is hidden
  4298. _this.segPopover.removeElement();
  4299. _this.segPopover = null;
  4300. _this.popoverSegs = null;
  4301. }
  4302. };
  4303. // Determine horizontal coordinate.
  4304. // We use the moreWrap instead of the <td> to avoid border confusion.
  4305. if (this.isRTL) {
  4306. options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  4307. }
  4308. else {
  4309. options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  4310. }
  4311. this.segPopover = new Popover(options);
  4312. this.segPopover.show();
  4313. },
  4314. // Builds the inner DOM contents of the segment popover
  4315. renderSegPopoverContent: function(cell, segs) {
  4316. var view = this.view;
  4317. var isTheme = view.opt('theme');
  4318. var title = cell.start.format(view.opt('dayPopoverFormat'));
  4319. var content = $(
  4320. '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  4321. '<span class="fc-close ' +
  4322. (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  4323. '"></span>' +
  4324. '<span class="fc-title">' +
  4325. htmlEscape(title) +
  4326. '</span>' +
  4327. '<div class="fc-clear"/>' +
  4328. '</div>' +
  4329. '<div class="fc-body ' + view.widgetContentClass + '">' +
  4330. '<div class="fc-event-container"></div>' +
  4331. '</div>'
  4332. );
  4333. var segContainer = content.find('.fc-event-container');
  4334. var i;
  4335. // render each seg's `el` and only return the visible segs
  4336. segs = this.renderFgSegEls(segs, true); // disableResizing=true
  4337. this.popoverSegs = segs;
  4338. for (i = 0; i < segs.length; i++) {
  4339. // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  4340. // grids that want to do drag-n-drop about which cell it came from
  4341. segs[i].cell = cell;
  4342. segContainer.append(segs[i].el);
  4343. }
  4344. return content;
  4345. },
  4346. // Given the events within an array of segment objects, reslice them to be in a single day
  4347. resliceDaySegs: function(segs, dayDate) {
  4348. // build an array of the original events
  4349. var events = $.map(segs, function(seg) {
  4350. return seg.event;
  4351. });
  4352. var dayStart = dayDate.clone().stripTime();
  4353. var dayEnd = dayStart.clone().add(1, 'days');
  4354. var dayRange = { start: dayStart, end: dayEnd };
  4355. // slice the events with a custom slicing function
  4356. segs = this.eventsToSegs(
  4357. events,
  4358. function(range) {
  4359. var seg = intersectionToSeg(range, dayRange); // undefind if no intersection
  4360. return seg ? [ seg ] : []; // must return an array of segments
  4361. }
  4362. );
  4363. // force an order because eventsToSegs doesn't guarantee one
  4364. segs.sort(compareSegs);
  4365. return segs;
  4366. },
  4367. // Generates the text that should be inside a "more" link, given the number of events it represents
  4368. getMoreLinkText: function(num) {
  4369. var opt = this.view.opt('eventLimitText');
  4370. if (typeof opt === 'function') {
  4371. return opt(num);
  4372. }
  4373. else {
  4374. return '+' + num + ' ' + opt;
  4375. }
  4376. },
  4377. // Returns segments within a given cell.
  4378. // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  4379. getCellSegs: function(cell, startLevel) {
  4380. var segMatrix = this.rowStructs[cell.row].segMatrix;
  4381. var level = startLevel || 0;
  4382. var segs = [];
  4383. var seg;
  4384. while (level < segMatrix.length) {
  4385. seg = segMatrix[level][cell.col];
  4386. if (seg) {
  4387. segs.push(seg);
  4388. }
  4389. level++;
  4390. }
  4391. return segs;
  4392. }
  4393. });
  4394. ;;
  4395. /* A component that renders one or more columns of vertical time slots
  4396. ----------------------------------------------------------------------------------------------------------------------*/
  4397. var TimeGrid = Grid.extend({
  4398. slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  4399. snapDuration: null, // granularity of time for dragging and selecting
  4400. minTime: null, // Duration object that denotes the first visible time of any given day
  4401. maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  4402. colDates: null, // whole-day dates for each column. left to right
  4403. axisFormat: null, // formatting string for times running along vertical axis
  4404. dayEls: null, // cells elements in the day-row background
  4405. slatEls: null, // elements running horizontally across all columns
  4406. slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
  4407. helperEl: null, // cell skeleton element for rendering the mock event "helper"
  4408. businessHourSegs: null,
  4409. constructor: function() {
  4410. Grid.apply(this, arguments); // call the super-constructor
  4411. this.processOptions();
  4412. },
  4413. // Renders the time grid into `this.el`, which should already be assigned.
  4414. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  4415. renderDates: function() {
  4416. this.el.html(this.renderHtml());
  4417. this.dayEls = this.el.find('.fc-day');
  4418. this.slatEls = this.el.find('.fc-slats tr');
  4419. },
  4420. renderBusinessHours: function() {
  4421. var events = this.view.calendar.getBusinessHoursEvents();
  4422. this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
  4423. },
  4424. // Renders the basic HTML skeleton for the grid
  4425. renderHtml: function() {
  4426. return '' +
  4427. '<div class="fc-bg">' +
  4428. '<table>' +
  4429. this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
  4430. '</table>' +
  4431. '</div>' +
  4432. '<div class="fc-slats">' +
  4433. '<table>' +
  4434. this.slatRowHtml() +
  4435. '</table>' +
  4436. '</div>';
  4437. },
  4438. // Renders the HTML for a vertical background cell behind the slots.
  4439. // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
  4440. slotBgCellHtml: function(cell) {
  4441. return this.bgCellHtml(cell);
  4442. },
  4443. // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  4444. slatRowHtml: function() {
  4445. var view = this.view;
  4446. var isRTL = this.isRTL;
  4447. var html = '';
  4448. var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
  4449. var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  4450. var slotDate; // will be on the view's first day, but we only care about its time
  4451. var minutes;
  4452. var axisHtml;
  4453. // Calculate the time for each slot
  4454. while (slotTime < this.maxTime) {
  4455. slotDate = this.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
  4456. minutes = slotDate.minutes();
  4457. axisHtml =
  4458. '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  4459. ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
  4460. '<span>' + // for matchCellWidths
  4461. htmlEscape(slotDate.format(this.axisFormat)) +
  4462. '</span>' :
  4463. ''
  4464. ) +
  4465. '</td>';
  4466. html +=
  4467. '<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
  4468. (!isRTL ? axisHtml : '') +
  4469. '<td class="' + view.widgetContentClass + '"/>' +
  4470. (isRTL ? axisHtml : '') +
  4471. "</tr>";
  4472. slotTime.add(this.slotDuration);
  4473. }
  4474. return html;
  4475. },
  4476. /* Options
  4477. ------------------------------------------------------------------------------------------------------------------*/
  4478. // Parses various options into properties of this object
  4479. processOptions: function() {
  4480. var view = this.view;
  4481. var slotDuration = view.opt('slotDuration');
  4482. var snapDuration = view.opt('snapDuration');
  4483. slotDuration = moment.duration(slotDuration);
  4484. snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  4485. this.slotDuration = slotDuration;
  4486. this.snapDuration = snapDuration;
  4487. this.cellDuration = snapDuration; // for Grid system
  4488. this.minTime = moment.duration(view.opt('minTime'));
  4489. this.maxTime = moment.duration(view.opt('maxTime'));
  4490. this.axisFormat = view.opt('axisFormat') || view.opt('smallTimeFormat');
  4491. },
  4492. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  4493. computeColHeadFormat: function() {
  4494. if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  4495. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  4496. }
  4497. else { // single day, so full single date string will probably be in title text
  4498. return 'dddd'; // "Saturday"
  4499. }
  4500. },
  4501. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  4502. computeEventTimeFormat: function() {
  4503. return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
  4504. },
  4505. // Computes a default `displayEventEnd` value if one is not expliclty defined
  4506. computeDisplayEventEnd: function() {
  4507. return true;
  4508. },
  4509. /* Cell System
  4510. ------------------------------------------------------------------------------------------------------------------*/
  4511. rangeUpdated: function() {
  4512. var view = this.view;
  4513. var colDates = [];
  4514. var date;
  4515. date = this.start.clone();
  4516. while (date.isBefore(this.end)) {
  4517. colDates.push(date.clone());
  4518. date.add(1, 'day');
  4519. date = view.skipHiddenDays(date);
  4520. }
  4521. if (this.isRTL) {
  4522. colDates.reverse();
  4523. }
  4524. this.colDates = colDates;
  4525. this.colCnt = colDates.length;
  4526. this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps
  4527. },
  4528. // Given a cell object, generates its start date. Returns a reference-free copy.
  4529. computeCellDate: function(cell) {
  4530. var date = this.colDates[cell.col];
  4531. var time = this.computeSnapTime(cell.row);
  4532. date = this.view.calendar.rezoneDate(date); // give it a 00:00 time
  4533. date.time(time);
  4534. return date;
  4535. },
  4536. // Retrieves the element representing the given column
  4537. getColEl: function(col) {
  4538. return this.dayEls.eq(col);
  4539. },
  4540. /* Dates
  4541. ------------------------------------------------------------------------------------------------------------------*/
  4542. // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
  4543. computeSnapTime: function(row) {
  4544. return moment.duration(this.minTime + this.snapDuration * row);
  4545. },
  4546. // Slices up a date range by column into an array of segments
  4547. rangeToSegs: function(range) {
  4548. var colCnt = this.colCnt;
  4549. var segs = [];
  4550. var seg;
  4551. var col;
  4552. var colDate;
  4553. var colRange;
  4554. // normalize :(
  4555. range = {
  4556. start: range.start.clone().stripZone(),
  4557. end: range.end.clone().stripZone()
  4558. };
  4559. for (col = 0; col < colCnt; col++) {
  4560. colDate = this.colDates[col]; // will be ambig time/timezone
  4561. colRange = {
  4562. start: colDate.clone().time(this.minTime),
  4563. end: colDate.clone().time(this.maxTime)
  4564. };
  4565. seg = intersectionToSeg(range, colRange); // both will be ambig timezone
  4566. if (seg) {
  4567. seg.col = col;
  4568. segs.push(seg);
  4569. }
  4570. }
  4571. return segs;
  4572. },
  4573. /* Coordinates
  4574. ------------------------------------------------------------------------------------------------------------------*/
  4575. updateSize: function(isResize) { // NOT a standard Grid method
  4576. this.computeSlatTops();
  4577. if (isResize) {
  4578. this.updateSegVerticals();
  4579. }
  4580. },
  4581. // Computes the top/bottom coordinates of each "snap" rows
  4582. computeRowCoords: function() {
  4583. var originTop = this.el.offset().top;
  4584. var items = [];
  4585. var i;
  4586. var item;
  4587. for (i = 0; i < this.rowCnt; i++) {
  4588. item = {
  4589. top: originTop + this.computeTimeTop(this.computeSnapTime(i))
  4590. };
  4591. if (i > 0) {
  4592. items[i - 1].bottom = item.top;
  4593. }
  4594. items.push(item);
  4595. }
  4596. item.bottom = item.top + this.computeTimeTop(this.computeSnapTime(i));
  4597. return items;
  4598. },
  4599. // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  4600. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  4601. computeDateTop: function(date, startOfDayDate) {
  4602. return this.computeTimeTop(
  4603. moment.duration(
  4604. date.clone().stripZone() - startOfDayDate.clone().stripTime()
  4605. )
  4606. );
  4607. },
  4608. // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  4609. computeTimeTop: function(time) {
  4610. var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  4611. var slatIndex;
  4612. var slatRemainder;
  4613. var slatTop;
  4614. var slatBottom;
  4615. // constrain. because minTime/maxTime might be customized
  4616. slatCoverage = Math.max(0, slatCoverage);
  4617. slatCoverage = Math.min(this.slatEls.length, slatCoverage);
  4618. slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
  4619. slatRemainder = slatCoverage - slatIndex;
  4620. slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
  4621. if (slatRemainder) { // time spans part-way into the slot
  4622. slatBottom = this.slatTops[slatIndex + 1];
  4623. return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
  4624. }
  4625. else {
  4626. return slatTop;
  4627. }
  4628. },
  4629. // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
  4630. // Includes the the bottom of the last slat as the last item in the array.
  4631. computeSlatTops: function() {
  4632. var tops = [];
  4633. var top;
  4634. this.slatEls.each(function(i, node) {
  4635. top = $(node).position().top;
  4636. tops.push(top);
  4637. });
  4638. tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
  4639. this.slatTops = tops;
  4640. },
  4641. /* Event Drag Visualization
  4642. ------------------------------------------------------------------------------------------------------------------*/
  4643. // Renders a visual indication of an event being dragged over the specified date(s).
  4644. // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info.
  4645. // A returned value of `true` signals that a mock "helper" event has been rendered.
  4646. renderDrag: function(dropLocation, seg) {
  4647. if (seg) { // if there is event information for this drag, render a helper event
  4648. this.renderRangeHelper(dropLocation, seg);
  4649. this.applyDragOpacity(this.helperEl);
  4650. return true; // signal that a helper has been rendered
  4651. }
  4652. else {
  4653. // otherwise, just render a highlight
  4654. this.renderHighlight(this.eventRangeToSegs(dropLocation));
  4655. }
  4656. },
  4657. // Unrenders any visual indication of an event being dragged
  4658. unrenderDrag: function() {
  4659. this.unrenderHelper();
  4660. this.unrenderHighlight();
  4661. },
  4662. /* Event Resize Visualization
  4663. ------------------------------------------------------------------------------------------------------------------*/
  4664. // Renders a visual indication of an event being resized
  4665. renderEventResize: function(range, seg) {
  4666. this.renderRangeHelper(range, seg);
  4667. },
  4668. // Unrenders any visual indication of an event being resized
  4669. unrenderEventResize: function() {
  4670. this.unrenderHelper();
  4671. },
  4672. /* Event Helper
  4673. ------------------------------------------------------------------------------------------------------------------*/
  4674. // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  4675. renderHelper: function(event, sourceSeg) {
  4676. var segs = this.eventsToSegs([ event ]);
  4677. var tableEl;
  4678. var i, seg;
  4679. var sourceEl;
  4680. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  4681. tableEl = this.renderSegTable(segs);
  4682. // Try to make the segment that is in the same row as sourceSeg look the same
  4683. for (i = 0; i < segs.length; i++) {
  4684. seg = segs[i];
  4685. if (sourceSeg && sourceSeg.col === seg.col) {
  4686. sourceEl = sourceSeg.el;
  4687. seg.el.css({
  4688. left: sourceEl.css('left'),
  4689. right: sourceEl.css('right'),
  4690. 'margin-left': sourceEl.css('margin-left'),
  4691. 'margin-right': sourceEl.css('margin-right')
  4692. });
  4693. }
  4694. }
  4695. this.helperEl = $('<div class="fc-helper-skeleton"/>')
  4696. .append(tableEl)
  4697. .appendTo(this.el);
  4698. },
  4699. // Unrenders any mock helper event
  4700. unrenderHelper: function() {
  4701. if (this.helperEl) {
  4702. this.helperEl.remove();
  4703. this.helperEl = null;
  4704. }
  4705. },
  4706. /* Selection
  4707. ------------------------------------------------------------------------------------------------------------------*/
  4708. // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  4709. renderSelection: function(range) {
  4710. if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  4711. this.renderRangeHelper(range);
  4712. }
  4713. else {
  4714. this.renderHighlight(this.selectionRangeToSegs(range));
  4715. }
  4716. },
  4717. // Unrenders any visual indication of a selection
  4718. unrenderSelection: function() {
  4719. this.unrenderHelper();
  4720. this.unrenderHighlight();
  4721. },
  4722. /* Fill System (highlight, background events, business hours)
  4723. ------------------------------------------------------------------------------------------------------------------*/
  4724. // Renders a set of rectangles over the given time segments.
  4725. // Only returns segments that successfully rendered.
  4726. renderFill: function(type, segs, className) {
  4727. var segCols;
  4728. var skeletonEl;
  4729. var trEl;
  4730. var col, colSegs;
  4731. var tdEl;
  4732. var containerEl;
  4733. var dayDate;
  4734. var i, seg;
  4735. if (segs.length) {
  4736. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  4737. segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  4738. className = className || type.toLowerCase();
  4739. skeletonEl = $(
  4740. '<div class="fc-' + className + '-skeleton">' +
  4741. '<table><tr/></table>' +
  4742. '</div>'
  4743. );
  4744. trEl = skeletonEl.find('tr');
  4745. for (col = 0; col < segCols.length; col++) {
  4746. colSegs = segCols[col];
  4747. tdEl = $('<td/>').appendTo(trEl);
  4748. if (colSegs.length) {
  4749. containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
  4750. dayDate = this.colDates[col];
  4751. for (i = 0; i < colSegs.length; i++) {
  4752. seg = colSegs[i];
  4753. containerEl.append(
  4754. seg.el.css({
  4755. top: this.computeDateTop(seg.start, dayDate),
  4756. bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
  4757. })
  4758. );
  4759. }
  4760. }
  4761. }
  4762. this.bookendCells(trEl, type);
  4763. this.el.append(skeletonEl);
  4764. this.elsByFill[type] = skeletonEl;
  4765. }
  4766. return segs;
  4767. }
  4768. });
  4769. ;;
  4770. /* Event-rendering methods for the TimeGrid class
  4771. ----------------------------------------------------------------------------------------------------------------------*/
  4772. TimeGrid.mixin({
  4773. eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
  4774. // Renders the given foreground event segments onto the grid
  4775. renderFgSegs: function(segs) {
  4776. segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
  4777. this.el.append(
  4778. this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
  4779. .append(this.renderSegTable(segs))
  4780. );
  4781. return segs; // return only the segs that were actually rendered
  4782. },
  4783. // Unrenders all currently rendered foreground event segments
  4784. unrenderFgSegs: function(segs) {
  4785. if (this.eventSkeletonEl) {
  4786. this.eventSkeletonEl.remove();
  4787. this.eventSkeletonEl = null;
  4788. }
  4789. },
  4790. // Renders and returns the <table> portion of the event-skeleton.
  4791. // Returns an object with properties 'tbodyEl' and 'segs'.
  4792. renderSegTable: function(segs) {
  4793. var tableEl = $('<table><tr/></table>');
  4794. var trEl = tableEl.find('tr');
  4795. var segCols;
  4796. var i, seg;
  4797. var col, colSegs;
  4798. var containerEl;
  4799. segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  4800. this.computeSegVerticals(segs); // compute and assign top/bottom
  4801. for (col = 0; col < segCols.length; col++) { // iterate each column grouping
  4802. colSegs = segCols[col];
  4803. placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
  4804. containerEl = $('<div class="fc-event-container"/>');
  4805. // assign positioning CSS and insert into container
  4806. for (i = 0; i < colSegs.length; i++) {
  4807. seg = colSegs[i];
  4808. seg.el.css(this.generateSegPositionCss(seg));
  4809. // if the height is short, add a className for alternate styling
  4810. if (seg.bottom - seg.top < 30) {
  4811. seg.el.addClass('fc-short');
  4812. }
  4813. containerEl.append(seg.el);
  4814. }
  4815. trEl.append($('<td/>').append(containerEl));
  4816. }
  4817. this.bookendCells(trEl, 'eventSkeleton');
  4818. return tableEl;
  4819. },
  4820. // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
  4821. // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
  4822. updateSegVerticals: function() {
  4823. var allSegs = (this.segs || []).concat(this.businessHourSegs || []);
  4824. var i;
  4825. this.computeSegVerticals(allSegs);
  4826. for (i = 0; i < allSegs.length; i++) {
  4827. allSegs[i].el.css(
  4828. this.generateSegVerticalCss(allSegs[i])
  4829. );
  4830. }
  4831. },
  4832. // For each segment in an array, computes and assigns its top and bottom properties
  4833. computeSegVerticals: function(segs) {
  4834. var i, seg;
  4835. for (i = 0; i < segs.length; i++) {
  4836. seg = segs[i];
  4837. seg.top = this.computeDateTop(seg.start, seg.start);
  4838. seg.bottom = this.computeDateTop(seg.end, seg.start);
  4839. }
  4840. },
  4841. // Renders the HTML for a single event segment's default rendering
  4842. fgSegHtml: function(seg, disableResizing) {
  4843. var view = this.view;
  4844. var event = seg.event;
  4845. var isDraggable = view.isEventDraggable(event);
  4846. var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
  4847. var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
  4848. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  4849. var skinCss = cssToStr(this.getEventSkinCss(event));
  4850. var timeText;
  4851. var fullTimeText; // more verbose time text. for the print stylesheet
  4852. var startTimeText; // just the start time text
  4853. classes.unshift('fc-time-grid-event', 'fc-v-event');
  4854. if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  4855. // Don't display time text on segments that run entirely through a day.
  4856. // That would appear as midnight-midnight and would look dumb.
  4857. // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  4858. if (seg.isStart || seg.isEnd) {
  4859. timeText = this.getEventTimeText(seg);
  4860. fullTimeText = this.getEventTimeText(seg, 'LT');
  4861. startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
  4862. }
  4863. } else {
  4864. // Display the normal time text for the *event's* times
  4865. timeText = this.getEventTimeText(event);
  4866. fullTimeText = this.getEventTimeText(event, 'LT');
  4867. startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
  4868. }
  4869. return '<a class="' + classes.join(' ') + '"' +
  4870. (event.url ?
  4871. ' href="' + htmlEscape(event.url) + '"' :
  4872. ''
  4873. ) +
  4874. (skinCss ?
  4875. ' style="' + skinCss + '"' :
  4876. ''
  4877. ) +
  4878. '>' +
  4879. '<div class="fc-content">' +
  4880. (timeText ?
  4881. '<div class="fc-time"' +
  4882. ' data-start="' + htmlEscape(startTimeText) + '"' +
  4883. ' data-full="' + htmlEscape(fullTimeText) + '"' +
  4884. '>' +
  4885. '<span>' + htmlEscape(timeText) + '</span>' +
  4886. '</div>' :
  4887. ''
  4888. ) +
  4889. (event.title ?
  4890. '<div class="fc-title">' +
  4891. htmlEscape(event.title) +
  4892. '</div>' :
  4893. ''
  4894. ) +
  4895. '</div>' +
  4896. '<div class="fc-bg"/>' +
  4897. /* TODO: write CSS for this
  4898. (isResizableFromStart ?
  4899. '<div class="fc-resizer fc-start-resizer" />' :
  4900. ''
  4901. ) +
  4902. */
  4903. (isResizableFromEnd ?
  4904. '<div class="fc-resizer fc-end-resizer" />' :
  4905. ''
  4906. ) +
  4907. '</a>';
  4908. },
  4909. // Generates an object with CSS properties/values that should be applied to an event segment element.
  4910. // Contains important positioning-related properties that should be applied to any event element, customized or not.
  4911. generateSegPositionCss: function(seg) {
  4912. var shouldOverlap = this.view.opt('slotEventOverlap');
  4913. var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  4914. var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  4915. var props = this.generateSegVerticalCss(seg); // get top/bottom first
  4916. var left; // amount of space from left edge, a fraction of the total width
  4917. var right; // amount of space from right edge, a fraction of the total width
  4918. if (shouldOverlap) {
  4919. // double the width, but don't go beyond the maximum forward coordinate (1.0)
  4920. forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  4921. }
  4922. if (this.isRTL) {
  4923. left = 1 - forwardCoord;
  4924. right = backwardCoord;
  4925. }
  4926. else {
  4927. left = backwardCoord;
  4928. right = 1 - forwardCoord;
  4929. }
  4930. props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  4931. props.left = left * 100 + '%';
  4932. props.right = right * 100 + '%';
  4933. if (shouldOverlap && seg.forwardPressure) {
  4934. // add padding to the edge so that forward stacked events don't cover the resizer's icon
  4935. props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  4936. }
  4937. return props;
  4938. },
  4939. // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  4940. generateSegVerticalCss: function(seg) {
  4941. return {
  4942. top: seg.top,
  4943. bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  4944. };
  4945. },
  4946. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  4947. groupSegCols: function(segs) {
  4948. var segCols = [];
  4949. var i;
  4950. for (i = 0; i < this.colCnt; i++) {
  4951. segCols.push([]);
  4952. }
  4953. for (i = 0; i < segs.length; i++) {
  4954. segCols[segs[i].col].push(segs[i]);
  4955. }
  4956. return segCols;
  4957. }
  4958. });
  4959. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  4960. // NOTE: Also reorders the given array by date!
  4961. function placeSlotSegs(segs) {
  4962. var levels;
  4963. var level0;
  4964. var i;
  4965. segs.sort(compareSegs); // order by date
  4966. levels = buildSlotSegLevels(segs);
  4967. computeForwardSlotSegs(levels);
  4968. if ((level0 = levels[0])) {
  4969. for (i = 0; i < level0.length; i++) {
  4970. computeSlotSegPressures(level0[i]);
  4971. }
  4972. for (i = 0; i < level0.length; i++) {
  4973. computeSlotSegCoords(level0[i], 0, 0);
  4974. }
  4975. }
  4976. }
  4977. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  4978. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  4979. function buildSlotSegLevels(segs) {
  4980. var levels = [];
  4981. var i, seg;
  4982. var j;
  4983. for (i=0; i<segs.length; i++) {
  4984. seg = segs[i];
  4985. // go through all the levels and stop on the first level where there are no collisions
  4986. for (j=0; j<levels.length; j++) {
  4987. if (!computeSlotSegCollisions(seg, levels[j]).length) {
  4988. break;
  4989. }
  4990. }
  4991. seg.level = j;
  4992. (levels[j] || (levels[j] = [])).push(seg);
  4993. }
  4994. return levels;
  4995. }
  4996. // For every segment, figure out the other segments that are in subsequent
  4997. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  4998. function computeForwardSlotSegs(levels) {
  4999. var i, level;
  5000. var j, seg;
  5001. var k;
  5002. for (i=0; i<levels.length; i++) {
  5003. level = levels[i];
  5004. for (j=0; j<level.length; j++) {
  5005. seg = level[j];
  5006. seg.forwardSegs = [];
  5007. for (k=i+1; k<levels.length; k++) {
  5008. computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  5009. }
  5010. }
  5011. }
  5012. }
  5013. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  5014. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  5015. function computeSlotSegPressures(seg) {
  5016. var forwardSegs = seg.forwardSegs;
  5017. var forwardPressure = 0;
  5018. var i, forwardSeg;
  5019. if (seg.forwardPressure === undefined) { // not already computed
  5020. for (i=0; i<forwardSegs.length; i++) {
  5021. forwardSeg = forwardSegs[i];
  5022. // figure out the child's maximum forward path
  5023. computeSlotSegPressures(forwardSeg);
  5024. // either use the existing maximum, or use the child's forward pressure
  5025. // plus one (for the forwardSeg itself)
  5026. forwardPressure = Math.max(
  5027. forwardPressure,
  5028. 1 + forwardSeg.forwardPressure
  5029. );
  5030. }
  5031. seg.forwardPressure = forwardPressure;
  5032. }
  5033. }
  5034. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  5035. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  5036. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  5037. //
  5038. // The segment might be part of a "series", which means consecutive segments with the same pressure
  5039. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  5040. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  5041. // coordinate of the first segment in the series.
  5042. function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
  5043. var forwardSegs = seg.forwardSegs;
  5044. var i;
  5045. if (seg.forwardCoord === undefined) { // not already computed
  5046. if (!forwardSegs.length) {
  5047. // if there are no forward segments, this segment should butt up against the edge
  5048. seg.forwardCoord = 1;
  5049. }
  5050. else {
  5051. // sort highest pressure first
  5052. forwardSegs.sort(compareForwardSlotSegs);
  5053. // this segment's forwardCoord will be calculated from the backwardCoord of the
  5054. // highest-pressure forward segment.
  5055. computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  5056. seg.forwardCoord = forwardSegs[0].backwardCoord;
  5057. }
  5058. // calculate the backwardCoord from the forwardCoord. consider the series
  5059. seg.backwardCoord = seg.forwardCoord -
  5060. (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  5061. (seriesBackwardPressure + 1); // # of segments in the series
  5062. // use this segment's coordinates to computed the coordinates of the less-pressurized
  5063. // forward segments
  5064. for (i=0; i<forwardSegs.length; i++) {
  5065. computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
  5066. }
  5067. }
  5068. }
  5069. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  5070. // Append into an optionally-supplied `results` array and return.
  5071. function computeSlotSegCollisions(seg, otherSegs, results) {
  5072. results = results || [];
  5073. for (var i=0; i<otherSegs.length; i++) {
  5074. if (isSlotSegCollision(seg, otherSegs[i])) {
  5075. results.push(otherSegs[i]);
  5076. }
  5077. }
  5078. return results;
  5079. }
  5080. // Do these segments occupy the same vertical space?
  5081. function isSlotSegCollision(seg1, seg2) {
  5082. return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  5083. }
  5084. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  5085. function compareForwardSlotSegs(seg1, seg2) {
  5086. // put higher-pressure first
  5087. return seg2.forwardPressure - seg1.forwardPressure ||
  5088. // put segments that are closer to initial edge first (and favor ones with no coords yet)
  5089. (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  5090. // do normal sorting...
  5091. compareSegs(seg1, seg2);
  5092. }
  5093. ;;
  5094. /* An abstract class from which other views inherit from
  5095. ----------------------------------------------------------------------------------------------------------------------*/
  5096. var View = fc.View = Class.extend({
  5097. type: null, // subclass' view name (string)
  5098. name: null, // deprecated. use `type` instead
  5099. title: null, // the text that will be displayed in the header's title
  5100. calendar: null, // owner Calendar object
  5101. options: null, // hash containing all options. already merged with view-specific-options
  5102. coordMap: null, // a CoordMap object for converting pixel regions to dates
  5103. el: null, // the view's containing element. set by Calendar
  5104. displaying: null, // a promise representing the state of rendering. null if no render requested
  5105. isSkeletonRendered: false,
  5106. isEventsRendered: false,
  5107. // range the view is actually displaying (moments)
  5108. start: null,
  5109. end: null, // exclusive
  5110. // range the view is formally responsible for (moments)
  5111. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  5112. intervalStart: null,
  5113. intervalEnd: null, // exclusive
  5114. intervalDuration: null,
  5115. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  5116. isRTL: false,
  5117. isSelected: false, // boolean whether a range of time is user-selected or not
  5118. // subclasses can optionally use a scroll container
  5119. scrollerEl: null, // the element that will most likely scroll when content is too tall
  5120. scrollTop: null, // cached vertical scroll value
  5121. // classNames styled by jqui themes
  5122. widgetHeaderClass: null,
  5123. widgetContentClass: null,
  5124. highlightStateClass: null,
  5125. // for date utils, computed from options
  5126. nextDayThreshold: null,
  5127. isHiddenDayHash: null,
  5128. // document handlers, bound to `this` object
  5129. documentMousedownProxy: null, // TODO: doesn't work with touch
  5130. constructor: function(calendar, type, options, intervalDuration) {
  5131. this.calendar = calendar;
  5132. this.type = this.name = type; // .name is deprecated
  5133. this.options = options;
  5134. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  5135. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  5136. this.initThemingProps();
  5137. this.initHiddenDays();
  5138. this.isRTL = this.opt('isRTL');
  5139. this.documentMousedownProxy = proxy(this, 'documentMousedown');
  5140. this.initialize();
  5141. },
  5142. // A good place for subclasses to initialize member variables
  5143. initialize: function() {
  5144. // subclasses can implement
  5145. },
  5146. // Retrieves an option with the given name
  5147. opt: function(name) {
  5148. return this.options[name];
  5149. },
  5150. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  5151. trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  5152. var calendar = this.calendar;
  5153. return calendar.trigger.apply(
  5154. calendar,
  5155. [name, thisObj || this].concat(
  5156. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  5157. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  5158. )
  5159. );
  5160. },
  5161. /* Dates
  5162. ------------------------------------------------------------------------------------------------------------------*/
  5163. // Updates all internal dates to center around the given current date
  5164. setDate: function(date) {
  5165. this.setRange(this.computeRange(date));
  5166. },
  5167. // Updates all internal dates for displaying the given range.
  5168. // Expects all values to be normalized (like what computeRange does).
  5169. setRange: function(range) {
  5170. $.extend(this, range);
  5171. this.updateTitle();
  5172. },
  5173. // Given a single current date, produce information about what range to display.
  5174. // Subclasses can override. Must return all properties.
  5175. computeRange: function(date) {
  5176. var intervalUnit = computeIntervalUnit(this.intervalDuration);
  5177. var intervalStart = date.clone().startOf(intervalUnit);
  5178. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  5179. var start, end;
  5180. // normalize the range's time-ambiguity
  5181. if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  5182. intervalStart.stripTime();
  5183. intervalEnd.stripTime();
  5184. }
  5185. else { // needs to have a time?
  5186. if (!intervalStart.hasTime()) {
  5187. intervalStart = this.calendar.rezoneDate(intervalStart); // convert to current timezone, with 00:00
  5188. }
  5189. if (!intervalEnd.hasTime()) {
  5190. intervalEnd = this.calendar.rezoneDate(intervalEnd); // convert to current timezone, with 00:00
  5191. }
  5192. }
  5193. start = intervalStart.clone();
  5194. start = this.skipHiddenDays(start);
  5195. end = intervalEnd.clone();
  5196. end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  5197. return {
  5198. intervalUnit: intervalUnit,
  5199. intervalStart: intervalStart,
  5200. intervalEnd: intervalEnd,
  5201. start: start,
  5202. end: end
  5203. };
  5204. },
  5205. // Computes the new date when the user hits the prev button, given the current date
  5206. computePrevDate: function(date) {
  5207. return this.massageCurrentDate(
  5208. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  5209. );
  5210. },
  5211. // Computes the new date when the user hits the next button, given the current date
  5212. computeNextDate: function(date) {
  5213. return this.massageCurrentDate(
  5214. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  5215. );
  5216. },
  5217. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  5218. // visible. `direction` is optional and indicates which direction the current date was being
  5219. // incremented or decremented (1 or -1).
  5220. massageCurrentDate: function(date, direction) {
  5221. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  5222. if (this.isHiddenDay(date)) {
  5223. date = this.skipHiddenDays(date, direction);
  5224. date.startOf('day');
  5225. }
  5226. }
  5227. return date;
  5228. },
  5229. /* Title and Date Formatting
  5230. ------------------------------------------------------------------------------------------------------------------*/
  5231. // Sets the view's title property to the most updated computed value
  5232. updateTitle: function() {
  5233. this.title = this.computeTitle();
  5234. },
  5235. // Computes what the title at the top of the calendar should be for this view
  5236. computeTitle: function() {
  5237. return this.formatRange(
  5238. { start: this.intervalStart, end: this.intervalEnd },
  5239. this.opt('titleFormat') || this.computeTitleFormat(),
  5240. this.opt('titleRangeSeparator')
  5241. );
  5242. },
  5243. // Generates the format string that should be used to generate the title for the current date range.
  5244. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  5245. computeTitleFormat: function() {
  5246. if (this.intervalUnit == 'year') {
  5247. return 'YYYY';
  5248. }
  5249. else if (this.intervalUnit == 'month') {
  5250. return this.opt('monthYearFormat'); // like "September 2014"
  5251. }
  5252. else if (this.intervalDuration.as('days') > 1) {
  5253. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  5254. }
  5255. else {
  5256. return 'LL'; // one day. longer, like "September 9 2014"
  5257. }
  5258. },
  5259. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  5260. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  5261. formatRange: function(range, formatStr, separator) {
  5262. var end = range.end;
  5263. if (!end.hasTime()) { // all-day?
  5264. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  5265. }
  5266. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  5267. },
  5268. /* Rendering
  5269. ------------------------------------------------------------------------------------------------------------------*/
  5270. // Sets the container element that the view should render inside of.
  5271. // Does other DOM-related initializations.
  5272. setElement: function(el) {
  5273. this.el = el;
  5274. this.bindGlobalHandlers();
  5275. },
  5276. // Removes the view's container element from the DOM, clearing any content beforehand.
  5277. // Undoes any other DOM-related attachments.
  5278. removeElement: function() {
  5279. this.clear(); // clears all content
  5280. // clean up the skeleton
  5281. if (this.isSkeletonRendered) {
  5282. this.unrenderSkeleton();
  5283. this.isSkeletonRendered = false;
  5284. }
  5285. this.unbindGlobalHandlers();
  5286. this.el.remove();
  5287. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  5288. // We don't null-out the View's other jQuery element references upon destroy,
  5289. // so we shouldn't kill this.el either.
  5290. },
  5291. // Does everything necessary to display the view centered around the given date.
  5292. // Does every type of rendering EXCEPT rendering events.
  5293. // Is asychronous and returns a promise.
  5294. display: function(date) {
  5295. var _this = this;
  5296. var scrollState = null;
  5297. if (this.displaying) {
  5298. scrollState = this.queryScroll();
  5299. }
  5300. return this.clear().then(function() { // clear the content first (async)
  5301. return (
  5302. _this.displaying =
  5303. $.when(_this.displayView(date)) // displayView might return a promise
  5304. .then(function() {
  5305. _this.forceScroll(_this.computeInitialScroll(scrollState));
  5306. _this.triggerRender();
  5307. })
  5308. );
  5309. });
  5310. },
  5311. // Does everything necessary to clear the content of the view.
  5312. // Clears dates and events. Does not clear the skeleton.
  5313. // Is asychronous and returns a promise.
  5314. clear: function() {
  5315. var _this = this;
  5316. var displaying = this.displaying;
  5317. if (displaying) { // previously displayed, or in the process of being displayed?
  5318. return displaying.then(function() { // wait for the display to finish
  5319. _this.displaying = null;
  5320. _this.clearEvents();
  5321. return _this.clearView(); // might return a promise. chain it
  5322. });
  5323. }
  5324. else {
  5325. return $.when(); // an immediately-resolved promise
  5326. }
  5327. },
  5328. // Displays the view's non-event content, such as date-related content or anything required by events.
  5329. // Renders the view's non-content skeleton if necessary.
  5330. // Can be asynchronous and return a promise.
  5331. displayView: function(date) {
  5332. if (!this.isSkeletonRendered) {
  5333. this.renderSkeleton();
  5334. this.isSkeletonRendered = true;
  5335. }
  5336. this.setDate(date);
  5337. if (this.render) {
  5338. this.render(); // TODO: deprecate
  5339. }
  5340. this.renderDates();
  5341. this.updateSize();
  5342. this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  5343. },
  5344. // Unrenders the view content that was rendered in displayView.
  5345. // Can be asynchronous and return a promise.
  5346. clearView: function() {
  5347. this.unselect();
  5348. this.triggerUnrender();
  5349. this.unrenderBusinessHours();
  5350. this.unrenderDates();
  5351. if (this.destroy) {
  5352. this.destroy(); // TODO: deprecate
  5353. }
  5354. },
  5355. // Renders the basic structure of the view before any content is rendered
  5356. renderSkeleton: function() {
  5357. // subclasses should implement
  5358. },
  5359. // Unrenders the basic structure of the view
  5360. unrenderSkeleton: function() {
  5361. // subclasses should implement
  5362. },
  5363. // Renders the view's date-related content (like cells that represent days/times).
  5364. // Assumes setRange has already been called and the skeleton has already been rendered.
  5365. renderDates: function() {
  5366. // subclasses should implement
  5367. },
  5368. // Unrenders the view's date-related content
  5369. unrenderDates: function() {
  5370. // subclasses should override
  5371. },
  5372. // Renders business-hours onto the view. Assumes updateSize has already been called.
  5373. renderBusinessHours: function() {
  5374. // subclasses should implement
  5375. },
  5376. // Unrenders previously-rendered business-hours
  5377. unrenderBusinessHours: function() {
  5378. // subclasses should implement
  5379. },
  5380. // Signals that the view's content has been rendered
  5381. triggerRender: function() {
  5382. this.trigger('viewRender', this, this, this.el);
  5383. },
  5384. // Signals that the view's content is about to be unrendered
  5385. triggerUnrender: function() {
  5386. this.trigger('viewDestroy', this, this, this.el);
  5387. },
  5388. // Binds DOM handlers to elements that reside outside the view container, such as the document
  5389. bindGlobalHandlers: function() {
  5390. $(document).on('mousedown', this.documentMousedownProxy);
  5391. },
  5392. // Unbinds DOM handlers from elements that reside outside the view container
  5393. unbindGlobalHandlers: function() {
  5394. $(document).off('mousedown', this.documentMousedownProxy);
  5395. },
  5396. // Initializes internal variables related to theming
  5397. initThemingProps: function() {
  5398. var tm = this.opt('theme') ? 'ui' : 'fc';
  5399. this.widgetHeaderClass = tm + '-widget-header';
  5400. this.widgetContentClass = tm + '-widget-content';
  5401. this.highlightStateClass = tm + '-state-highlight';
  5402. },
  5403. /* Dimensions
  5404. ------------------------------------------------------------------------------------------------------------------*/
  5405. // Refreshes anything dependant upon sizing of the container element of the grid
  5406. updateSize: function(isResize) {
  5407. var scrollState;
  5408. if (isResize) {
  5409. scrollState = this.queryScroll();
  5410. }
  5411. this.updateHeight(isResize);
  5412. this.updateWidth(isResize);
  5413. if (isResize) {
  5414. this.setScroll(scrollState);
  5415. }
  5416. },
  5417. // Refreshes the horizontal dimensions of the calendar
  5418. updateWidth: function(isResize) {
  5419. // subclasses should implement
  5420. },
  5421. // Refreshes the vertical dimensions of the calendar
  5422. updateHeight: function(isResize) {
  5423. var calendar = this.calendar; // we poll the calendar for height information
  5424. this.setHeight(
  5425. calendar.getSuggestedViewHeight(),
  5426. calendar.isHeightAuto()
  5427. );
  5428. },
  5429. // Updates the vertical dimensions of the calendar to the specified height.
  5430. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  5431. setHeight: function(height, isAuto) {
  5432. // subclasses should implement
  5433. },
  5434. /* Scroller
  5435. ------------------------------------------------------------------------------------------------------------------*/
  5436. // Given the total height of the view, return the number of pixels that should be used for the scroller.
  5437. // Utility for subclasses.
  5438. computeScrollerHeight: function(totalHeight) {
  5439. var scrollerEl = this.scrollerEl;
  5440. var both;
  5441. var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
  5442. both = this.el.add(scrollerEl);
  5443. // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  5444. both.css({
  5445. position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  5446. left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  5447. });
  5448. otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
  5449. both.css({ position: '', left: '' }); // undo hack
  5450. return totalHeight - otherHeight;
  5451. },
  5452. // Computes the initial pre-configured scroll state prior to allowing the user to change it.
  5453. // Given the scroll state from the previous rendering. If first time rendering, given null.
  5454. computeInitialScroll: function(previousScrollState) {
  5455. return 0;
  5456. },
  5457. // Retrieves the view's current natural scroll state. Can return an arbitrary format.
  5458. queryScroll: function() {
  5459. if (this.scrollerEl) {
  5460. return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
  5461. }
  5462. },
  5463. // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
  5464. setScroll: function(scrollState) {
  5465. if (this.scrollerEl) {
  5466. return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
  5467. }
  5468. },
  5469. // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
  5470. forceScroll: function(scrollState) {
  5471. var _this = this;
  5472. this.setScroll(scrollState);
  5473. setTimeout(function() {
  5474. _this.setScroll(scrollState);
  5475. }, 0);
  5476. },
  5477. /* Event Elements / Segments
  5478. ------------------------------------------------------------------------------------------------------------------*/
  5479. // Does everything necessary to display the given events onto the current view
  5480. displayEvents: function(events) {
  5481. var scrollState = this.queryScroll();
  5482. this.clearEvents();
  5483. this.renderEvents(events);
  5484. this.isEventsRendered = true;
  5485. this.setScroll(scrollState);
  5486. this.triggerEventRender();
  5487. },
  5488. // Does everything necessary to clear the view's currently-rendered events
  5489. clearEvents: function() {
  5490. if (this.isEventsRendered) {
  5491. this.triggerEventUnrender();
  5492. if (this.destroyEvents) {
  5493. this.destroyEvents(); // TODO: deprecate
  5494. }
  5495. this.unrenderEvents();
  5496. this.isEventsRendered = false;
  5497. }
  5498. },
  5499. // Renders the events onto the view.
  5500. renderEvents: function(events) {
  5501. // subclasses should implement
  5502. },
  5503. // Removes event elements from the view.
  5504. unrenderEvents: function() {
  5505. // subclasses should implement
  5506. },
  5507. // Signals that all events have been rendered
  5508. triggerEventRender: function() {
  5509. this.renderedEventSegEach(function(seg) {
  5510. this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  5511. });
  5512. this.trigger('eventAfterAllRender');
  5513. },
  5514. // Signals that all event elements are about to be removed
  5515. triggerEventUnrender: function() {
  5516. this.renderedEventSegEach(function(seg) {
  5517. this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  5518. });
  5519. },
  5520. // Given an event and the default element used for rendering, returns the element that should actually be used.
  5521. // Basically runs events and elements through the eventRender hook.
  5522. resolveEventEl: function(event, el) {
  5523. var custom = this.trigger('eventRender', event, event, el);
  5524. if (custom === false) { // means don't render at all
  5525. el = null;
  5526. }
  5527. else if (custom && custom !== true) {
  5528. el = $(custom);
  5529. }
  5530. return el;
  5531. },
  5532. // Hides all rendered event segments linked to the given event
  5533. showEvent: function(event) {
  5534. this.renderedEventSegEach(function(seg) {
  5535. seg.el.css('visibility', '');
  5536. }, event);
  5537. },
  5538. // Shows all rendered event segments linked to the given event
  5539. hideEvent: function(event) {
  5540. this.renderedEventSegEach(function(seg) {
  5541. seg.el.css('visibility', 'hidden');
  5542. }, event);
  5543. },
  5544. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  5545. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  5546. // The `this` value of the callback function will be the view.
  5547. renderedEventSegEach: function(func, event) {
  5548. var segs = this.getEventSegs();
  5549. var i;
  5550. for (i = 0; i < segs.length; i++) {
  5551. if (!event || segs[i].event._id === event._id) {
  5552. if (segs[i].el) {
  5553. func.call(this, segs[i]);
  5554. }
  5555. }
  5556. }
  5557. },
  5558. // Retrieves all the rendered segment objects for the view
  5559. getEventSegs: function() {
  5560. // subclasses must implement
  5561. return [];
  5562. },
  5563. /* Event Drag-n-Drop
  5564. ------------------------------------------------------------------------------------------------------------------*/
  5565. // Computes if the given event is allowed to be dragged by the user
  5566. isEventDraggable: function(event) {
  5567. var source = event.source || {};
  5568. return firstDefined(
  5569. event.startEditable,
  5570. source.startEditable,
  5571. this.opt('eventStartEditable'),
  5572. event.editable,
  5573. source.editable,
  5574. this.opt('editable')
  5575. );
  5576. },
  5577. // Must be called when an event in the view is dropped onto new location.
  5578. // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  5579. reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  5580. var calendar = this.calendar;
  5581. var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  5582. var undoFunc = function() {
  5583. mutateResult.undo();
  5584. calendar.reportEventChange();
  5585. };
  5586. this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  5587. calendar.reportEventChange(); // will rerender events
  5588. },
  5589. // Triggers event-drop handlers that have subscribed via the API
  5590. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  5591. this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  5592. },
  5593. /* External Element Drag-n-Drop
  5594. ------------------------------------------------------------------------------------------------------------------*/
  5595. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  5596. // `meta` is the parsed data that has been embedded into the dragging event.
  5597. // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  5598. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  5599. var eventProps = meta.eventProps;
  5600. var eventInput;
  5601. var event;
  5602. // Try to build an event object and render it. TODO: decouple the two
  5603. if (eventProps) {
  5604. eventInput = $.extend({}, eventProps, dropLocation);
  5605. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  5606. }
  5607. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  5608. },
  5609. // Triggers external-drop handlers that have subscribed via the API
  5610. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  5611. // trigger 'drop' regardless of whether element represents an event
  5612. this.trigger('drop', el[0], dropLocation.start, ev, ui);
  5613. if (event) {
  5614. this.trigger('eventReceive', null, event); // signal an external event landed
  5615. }
  5616. },
  5617. /* Drag-n-Drop Rendering (for both events and external elements)
  5618. ------------------------------------------------------------------------------------------------------------------*/
  5619. // Renders a visual indication of a event or external-element drag over the given drop zone.
  5620. // If an external-element, seg will be `null`
  5621. renderDrag: function(dropLocation, seg) {
  5622. // subclasses must implement
  5623. },
  5624. // Unrenders a visual indication of an event or external-element being dragged.
  5625. unrenderDrag: function() {
  5626. // subclasses must implement
  5627. },
  5628. /* Event Resizing
  5629. ------------------------------------------------------------------------------------------------------------------*/
  5630. // Computes if the given event is allowed to be resized from its starting edge
  5631. isEventResizableFromStart: function(event) {
  5632. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  5633. },
  5634. // Computes if the given event is allowed to be resized from its ending edge
  5635. isEventResizableFromEnd: function(event) {
  5636. return this.isEventResizable(event);
  5637. },
  5638. // Computes if the given event is allowed to be resized by the user at all
  5639. isEventResizable: function(event) {
  5640. var source = event.source || {};
  5641. return firstDefined(
  5642. event.durationEditable,
  5643. source.durationEditable,
  5644. this.opt('eventDurationEditable'),
  5645. event.editable,
  5646. source.editable,
  5647. this.opt('editable')
  5648. );
  5649. },
  5650. // Must be called when an event in the view has been resized to a new length
  5651. reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  5652. var calendar = this.calendar;
  5653. var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  5654. var undoFunc = function() {
  5655. mutateResult.undo();
  5656. calendar.reportEventChange();
  5657. };
  5658. this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  5659. calendar.reportEventChange(); // will rerender events
  5660. },
  5661. // Triggers event-resize handlers that have subscribed via the API
  5662. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  5663. this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  5664. },
  5665. /* Selection
  5666. ------------------------------------------------------------------------------------------------------------------*/
  5667. // Selects a date range on the view. `start` and `end` are both Moments.
  5668. // `ev` is the native mouse event that begin the interaction.
  5669. select: function(range, ev) {
  5670. this.unselect(ev);
  5671. this.renderSelection(range);
  5672. this.reportSelection(range, ev);
  5673. },
  5674. // Renders a visual indication of the selection
  5675. renderSelection: function(range) {
  5676. // subclasses should implement
  5677. },
  5678. // Called when a new selection is made. Updates internal state and triggers handlers.
  5679. reportSelection: function(range, ev) {
  5680. this.isSelected = true;
  5681. this.triggerSelect(range, ev);
  5682. },
  5683. // Triggers handlers to 'select'
  5684. triggerSelect: function(range, ev) {
  5685. this.trigger('select', null, range.start, range.end, ev);
  5686. },
  5687. // Undoes a selection. updates in the internal state and triggers handlers.
  5688. // `ev` is the native mouse event that began the interaction.
  5689. unselect: function(ev) {
  5690. if (this.isSelected) {
  5691. this.isSelected = false;
  5692. if (this.destroySelection) {
  5693. this.destroySelection(); // TODO: deprecate
  5694. }
  5695. this.unrenderSelection();
  5696. this.trigger('unselect', null, ev);
  5697. }
  5698. },
  5699. // Unrenders a visual indication of selection
  5700. unrenderSelection: function() {
  5701. // subclasses should implement
  5702. },
  5703. // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
  5704. documentMousedown: function(ev) {
  5705. var ignore;
  5706. // is there a selection, and has the user made a proper left click?
  5707. if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
  5708. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  5709. ignore = this.opt('unselectCancel');
  5710. if (!ignore || !$(ev.target).closest(ignore).length) {
  5711. this.unselect(ev);
  5712. }
  5713. }
  5714. },
  5715. /* Day Click
  5716. ------------------------------------------------------------------------------------------------------------------*/
  5717. // Triggers handlers to 'dayClick'
  5718. triggerDayClick: function(cell, dayEl, ev) {
  5719. this.trigger('dayClick', dayEl, cell.start, ev);
  5720. },
  5721. /* Date Utils
  5722. ------------------------------------------------------------------------------------------------------------------*/
  5723. // Initializes internal variables related to calculating hidden days-of-week
  5724. initHiddenDays: function() {
  5725. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  5726. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  5727. var dayCnt = 0;
  5728. var i;
  5729. if (this.opt('weekends') === false) {
  5730. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  5731. }
  5732. for (i = 0; i < 7; i++) {
  5733. if (
  5734. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  5735. ) {
  5736. dayCnt++;
  5737. }
  5738. }
  5739. if (!dayCnt) {
  5740. throw 'invalid hiddenDays'; // all days were hidden? bad.
  5741. }
  5742. this.isHiddenDayHash = isHiddenDayHash;
  5743. },
  5744. // Is the current day hidden?
  5745. // `day` is a day-of-week index (0-6), or a Moment
  5746. isHiddenDay: function(day) {
  5747. if (moment.isMoment(day)) {
  5748. day = day.day();
  5749. }
  5750. return this.isHiddenDayHash[day];
  5751. },
  5752. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  5753. // If the initial value of `date` is not a hidden day, don't do anything.
  5754. // Pass `isExclusive` as `true` if you are dealing with an end date.
  5755. // `inc` defaults to `1` (increment one day forward each time)
  5756. skipHiddenDays: function(date, inc, isExclusive) {
  5757. var out = date.clone();
  5758. inc = inc || 1;
  5759. while (
  5760. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  5761. ) {
  5762. out.add(inc, 'days');
  5763. }
  5764. return out;
  5765. },
  5766. // Returns the date range of the full days the given range visually appears to occupy.
  5767. // Returns a new range object.
  5768. computeDayRange: function(range) {
  5769. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  5770. var end = range.end;
  5771. var endDay = null;
  5772. var endTimeMS;
  5773. if (end) {
  5774. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  5775. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  5776. // If the end time is actually inclusively part of the next day and is equal to or
  5777. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  5778. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  5779. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  5780. endDay.add(1, 'days');
  5781. }
  5782. }
  5783. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  5784. // assign the default duration of one day.
  5785. if (!end || endDay <= startDay) {
  5786. endDay = startDay.clone().add(1, 'days');
  5787. }
  5788. return { start: startDay, end: endDay };
  5789. },
  5790. // Does the given event visually appear to occupy more than one day?
  5791. isMultiDayEvent: function(event) {
  5792. var range = this.computeDayRange(event); // event is range-ish
  5793. return range.end.diff(range.start, 'days') > 1;
  5794. }
  5795. });
  5796. ;;
  5797. var Calendar = fc.Calendar = Class.extend({
  5798. dirDefaults: null, // option defaults related to LTR or RTL
  5799. langDefaults: null, // option defaults related to current locale
  5800. overrides: null, // option overrides given to the fullCalendar constructor
  5801. options: null, // all defaults combined with overrides
  5802. viewSpecCache: null, // cache of view definitions
  5803. view: null, // current View object
  5804. header: null,
  5805. loadingLevel: 0, // number of simultaneous loading tasks
  5806. // a lot of this class' OOP logic is scoped within this constructor function,
  5807. // but in the future, write individual methods on the prototype.
  5808. constructor: Calendar_constructor,
  5809. // Subclasses can override this for initialization logic after the constructor has been called
  5810. initialize: function() {
  5811. },
  5812. // Initializes `this.options` and other important options-related objects
  5813. initOptions: function(overrides) {
  5814. var lang, langDefaults;
  5815. var isRTL, dirDefaults;
  5816. // converts legacy options into non-legacy ones.
  5817. // in the future, when this is removed, don't use `overrides` reference. make a copy.
  5818. overrides = massageOverrides(overrides);
  5819. lang = overrides.lang;
  5820. langDefaults = langOptionHash[lang];
  5821. if (!langDefaults) {
  5822. lang = Calendar.defaults.lang;
  5823. langDefaults = langOptionHash[lang] || {};
  5824. }
  5825. isRTL = firstDefined(
  5826. overrides.isRTL,
  5827. langDefaults.isRTL,
  5828. Calendar.defaults.isRTL
  5829. );
  5830. dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  5831. this.dirDefaults = dirDefaults;
  5832. this.langDefaults = langDefaults;
  5833. this.overrides = overrides;
  5834. this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  5835. Calendar.defaults, // global defaults
  5836. dirDefaults,
  5837. langDefaults,
  5838. overrides
  5839. ]);
  5840. populateInstanceComputableOptions(this.options);
  5841. this.viewSpecCache = {}; // somewhat unrelated
  5842. },
  5843. // Gets information about how to create a view. Will use a cache.
  5844. getViewSpec: function(viewType) {
  5845. var cache = this.viewSpecCache;
  5846. return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  5847. },
  5848. // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  5849. // Preference is given to views that have corresponding buttons.
  5850. getUnitViewSpec: function(unit) {
  5851. var viewTypes;
  5852. var i;
  5853. var spec;
  5854. if ($.inArray(unit, intervalUnits) != -1) {
  5855. // put views that have buttons first. there will be duplicates, but oh well
  5856. viewTypes = this.header.getViewsWithButtons();
  5857. $.each(fc.views, function(viewType) { // all views
  5858. viewTypes.push(viewType);
  5859. });
  5860. for (i = 0; i < viewTypes.length; i++) {
  5861. spec = this.getViewSpec(viewTypes[i]);
  5862. if (spec) {
  5863. if (spec.singleUnit == unit) {
  5864. return spec;
  5865. }
  5866. }
  5867. }
  5868. }
  5869. },
  5870. // Builds an object with information on how to create a given view
  5871. buildViewSpec: function(requestedViewType) {
  5872. var viewOverrides = this.overrides.views || {};
  5873. var specChain = []; // for the view. lowest to highest priority
  5874. var defaultsChain = []; // for the view. lowest to highest priority
  5875. var overridesChain = []; // for the view. lowest to highest priority
  5876. var viewType = requestedViewType;
  5877. var spec; // for the view
  5878. var overrides; // for the view
  5879. var duration;
  5880. var unit;
  5881. // iterate from the specific view definition to a more general one until we hit an actual View class
  5882. while (viewType) {
  5883. spec = fcViews[viewType];
  5884. overrides = viewOverrides[viewType];
  5885. viewType = null; // clear. might repopulate for another iteration
  5886. if (typeof spec === 'function') { // TODO: deprecate
  5887. spec = { 'class': spec };
  5888. }
  5889. if (spec) {
  5890. specChain.unshift(spec);
  5891. defaultsChain.unshift(spec.defaults || {});
  5892. duration = duration || spec.duration;
  5893. viewType = viewType || spec.type;
  5894. }
  5895. if (overrides) {
  5896. overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  5897. duration = duration || overrides.duration;
  5898. viewType = viewType || overrides.type;
  5899. }
  5900. }
  5901. spec = mergeProps(specChain);
  5902. spec.type = requestedViewType;
  5903. if (!spec['class']) {
  5904. return false;
  5905. }
  5906. if (duration) {
  5907. duration = moment.duration(duration);
  5908. if (duration.valueOf()) { // valid?
  5909. spec.duration = duration;
  5910. unit = computeIntervalUnit(duration);
  5911. // view is a single-unit duration, like "week" or "day"
  5912. // incorporate options for this. lowest priority
  5913. if (duration.as(unit) === 1) {
  5914. spec.singleUnit = unit;
  5915. overridesChain.unshift(viewOverrides[unit] || {});
  5916. }
  5917. }
  5918. }
  5919. spec.defaults = mergeOptions(defaultsChain);
  5920. spec.overrides = mergeOptions(overridesChain);
  5921. this.buildViewSpecOptions(spec);
  5922. this.buildViewSpecButtonText(spec, requestedViewType);
  5923. return spec;
  5924. },
  5925. // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  5926. buildViewSpecOptions: function(spec) {
  5927. spec.options = mergeOptions([ // lowest to highest priority
  5928. Calendar.defaults, // global defaults
  5929. spec.defaults, // view's defaults (from ViewSubclass.defaults)
  5930. this.dirDefaults,
  5931. this.langDefaults, // locale and dir take precedence over view's defaults!
  5932. this.overrides, // calendar's overrides (options given to constructor)
  5933. spec.overrides // view's overrides (view-specific options)
  5934. ]);
  5935. populateInstanceComputableOptions(spec.options);
  5936. },
  5937. // Computes and assigns a view spec's buttonText-related options
  5938. buildViewSpecButtonText: function(spec, requestedViewType) {
  5939. // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  5940. // requested view, falling back to a generic unit entry like "week" or "day"
  5941. function queryButtonText(options) {
  5942. var buttonText = options.buttonText || {};
  5943. return buttonText[requestedViewType] ||
  5944. (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  5945. }
  5946. // highest to lowest priority
  5947. spec.buttonTextOverride =
  5948. queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  5949. spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  5950. // highest to lowest priority. mirrors buildViewSpecOptions
  5951. spec.buttonTextDefault =
  5952. queryButtonText(this.langDefaults) ||
  5953. queryButtonText(this.dirDefaults) ||
  5954. spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  5955. queryButtonText(Calendar.defaults) ||
  5956. (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  5957. requestedViewType; // fall back to given view name
  5958. },
  5959. // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  5960. instantiateView: function(viewType) {
  5961. var spec = this.getViewSpec(viewType);
  5962. return new spec['class'](this, viewType, spec.options, spec.duration);
  5963. },
  5964. // Returns a boolean about whether the view is okay to instantiate at some point
  5965. isValidViewType: function(viewType) {
  5966. return Boolean(this.getViewSpec(viewType));
  5967. },
  5968. // Should be called when any type of async data fetching begins
  5969. pushLoading: function() {
  5970. if (!(this.loadingLevel++)) {
  5971. this.trigger('loading', null, true, this.view);
  5972. }
  5973. },
  5974. // Should be called when any type of async data fetching completes
  5975. popLoading: function() {
  5976. if (!(--this.loadingLevel)) {
  5977. this.trigger('loading', null, false, this.view);
  5978. }
  5979. },
  5980. // Given arguments to the select method in the API, returns a range
  5981. buildSelectRange: function(start, end) {
  5982. start = this.moment(start);
  5983. if (end) {
  5984. end = this.moment(end);
  5985. }
  5986. else if (start.hasTime()) {
  5987. end = start.clone().add(this.defaultTimedEventDuration);
  5988. }
  5989. else {
  5990. end = start.clone().add(this.defaultAllDayEventDuration);
  5991. }
  5992. return { start: start, end: end };
  5993. }
  5994. });
  5995. function Calendar_constructor(element, overrides) {
  5996. var t = this;
  5997. t.initOptions(overrides || {});
  5998. var options = this.options;
  5999. // Exports
  6000. // -----------------------------------------------------------------------------------
  6001. t.render = render;
  6002. t.destroy = destroy;
  6003. t.refetchEvents = refetchEvents;
  6004. t.reportEvents = reportEvents;
  6005. t.reportEventChange = reportEventChange;
  6006. t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
  6007. t.changeView = renderView; // `renderView` will switch to another view
  6008. t.select = select;
  6009. t.unselect = unselect;
  6010. t.prev = prev;
  6011. t.next = next;
  6012. t.prevYear = prevYear;
  6013. t.nextYear = nextYear;
  6014. t.today = today;
  6015. t.gotoDate = gotoDate;
  6016. t.incrementDate = incrementDate;
  6017. t.zoomTo = zoomTo;
  6018. t.getDate = getDate;
  6019. t.getCalendar = getCalendar;
  6020. t.getView = getView;
  6021. t.option = option;
  6022. t.trigger = trigger;
  6023. // Language-data Internals
  6024. // -----------------------------------------------------------------------------------
  6025. // Apply overrides to the current language's data
  6026. var localeData = createObject( // make a cheap copy
  6027. getMomentLocaleData(options.lang) // will fall back to en
  6028. );
  6029. if (options.monthNames) {
  6030. localeData._months = options.monthNames;
  6031. }
  6032. if (options.monthNamesShort) {
  6033. localeData._monthsShort = options.monthNamesShort;
  6034. }
  6035. if (options.dayNames) {
  6036. localeData._weekdays = options.dayNames;
  6037. }
  6038. if (options.dayNamesShort) {
  6039. localeData._weekdaysShort = options.dayNamesShort;
  6040. }
  6041. if (options.firstDay != null) {
  6042. var _week = createObject(localeData._week); // _week: { dow: # }
  6043. _week.dow = options.firstDay;
  6044. localeData._week = _week;
  6045. }
  6046. // assign a normalized value, to be used by our .week() moment extension
  6047. localeData._fullCalendar_weekCalc = (function(weekCalc) {
  6048. if (typeof weekCalc === 'function') {
  6049. return weekCalc;
  6050. }
  6051. else if (weekCalc === 'local') {
  6052. return weekCalc;
  6053. }
  6054. else if (weekCalc === 'iso' || weekCalc === 'ISO') {
  6055. return 'ISO';
  6056. }
  6057. })(options.weekNumberCalculation);
  6058. // Calendar-specific Date Utilities
  6059. // -----------------------------------------------------------------------------------
  6060. t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  6061. t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  6062. // Builds a moment using the settings of the current calendar: timezone and language.
  6063. // Accepts anything the vanilla moment() constructor accepts.
  6064. t.moment = function() {
  6065. var mom;
  6066. if (options.timezone === 'local') {
  6067. mom = fc.moment.apply(null, arguments);
  6068. // Force the moment to be local, because fc.moment doesn't guarantee it.
  6069. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  6070. mom.local();
  6071. }
  6072. }
  6073. else if (options.timezone === 'UTC') {
  6074. mom = fc.moment.utc.apply(null, arguments); // process as UTC
  6075. }
  6076. else {
  6077. mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
  6078. }
  6079. if ('_locale' in mom) { // moment 2.8 and above
  6080. mom._locale = localeData;
  6081. }
  6082. else { // pre-moment-2.8
  6083. mom._lang = localeData;
  6084. }
  6085. return mom;
  6086. };
  6087. // Returns a boolean about whether or not the calendar knows how to calculate
  6088. // the timezone offset of arbitrary dates in the current timezone.
  6089. t.getIsAmbigTimezone = function() {
  6090. return options.timezone !== 'local' && options.timezone !== 'UTC';
  6091. };
  6092. // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  6093. // This will also give the date an unambiguous time.
  6094. t.rezoneDate = function(date) {
  6095. return t.moment(date.toArray());
  6096. };
  6097. // Returns a moment for the current date, as defined by the client's computer,
  6098. // or overridden by the `now` option.
  6099. t.getNow = function() {
  6100. var now = options.now;
  6101. if (typeof now === 'function') {
  6102. now = now();
  6103. }
  6104. return t.moment(now);
  6105. };
  6106. // Get an event's normalized end date. If not present, calculate it from the defaults.
  6107. t.getEventEnd = function(event) {
  6108. if (event.end) {
  6109. return event.end.clone();
  6110. }
  6111. else {
  6112. return t.getDefaultEventEnd(event.allDay, event.start);
  6113. }
  6114. };
  6115. // Given an event's allDay status and start date, return swhat its fallback end date should be.
  6116. t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  6117. var end = start.clone();
  6118. if (allDay) {
  6119. end.stripTime().add(t.defaultAllDayEventDuration);
  6120. }
  6121. else {
  6122. end.add(t.defaultTimedEventDuration);
  6123. }
  6124. if (t.getIsAmbigTimezone()) {
  6125. end.stripZone(); // we don't know what the tzo should be
  6126. }
  6127. return end;
  6128. };
  6129. // Produces a human-readable string for the given duration.
  6130. // Side-effect: changes the locale of the given duration.
  6131. t.humanizeDuration = function(duration) {
  6132. return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8
  6133. .humanize();
  6134. };
  6135. // Imports
  6136. // -----------------------------------------------------------------------------------
  6137. EventManager.call(t, options);
  6138. var isFetchNeeded = t.isFetchNeeded;
  6139. var fetchEvents = t.fetchEvents;
  6140. // Locals
  6141. // -----------------------------------------------------------------------------------
  6142. var _element = element[0];
  6143. var header;
  6144. var headerElement;
  6145. var content;
  6146. var tm; // for making theme classes
  6147. var currentView; // NOTE: keep this in sync with this.view
  6148. var viewsByType = {}; // holds all instantiated view instances, current or not
  6149. var suggestedViewHeight;
  6150. var windowResizeProxy; // wraps the windowResize function
  6151. var ignoreWindowResize = 0;
  6152. var date;
  6153. var events = [];
  6154. // Main Rendering
  6155. // -----------------------------------------------------------------------------------
  6156. if (options.defaultDate != null) {
  6157. date = t.moment(options.defaultDate);
  6158. }
  6159. else {
  6160. date = t.getNow();
  6161. }
  6162. function render() {
  6163. if (!content) {
  6164. initialRender();
  6165. }
  6166. else if (elementVisible()) {
  6167. // mainly for the public API
  6168. calcSize();
  6169. renderView();
  6170. }
  6171. }
  6172. function initialRender() {
  6173. tm = options.theme ? 'ui' : 'fc';
  6174. element.addClass('fc');
  6175. if (options.isRTL) {
  6176. element.addClass('fc-rtl');
  6177. }
  6178. else {
  6179. element.addClass('fc-ltr');
  6180. }
  6181. if (options.theme) {
  6182. element.addClass('ui-widget');
  6183. }
  6184. else {
  6185. element.addClass('fc-unthemed');
  6186. }
  6187. content = $("<div class='fc-view-container'/>").prependTo(element);
  6188. header = t.header = new Header(t, options);
  6189. headerElement = header.render();
  6190. if (headerElement) {
  6191. element.prepend(headerElement);
  6192. }
  6193. renderView(options.defaultView);
  6194. if (options.handleWindowResize) {
  6195. windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
  6196. $(window).resize(windowResizeProxy);
  6197. }
  6198. }
  6199. function destroy() {
  6200. if (currentView) {
  6201. currentView.removeElement();
  6202. // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  6203. // It is still the "current" view, just not rendered.
  6204. }
  6205. header.removeElement();
  6206. content.remove();
  6207. element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  6208. if (windowResizeProxy) {
  6209. $(window).unbind('resize', windowResizeProxy);
  6210. }
  6211. }
  6212. function elementVisible() {
  6213. return element.is(':visible');
  6214. }
  6215. // View Rendering
  6216. // -----------------------------------------------------------------------------------
  6217. // Renders a view because of a date change, view-type change, or for the first time.
  6218. // If not given a viewType, keep the current view but render different dates.
  6219. function renderView(viewType) {
  6220. ignoreWindowResize++;
  6221. // if viewType is changing, remove the old view's rendering
  6222. if (currentView && viewType && currentView.type !== viewType) {
  6223. header.deactivateButton(currentView.type);
  6224. freezeContentHeight(); // prevent a scroll jump when view element is removed
  6225. currentView.removeElement();
  6226. currentView = t.view = null;
  6227. }
  6228. // if viewType changed, or the view was never created, create a fresh view
  6229. if (!currentView && viewType) {
  6230. currentView = t.view =
  6231. viewsByType[viewType] ||
  6232. (viewsByType[viewType] = t.instantiateView(viewType));
  6233. currentView.setElement(
  6234. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  6235. );
  6236. header.activateButton(viewType);
  6237. }
  6238. if (currentView) {
  6239. // in case the view should render a period of time that is completely hidden
  6240. date = currentView.massageCurrentDate(date);
  6241. // render or rerender the view
  6242. if (
  6243. !currentView.displaying ||
  6244. !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  6245. ) {
  6246. if (elementVisible()) {
  6247. freezeContentHeight();
  6248. currentView.display(date);
  6249. unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
  6250. // need to do this after View::render, so dates are calculated
  6251. updateHeaderTitle();
  6252. updateTodayButton();
  6253. getAndRenderEvents();
  6254. }
  6255. }
  6256. }
  6257. unfreezeContentHeight(); // undo any lone freezeContentHeight calls
  6258. ignoreWindowResize--;
  6259. }
  6260. // Resizing
  6261. // -----------------------------------------------------------------------------------
  6262. t.getSuggestedViewHeight = function() {
  6263. if (suggestedViewHeight === undefined) {
  6264. calcSize();
  6265. }
  6266. return suggestedViewHeight;
  6267. };
  6268. t.isHeightAuto = function() {
  6269. return options.contentHeight === 'auto' || options.height === 'auto';
  6270. };
  6271. function updateSize(shouldRecalc) {
  6272. if (elementVisible()) {
  6273. if (shouldRecalc) {
  6274. _calcSize();
  6275. }
  6276. ignoreWindowResize++;
  6277. currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  6278. ignoreWindowResize--;
  6279. return true; // signal success
  6280. }
  6281. }
  6282. function calcSize() {
  6283. if (elementVisible()) {
  6284. _calcSize();
  6285. }
  6286. }
  6287. function _calcSize() { // assumes elementVisible
  6288. if (typeof options.contentHeight === 'number') { // exists and not 'auto'
  6289. suggestedViewHeight = options.contentHeight;
  6290. }
  6291. else if (typeof options.height === 'number') { // exists and not 'auto'
  6292. suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
  6293. }
  6294. else {
  6295. suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  6296. }
  6297. }
  6298. function windowResize(ev) {
  6299. if (
  6300. !ignoreWindowResize &&
  6301. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  6302. currentView.start // view has already been rendered
  6303. ) {
  6304. if (updateSize(true)) {
  6305. currentView.trigger('windowResize', _element);
  6306. }
  6307. }
  6308. }
  6309. /* Event Fetching/Rendering
  6310. -----------------------------------------------------------------------------*/
  6311. // TODO: going forward, most of this stuff should be directly handled by the view
  6312. function refetchEvents() { // can be called as an API method
  6313. destroyEvents(); // so that events are cleared before user starts waiting for AJAX
  6314. fetchAndRenderEvents();
  6315. }
  6316. function renderEvents() { // destroys old events if previously rendered
  6317. if (elementVisible()) {
  6318. freezeContentHeight();
  6319. currentView.displayEvents(events);
  6320. unfreezeContentHeight();
  6321. }
  6322. }
  6323. function destroyEvents() {
  6324. freezeContentHeight();
  6325. currentView.clearEvents();
  6326. unfreezeContentHeight();
  6327. }
  6328. function getAndRenderEvents() {
  6329. if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  6330. fetchAndRenderEvents();
  6331. }
  6332. else {
  6333. renderEvents();
  6334. }
  6335. }
  6336. function fetchAndRenderEvents() {
  6337. fetchEvents(currentView.start, currentView.end);
  6338. // ... will call reportEvents
  6339. // ... which will call renderEvents
  6340. }
  6341. // called when event data arrives
  6342. function reportEvents(_events) {
  6343. events = _events;
  6344. renderEvents();
  6345. }
  6346. // called when a single event's data has been changed
  6347. function reportEventChange() {
  6348. renderEvents();
  6349. }
  6350. /* Header Updating
  6351. -----------------------------------------------------------------------------*/
  6352. function updateHeaderTitle() {
  6353. header.updateTitle(currentView.title);
  6354. }
  6355. function updateTodayButton() {
  6356. var now = t.getNow();
  6357. if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  6358. header.disableButton('today');
  6359. }
  6360. else {
  6361. header.enableButton('today');
  6362. }
  6363. }
  6364. /* Selection
  6365. -----------------------------------------------------------------------------*/
  6366. function select(start, end) {
  6367. currentView.select(
  6368. t.buildSelectRange.apply(t, arguments)
  6369. );
  6370. }
  6371. function unselect() { // safe to be called before renderView
  6372. if (currentView) {
  6373. currentView.unselect();
  6374. }
  6375. }
  6376. /* Date
  6377. -----------------------------------------------------------------------------*/
  6378. function prev() {
  6379. date = currentView.computePrevDate(date);
  6380. renderView();
  6381. }
  6382. function next() {
  6383. date = currentView.computeNextDate(date);
  6384. renderView();
  6385. }
  6386. function prevYear() {
  6387. date.add(-1, 'years');
  6388. renderView();
  6389. }
  6390. function nextYear() {
  6391. date.add(1, 'years');
  6392. renderView();
  6393. }
  6394. function today() {
  6395. date = t.getNow();
  6396. renderView();
  6397. }
  6398. function gotoDate(dateInput) {
  6399. date = t.moment(dateInput);
  6400. renderView();
  6401. }
  6402. function incrementDate(delta) {
  6403. date.add(moment.duration(delta));
  6404. renderView();
  6405. }
  6406. // Forces navigation to a view for the given date.
  6407. // `viewType` can be a specific view name or a generic one like "week" or "day".
  6408. function zoomTo(newDate, viewType) {
  6409. var spec;
  6410. viewType = viewType || 'day'; // day is default zoom
  6411. spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  6412. date = newDate;
  6413. renderView(spec ? spec.type : null);
  6414. }
  6415. function getDate() {
  6416. return date.clone();
  6417. }
  6418. /* Height "Freezing"
  6419. -----------------------------------------------------------------------------*/
  6420. // TODO: move this into the view
  6421. function freezeContentHeight() {
  6422. content.css({
  6423. width: '100%',
  6424. height: content.height(),
  6425. overflow: 'hidden'
  6426. });
  6427. }
  6428. function unfreezeContentHeight() {
  6429. content.css({
  6430. width: '',
  6431. height: '',
  6432. overflow: ''
  6433. });
  6434. }
  6435. /* Misc
  6436. -----------------------------------------------------------------------------*/
  6437. function getCalendar() {
  6438. return t;
  6439. }
  6440. function getView() {
  6441. return currentView;
  6442. }
  6443. function option(name, value) {
  6444. if (value === undefined) {
  6445. return options[name];
  6446. }
  6447. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  6448. options[name] = value;
  6449. updateSize(true); // true = allow recalculation of height
  6450. }
  6451. }
  6452. function trigger(name, thisObj) {
  6453. if (options[name]) {
  6454. return options[name].apply(
  6455. thisObj || _element,
  6456. Array.prototype.slice.call(arguments, 2)
  6457. );
  6458. }
  6459. }
  6460. t.initialize();
  6461. }
  6462. ;;
  6463. Calendar.defaults = {
  6464. titleRangeSeparator: ' \u2014 ', // emphasized dash
  6465. monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
  6466. defaultTimedEventDuration: '02:00:00',
  6467. defaultAllDayEventDuration: { days: 1 },
  6468. forceEventDuration: false,
  6469. nextDayThreshold: '09:00:00', // 9am
  6470. // display
  6471. defaultView: 'month',
  6472. aspectRatio: 1.35,
  6473. header: {
  6474. left: 'title',
  6475. center: '',
  6476. right: 'today prev,next'
  6477. },
  6478. weekends: true,
  6479. weekNumbers: false,
  6480. weekNumberTitle: 'W',
  6481. weekNumberCalculation: 'local',
  6482. //editable: false,
  6483. scrollTime: '06:00:00',
  6484. // event ajax
  6485. lazyFetching: true,
  6486. startParam: 'start',
  6487. endParam: 'end',
  6488. timezoneParam: 'timezone',
  6489. timezone: false,
  6490. //allDayDefault: undefined,
  6491. // locale
  6492. isRTL: false,
  6493. buttonText: {
  6494. prev: "prev",
  6495. next: "next",
  6496. prevYear: "prev year",
  6497. nextYear: "next year",
  6498. year: 'year', // TODO: locale files need to specify this
  6499. today: 'today',
  6500. month: 'month',
  6501. week: 'week',
  6502. day: 'day'
  6503. },
  6504. buttonIcons: {
  6505. prev: 'left-single-arrow',
  6506. next: 'right-single-arrow',
  6507. prevYear: 'left-double-arrow',
  6508. nextYear: 'right-double-arrow'
  6509. },
  6510. // jquery-ui theming
  6511. theme: false,
  6512. themeButtonIcons: {
  6513. prev: 'circle-triangle-w',
  6514. next: 'circle-triangle-e',
  6515. prevYear: 'seek-prev',
  6516. nextYear: 'seek-next'
  6517. },
  6518. //eventResizableFromStart: false,
  6519. dragOpacity: .75,
  6520. dragRevertDuration: 500,
  6521. dragScroll: true,
  6522. //selectable: false,
  6523. unselectAuto: true,
  6524. dropAccept: '*',
  6525. eventLimit: false,
  6526. eventLimitText: 'more',
  6527. eventLimitClick: 'popover',
  6528. dayPopoverFormat: 'LL',
  6529. handleWindowResize: true,
  6530. windowResizeDelay: 200 // milliseconds before an updateSize happens
  6531. };
  6532. Calendar.englishDefaults = { // used by lang.js
  6533. dayPopoverFormat: 'dddd, MMMM D'
  6534. };
  6535. Calendar.rtlDefaults = { // right-to-left defaults
  6536. header: { // TODO: smarter solution (first/center/last ?)
  6537. left: 'next,prev today',
  6538. center: '',
  6539. right: 'title'
  6540. },
  6541. buttonIcons: {
  6542. prev: 'right-single-arrow',
  6543. next: 'left-single-arrow',
  6544. prevYear: 'right-double-arrow',
  6545. nextYear: 'left-double-arrow'
  6546. },
  6547. themeButtonIcons: {
  6548. prev: 'circle-triangle-e',
  6549. next: 'circle-triangle-w',
  6550. nextYear: 'seek-prev',
  6551. prevYear: 'seek-next'
  6552. }
  6553. };
  6554. ;;
  6555. var langOptionHash = fc.langs = {}; // initialize and expose
  6556. // TODO: document the structure and ordering of a FullCalendar lang file
  6557. // TODO: rename everything "lang" to "locale", like what the moment project did
  6558. // Initialize jQuery UI datepicker translations while using some of the translations
  6559. // Will set this as the default language for datepicker.
  6560. fc.datepickerLang = function(langCode, dpLangCode, dpOptions) {
  6561. // get the FullCalendar internal option hash for this language. create if necessary
  6562. var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  6563. // transfer some simple options from datepicker to fc
  6564. fcOptions.isRTL = dpOptions.isRTL;
  6565. fcOptions.weekNumberTitle = dpOptions.weekHeader;
  6566. // compute some more complex options from datepicker
  6567. $.each(dpComputableOptions, function(name, func) {
  6568. fcOptions[name] = func(dpOptions);
  6569. });
  6570. // is jQuery UI Datepicker is on the page?
  6571. if ($.datepicker) {
  6572. // Register the language data.
  6573. // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
  6574. // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
  6575. // Make an alias so the language can be referenced either way.
  6576. $.datepicker.regional[dpLangCode] =
  6577. $.datepicker.regional[langCode] = // alias
  6578. dpOptions;
  6579. // Alias 'en' to the default language data. Do this every time.
  6580. $.datepicker.regional.en = $.datepicker.regional[''];
  6581. // Set as Datepicker's global defaults.
  6582. $.datepicker.setDefaults(dpOptions);
  6583. }
  6584. };
  6585. // Sets FullCalendar-specific translations. Will set the language as the global default.
  6586. fc.lang = function(langCode, newFcOptions) {
  6587. var fcOptions;
  6588. var momOptions;
  6589. // get the FullCalendar internal option hash for this language. create if necessary
  6590. fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  6591. // provided new options for this language? merge them in
  6592. if (newFcOptions) {
  6593. fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
  6594. }
  6595. // compute language options that weren't defined.
  6596. // always do this. newFcOptions can be undefined when initializing from i18n file,
  6597. // so no way to tell if this is an initialization or a default-setting.
  6598. momOptions = getMomentLocaleData(langCode); // will fall back to en
  6599. $.each(momComputableOptions, function(name, func) {
  6600. if (fcOptions[name] == null) {
  6601. fcOptions[name] = func(momOptions, fcOptions);
  6602. }
  6603. });
  6604. // set it as the default language for FullCalendar
  6605. Calendar.defaults.lang = langCode;
  6606. };
  6607. // NOTE: can't guarantee any of these computations will run because not every language has datepicker
  6608. // configs, so make sure there are English fallbacks for these in the defaults file.
  6609. var dpComputableOptions = {
  6610. buttonText: function(dpOptions) {
  6611. return {
  6612. // the translations sometimes wrongly contain HTML entities
  6613. prev: stripHtmlEntities(dpOptions.prevText),
  6614. next: stripHtmlEntities(dpOptions.nextText),
  6615. today: stripHtmlEntities(dpOptions.currentText)
  6616. };
  6617. },
  6618. // Produces format strings like "MMMM YYYY" -> "September 2014"
  6619. monthYearFormat: function(dpOptions) {
  6620. return dpOptions.showMonthAfterYear ?
  6621. 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
  6622. 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
  6623. }
  6624. };
  6625. var momComputableOptions = {
  6626. // Produces format strings like "ddd M/D" -> "Fri 9/15"
  6627. dayOfMonthFormat: function(momOptions, fcOptions) {
  6628. var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
  6629. // strip the year off the edge, as well as other misc non-whitespace chars
  6630. format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
  6631. if (fcOptions.isRTL) {
  6632. format += ' ddd'; // for RTL, add day-of-week to end
  6633. }
  6634. else {
  6635. format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  6636. }
  6637. return format;
  6638. },
  6639. // Produces format strings like "h:mma" -> "6:00pm"
  6640. mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
  6641. return momOptions.longDateFormat('LT')
  6642. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6643. },
  6644. // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
  6645. smallTimeFormat: function(momOptions) {
  6646. return momOptions.longDateFormat('LT')
  6647. .replace(':mm', '(:mm)')
  6648. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  6649. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6650. },
  6651. // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
  6652. extraSmallTimeFormat: function(momOptions) {
  6653. return momOptions.longDateFormat('LT')
  6654. .replace(':mm', '(:mm)')
  6655. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  6656. .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  6657. },
  6658. // Produces format strings like "ha" / "H" -> "6pm" / "18"
  6659. hourFormat: function(momOptions) {
  6660. return momOptions.longDateFormat('LT')
  6661. .replace(':mm', '')
  6662. .replace(/(\Wmm)$/, '') // like above, but for foreign langs
  6663. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6664. },
  6665. // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
  6666. noMeridiemTimeFormat: function(momOptions) {
  6667. return momOptions.longDateFormat('LT')
  6668. .replace(/\s*a$/i, ''); // remove trailing AM/PM
  6669. }
  6670. };
  6671. // options that should be computed off live calendar options (considers override options)
  6672. var instanceComputableOptions = { // TODO: best place for this? related to lang?
  6673. // Produces format strings for results like "Mo 16"
  6674. smallDayDateFormat: function(options) {
  6675. return options.isRTL ?
  6676. 'D dd' :
  6677. 'dd D';
  6678. },
  6679. // Produces format strings for results like "Wk 5"
  6680. weekFormat: function(options) {
  6681. return options.isRTL ?
  6682. 'w[ ' + options.weekNumberTitle + ']' :
  6683. '[' + options.weekNumberTitle + ' ]w';
  6684. },
  6685. // Produces format strings for results like "Wk5"
  6686. smallWeekFormat: function(options) {
  6687. return options.isRTL ?
  6688. 'w[' + options.weekNumberTitle + ']' :
  6689. '[' + options.weekNumberTitle + ']w';
  6690. }
  6691. };
  6692. function populateInstanceComputableOptions(options) {
  6693. $.each(instanceComputableOptions, function(name, func) {
  6694. if (options[name] == null) {
  6695. options[name] = func(options);
  6696. }
  6697. });
  6698. }
  6699. // Returns moment's internal locale data. If doesn't exist, returns English.
  6700. // Works with moment-pre-2.8
  6701. function getMomentLocaleData(langCode) {
  6702. var func = moment.localeData || moment.langData;
  6703. return func.call(moment, langCode) ||
  6704. func.call(moment, 'en'); // the newer localData could return null, so fall back to en
  6705. }
  6706. // Initialize English by forcing computation of moment-derived options.
  6707. // Also, sets it as the default.
  6708. fc.lang('en', Calendar.englishDefaults);
  6709. ;;
  6710. /* Top toolbar area with buttons and title
  6711. ----------------------------------------------------------------------------------------------------------------------*/
  6712. // TODO: rename all header-related things to "toolbar"
  6713. function Header(calendar, options) {
  6714. var t = this;
  6715. // exports
  6716. t.render = render;
  6717. t.removeElement = removeElement;
  6718. t.updateTitle = updateTitle;
  6719. t.activateButton = activateButton;
  6720. t.deactivateButton = deactivateButton;
  6721. t.disableButton = disableButton;
  6722. t.enableButton = enableButton;
  6723. t.getViewsWithButtons = getViewsWithButtons;
  6724. // locals
  6725. var el = $();
  6726. var viewsWithButtons = [];
  6727. var tm;
  6728. function render() {
  6729. var sections = options.header;
  6730. tm = options.theme ? 'ui' : 'fc';
  6731. if (sections) {
  6732. el = $("<div class='fc-toolbar'/>")
  6733. .append(renderSection('left'))
  6734. .append(renderSection('right'))
  6735. .append(renderSection('center'))
  6736. .append('<div class="fc-clear"/>');
  6737. return el;
  6738. }
  6739. }
  6740. function removeElement() {
  6741. el.remove();
  6742. el = $();
  6743. }
  6744. function renderSection(position) {
  6745. var sectionEl = $('<div class="fc-' + position + '"/>');
  6746. var buttonStr = options.header[position];
  6747. if (buttonStr) {
  6748. $.each(buttonStr.split(' '), function(i) {
  6749. var groupChildren = $();
  6750. var isOnlyButtons = true;
  6751. var groupEl;
  6752. $.each(this.split(','), function(j, buttonName) {
  6753. var viewSpec;
  6754. var buttonClick;
  6755. var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
  6756. var defaultText;
  6757. var themeIcon;
  6758. var normalIcon;
  6759. var innerHtml;
  6760. var classes;
  6761. var button;
  6762. if (buttonName == 'title') {
  6763. groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  6764. isOnlyButtons = false;
  6765. }
  6766. else {
  6767. viewSpec = calendar.getViewSpec(buttonName);
  6768. if (viewSpec) {
  6769. buttonClick = function() {
  6770. calendar.changeView(buttonName);
  6771. };
  6772. viewsWithButtons.push(buttonName);
  6773. overrideText = viewSpec.buttonTextOverride;
  6774. defaultText = viewSpec.buttonTextDefault;
  6775. }
  6776. else if (calendar[buttonName]) { // a calendar method
  6777. buttonClick = function() {
  6778. calendar[buttonName]();
  6779. };
  6780. overrideText = (calendar.overrides.buttonText || {})[buttonName];
  6781. defaultText = options.buttonText[buttonName]; // everything else is considered default
  6782. }
  6783. if (buttonClick) {
  6784. themeIcon = options.themeButtonIcons[buttonName];
  6785. normalIcon = options.buttonIcons[buttonName];
  6786. if (overrideText) {
  6787. innerHtml = htmlEscape(overrideText);
  6788. }
  6789. else if (themeIcon && options.theme) {
  6790. innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  6791. }
  6792. else if (normalIcon && !options.theme) {
  6793. innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  6794. }
  6795. else {
  6796. innerHtml = htmlEscape(defaultText);
  6797. }
  6798. classes = [
  6799. 'fc-' + buttonName + '-button',
  6800. tm + '-button',
  6801. tm + '-state-default'
  6802. ];
  6803. button = $( // type="button" so that it doesn't submit a form
  6804. '<button type="button" class="' + classes.join(' ') + '">' +
  6805. innerHtml +
  6806. '</button>'
  6807. )
  6808. .click(function() {
  6809. // don't process clicks for disabled buttons
  6810. if (!button.hasClass(tm + '-state-disabled')) {
  6811. buttonClick();
  6812. // after the click action, if the button becomes the "active" tab, or disabled,
  6813. // it should never have a hover class, so remove it now.
  6814. if (
  6815. button.hasClass(tm + '-state-active') ||
  6816. button.hasClass(tm + '-state-disabled')
  6817. ) {
  6818. button.removeClass(tm + '-state-hover');
  6819. }
  6820. }
  6821. })
  6822. .mousedown(function() {
  6823. // the *down* effect (mouse pressed in).
  6824. // only on buttons that are not the "active" tab, or disabled
  6825. button
  6826. .not('.' + tm + '-state-active')
  6827. .not('.' + tm + '-state-disabled')
  6828. .addClass(tm + '-state-down');
  6829. })
  6830. .mouseup(function() {
  6831. // undo the *down* effect
  6832. button.removeClass(tm + '-state-down');
  6833. })
  6834. .hover(
  6835. function() {
  6836. // the *hover* effect.
  6837. // only on buttons that are not the "active" tab, or disabled
  6838. button
  6839. .not('.' + tm + '-state-active')
  6840. .not('.' + tm + '-state-disabled')
  6841. .addClass(tm + '-state-hover');
  6842. },
  6843. function() {
  6844. // undo the *hover* effect
  6845. button
  6846. .removeClass(tm + '-state-hover')
  6847. .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  6848. }
  6849. );
  6850. groupChildren = groupChildren.add(button);
  6851. }
  6852. }
  6853. });
  6854. if (isOnlyButtons) {
  6855. groupChildren
  6856. .first().addClass(tm + '-corner-left').end()
  6857. .last().addClass(tm + '-corner-right').end();
  6858. }
  6859. if (groupChildren.length > 1) {
  6860. groupEl = $('<div/>');
  6861. if (isOnlyButtons) {
  6862. groupEl.addClass('fc-button-group');
  6863. }
  6864. groupEl.append(groupChildren);
  6865. sectionEl.append(groupEl);
  6866. }
  6867. else {
  6868. sectionEl.append(groupChildren); // 1 or 0 children
  6869. }
  6870. });
  6871. }
  6872. return sectionEl;
  6873. }
  6874. function updateTitle(text) {
  6875. el.find('h2').text(text);
  6876. }
  6877. function activateButton(buttonName) {
  6878. el.find('.fc-' + buttonName + '-button')
  6879. .addClass(tm + '-state-active');
  6880. }
  6881. function deactivateButton(buttonName) {
  6882. el.find('.fc-' + buttonName + '-button')
  6883. .removeClass(tm + '-state-active');
  6884. }
  6885. function disableButton(buttonName) {
  6886. el.find('.fc-' + buttonName + '-button')
  6887. .attr('disabled', 'disabled')
  6888. .addClass(tm + '-state-disabled');
  6889. }
  6890. function enableButton(buttonName) {
  6891. el.find('.fc-' + buttonName + '-button')
  6892. .removeAttr('disabled')
  6893. .removeClass(tm + '-state-disabled');
  6894. }
  6895. function getViewsWithButtons() {
  6896. return viewsWithButtons;
  6897. }
  6898. }
  6899. ;;
  6900. fc.sourceNormalizers = [];
  6901. fc.sourceFetchers = [];
  6902. var ajaxDefaults = {
  6903. dataType: 'json',
  6904. cache: false
  6905. };
  6906. var eventGUID = 1;
  6907. function EventManager(options) { // assumed to be a calendar
  6908. var t = this;
  6909. // exports
  6910. t.isFetchNeeded = isFetchNeeded;
  6911. t.fetchEvents = fetchEvents;
  6912. t.addEventSource = addEventSource;
  6913. t.removeEventSource = removeEventSource;
  6914. t.updateEvent = updateEvent;
  6915. t.renderEvent = renderEvent;
  6916. t.removeEvents = removeEvents;
  6917. t.clientEvents = clientEvents;
  6918. t.mutateEvent = mutateEvent;
  6919. t.normalizeEventRange = normalizeEventRange;
  6920. t.normalizeEventRangeTimes = normalizeEventRangeTimes;
  6921. t.ensureVisibleEventRange = ensureVisibleEventRange;
  6922. // imports
  6923. var reportEvents = t.reportEvents;
  6924. // locals
  6925. var stickySource = { events: [] };
  6926. var sources = [ stickySource ];
  6927. var rangeStart, rangeEnd;
  6928. var currentFetchID = 0;
  6929. var pendingSourceCnt = 0;
  6930. var cache = []; // holds events that have already been expanded
  6931. $.each(
  6932. (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  6933. function(i, sourceInput) {
  6934. var source = buildEventSource(sourceInput);
  6935. if (source) {
  6936. sources.push(source);
  6937. }
  6938. }
  6939. );
  6940. /* Fetching
  6941. -----------------------------------------------------------------------------*/
  6942. function isFetchNeeded(start, end) {
  6943. return !rangeStart || // nothing has been fetched yet?
  6944. // or, a part of the new range is outside of the old range? (after normalizing)
  6945. start.clone().stripZone() < rangeStart.clone().stripZone() ||
  6946. end.clone().stripZone() > rangeEnd.clone().stripZone();
  6947. }
  6948. function fetchEvents(start, end) {
  6949. rangeStart = start;
  6950. rangeEnd = end;
  6951. cache = [];
  6952. var fetchID = ++currentFetchID;
  6953. var len = sources.length;
  6954. pendingSourceCnt = len;
  6955. for (var i=0; i<len; i++) {
  6956. fetchEventSource(sources[i], fetchID);
  6957. }
  6958. }
  6959. function fetchEventSource(source, fetchID) {
  6960. _fetchEventSource(source, function(eventInputs) {
  6961. var isArraySource = $.isArray(source.events);
  6962. var i, eventInput;
  6963. var abstractEvent;
  6964. if (fetchID == currentFetchID) {
  6965. if (eventInputs) {
  6966. for (i = 0; i < eventInputs.length; i++) {
  6967. eventInput = eventInputs[i];
  6968. if (isArraySource) { // array sources have already been convert to Event Objects
  6969. abstractEvent = eventInput;
  6970. }
  6971. else {
  6972. abstractEvent = buildEventFromInput(eventInput, source);
  6973. }
  6974. if (abstractEvent) { // not false (an invalid event)
  6975. cache.push.apply(
  6976. cache,
  6977. expandEvent(abstractEvent) // add individual expanded events to the cache
  6978. );
  6979. }
  6980. }
  6981. }
  6982. pendingSourceCnt--;
  6983. if (!pendingSourceCnt) {
  6984. reportEvents(cache);
  6985. }
  6986. }
  6987. });
  6988. }
  6989. function _fetchEventSource(source, callback) {
  6990. var i;
  6991. var fetchers = fc.sourceFetchers;
  6992. var res;
  6993. for (i=0; i<fetchers.length; i++) {
  6994. res = fetchers[i].call(
  6995. t, // this, the Calendar object
  6996. source,
  6997. rangeStart.clone(),
  6998. rangeEnd.clone(),
  6999. options.timezone,
  7000. callback
  7001. );
  7002. if (res === true) {
  7003. // the fetcher is in charge. made its own async request
  7004. return;
  7005. }
  7006. else if (typeof res == 'object') {
  7007. // the fetcher returned a new source. process it
  7008. _fetchEventSource(res, callback);
  7009. return;
  7010. }
  7011. }
  7012. var events = source.events;
  7013. if (events) {
  7014. if ($.isFunction(events)) {
  7015. t.pushLoading();
  7016. events.call(
  7017. t, // this, the Calendar object
  7018. rangeStart.clone(),
  7019. rangeEnd.clone(),
  7020. options.timezone,
  7021. function(events) {
  7022. callback(events);
  7023. t.popLoading();
  7024. }
  7025. );
  7026. }
  7027. else if ($.isArray(events)) {
  7028. callback(events);
  7029. }
  7030. else {
  7031. callback();
  7032. }
  7033. }else{
  7034. var url = source.url;
  7035. if (url) {
  7036. var success = source.success;
  7037. var error = source.error;
  7038. var complete = source.complete;
  7039. // retrieve any outbound GET/POST $.ajax data from the options
  7040. var customData;
  7041. if ($.isFunction(source.data)) {
  7042. // supplied as a function that returns a key/value object
  7043. customData = source.data();
  7044. }
  7045. else {
  7046. // supplied as a straight key/value object
  7047. customData = source.data;
  7048. }
  7049. // use a copy of the custom data so we can modify the parameters
  7050. // and not affect the passed-in object.
  7051. var data = $.extend({}, customData || {});
  7052. var startParam = firstDefined(source.startParam, options.startParam);
  7053. var endParam = firstDefined(source.endParam, options.endParam);
  7054. var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  7055. if (startParam) {
  7056. data[startParam] = rangeStart.format();
  7057. }
  7058. if (endParam) {
  7059. data[endParam] = rangeEnd.format();
  7060. }
  7061. if (options.timezone && options.timezone != 'local') {
  7062. data[timezoneParam] = options.timezone;
  7063. }
  7064. t.pushLoading();
  7065. $.ajax($.extend({}, ajaxDefaults, source, {
  7066. data: data,
  7067. success: function(events) {
  7068. events = events || [];
  7069. var res = applyAll(success, this, arguments);
  7070. if ($.isArray(res)) {
  7071. events = res;
  7072. }
  7073. callback(events);
  7074. },
  7075. error: function() {
  7076. applyAll(error, this, arguments);
  7077. callback();
  7078. },
  7079. complete: function() {
  7080. applyAll(complete, this, arguments);
  7081. t.popLoading();
  7082. }
  7083. }));
  7084. }else{
  7085. callback();
  7086. }
  7087. }
  7088. }
  7089. /* Sources
  7090. -----------------------------------------------------------------------------*/
  7091. function addEventSource(sourceInput) {
  7092. var source = buildEventSource(sourceInput);
  7093. if (source) {
  7094. sources.push(source);
  7095. pendingSourceCnt++;
  7096. fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  7097. }
  7098. }
  7099. function buildEventSource(sourceInput) { // will return undefined if invalid source
  7100. var normalizers = fc.sourceNormalizers;
  7101. var source;
  7102. var i;
  7103. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  7104. source = { events: sourceInput };
  7105. }
  7106. else if (typeof sourceInput === 'string') {
  7107. source = { url: sourceInput };
  7108. }
  7109. else if (typeof sourceInput === 'object') {
  7110. source = $.extend({}, sourceInput); // shallow copy
  7111. }
  7112. if (source) {
  7113. // TODO: repeat code, same code for event classNames
  7114. if (source.className) {
  7115. if (typeof source.className === 'string') {
  7116. source.className = source.className.split(/\s+/);
  7117. }
  7118. // otherwise, assumed to be an array
  7119. }
  7120. else {
  7121. source.className = [];
  7122. }
  7123. // for array sources, we convert to standard Event Objects up front
  7124. if ($.isArray(source.events)) {
  7125. source.origArray = source.events; // for removeEventSource
  7126. source.events = $.map(source.events, function(eventInput) {
  7127. return buildEventFromInput(eventInput, source);
  7128. });
  7129. }
  7130. for (i=0; i<normalizers.length; i++) {
  7131. normalizers[i].call(t, source);
  7132. }
  7133. return source;
  7134. }
  7135. }
  7136. function removeEventSource(source) {
  7137. sources = $.grep(sources, function(src) {
  7138. return !isSourcesEqual(src, source);
  7139. });
  7140. // remove all client events from that source
  7141. cache = $.grep(cache, function(e) {
  7142. return !isSourcesEqual(e.source, source);
  7143. });
  7144. reportEvents(cache);
  7145. }
  7146. function isSourcesEqual(source1, source2) {
  7147. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  7148. }
  7149. function getSourcePrimitive(source) {
  7150. return (
  7151. (typeof source === 'object') ? // a normalized event source?
  7152. (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  7153. null
  7154. ) ||
  7155. source; // the given argument *is* the primitive
  7156. }
  7157. /* Manipulation
  7158. -----------------------------------------------------------------------------*/
  7159. // Only ever called from the externally-facing API
  7160. function updateEvent(event) {
  7161. // massage start/end values, even if date string values
  7162. event.start = t.moment(event.start);
  7163. if (event.end) {
  7164. event.end = t.moment(event.end);
  7165. }
  7166. else {
  7167. event.end = null;
  7168. }
  7169. mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  7170. reportEvents(cache); // reports event modifications (so we can redraw)
  7171. }
  7172. // Returns a hash of misc event properties that should be copied over to related events.
  7173. function getMiscEventProps(event) {
  7174. var props = {};
  7175. $.each(event, function(name, val) {
  7176. if (isMiscEventPropName(name)) {
  7177. if (val !== undefined && isAtomic(val)) { // a defined non-object
  7178. props[name] = val;
  7179. }
  7180. }
  7181. });
  7182. return props;
  7183. }
  7184. // non-date-related, non-id-related, non-secret
  7185. function isMiscEventPropName(name) {
  7186. return !/^_|^(id|allDay|start|end)$/.test(name);
  7187. }
  7188. // returns the expanded events that were created
  7189. function renderEvent(eventInput, stick) {
  7190. var abstractEvent = buildEventFromInput(eventInput);
  7191. var events;
  7192. var i, event;
  7193. if (abstractEvent) { // not false (a valid input)
  7194. events = expandEvent(abstractEvent);
  7195. for (i = 0; i < events.length; i++) {
  7196. event = events[i];
  7197. if (!event.source) {
  7198. if (stick) {
  7199. stickySource.events.push(event);
  7200. event.source = stickySource;
  7201. }
  7202. cache.push(event);
  7203. }
  7204. }
  7205. reportEvents(cache);
  7206. return events;
  7207. }
  7208. return [];
  7209. }
  7210. function removeEvents(filter) {
  7211. var eventID;
  7212. var i;
  7213. if (filter == null) { // null or undefined. remove all events
  7214. filter = function() { return true; }; // will always match
  7215. }
  7216. else if (!$.isFunction(filter)) { // an event ID
  7217. eventID = filter + '';
  7218. filter = function(event) {
  7219. return event._id == eventID;
  7220. };
  7221. }
  7222. // Purge event(s) from our local cache
  7223. cache = $.grep(cache, filter, true); // inverse=true
  7224. // Remove events from array sources.
  7225. // This works because they have been converted to official Event Objects up front.
  7226. // (and as a result, event._id has been calculated).
  7227. for (i=0; i<sources.length; i++) {
  7228. if ($.isArray(sources[i].events)) {
  7229. sources[i].events = $.grep(sources[i].events, filter, true);
  7230. }
  7231. }
  7232. reportEvents(cache);
  7233. }
  7234. function clientEvents(filter) {
  7235. if ($.isFunction(filter)) {
  7236. return $.grep(cache, filter);
  7237. }
  7238. else if (filter != null) { // not null, not undefined. an event ID
  7239. filter += '';
  7240. return $.grep(cache, function(e) {
  7241. return e._id == filter;
  7242. });
  7243. }
  7244. return cache; // else, return all
  7245. }
  7246. /* Event Normalization
  7247. -----------------------------------------------------------------------------*/
  7248. // Given a raw object with key/value properties, returns an "abstract" Event object.
  7249. // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  7250. // Will return `false` when input is invalid.
  7251. // `source` is optional
  7252. function buildEventFromInput(input, source) {
  7253. var out = {};
  7254. var start, end;
  7255. var allDay;
  7256. if (options.eventDataTransform) {
  7257. input = options.eventDataTransform(input);
  7258. }
  7259. if (source && source.eventDataTransform) {
  7260. input = source.eventDataTransform(input);
  7261. }
  7262. // Copy all properties over to the resulting object.
  7263. // The special-case properties will be copied over afterwards.
  7264. $.extend(out, input);
  7265. if (source) {
  7266. out.source = source;
  7267. }
  7268. out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  7269. if (input.className) {
  7270. if (typeof input.className == 'string') {
  7271. out.className = input.className.split(/\s+/);
  7272. }
  7273. else { // assumed to be an array
  7274. out.className = input.className;
  7275. }
  7276. }
  7277. else {
  7278. out.className = [];
  7279. }
  7280. start = input.start || input.date; // "date" is an alias for "start"
  7281. end = input.end;
  7282. // parse as a time (Duration) if applicable
  7283. if (isTimeString(start)) {
  7284. start = moment.duration(start);
  7285. }
  7286. if (isTimeString(end)) {
  7287. end = moment.duration(end);
  7288. }
  7289. if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  7290. // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  7291. out.start = start ? moment.duration(start) : null; // will be a Duration or null
  7292. out.end = end ? moment.duration(end) : null; // will be a Duration or null
  7293. out._recurring = true; // our internal marker
  7294. }
  7295. else {
  7296. if (start) {
  7297. start = t.moment(start);
  7298. if (!start.isValid()) {
  7299. return false;
  7300. }
  7301. }
  7302. if (end) {
  7303. end = t.moment(end);
  7304. if (!end.isValid()) {
  7305. end = null; // let defaults take over
  7306. }
  7307. }
  7308. allDay = input.allDay;
  7309. if (allDay === undefined) { // still undefined? fallback to default
  7310. allDay = firstDefined(
  7311. source ? source.allDayDefault : undefined,
  7312. options.allDayDefault
  7313. );
  7314. // still undefined? normalizeEventRange will calculate it
  7315. }
  7316. assignDatesToEvent(start, end, allDay, out);
  7317. }
  7318. return out;
  7319. }
  7320. // Normalizes and assigns the given dates to the given partially-formed event object.
  7321. // NOTE: mutates the given start/end moments. does not make a copy.
  7322. function assignDatesToEvent(start, end, allDay, event) {
  7323. event.start = start;
  7324. event.end = end;
  7325. event.allDay = allDay;
  7326. normalizeEventRange(event);
  7327. backupEventDates(event);
  7328. }
  7329. // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  7330. // NOTE: Will modify the given object.
  7331. function normalizeEventRange(props) {
  7332. normalizeEventRangeTimes(props);
  7333. if (props.end && !props.end.isAfter(props.start)) {
  7334. props.end = null;
  7335. }
  7336. if (!props.end) {
  7337. if (options.forceEventDuration) {
  7338. props.end = t.getDefaultEventEnd(props.allDay, props.start);
  7339. }
  7340. else {
  7341. props.end = null;
  7342. }
  7343. }
  7344. }
  7345. // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  7346. function normalizeEventRangeTimes(range) {
  7347. if (range.allDay == null) {
  7348. range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime()));
  7349. }
  7350. if (range.allDay) {
  7351. range.start.stripTime();
  7352. if (range.end) {
  7353. // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  7354. range.end.stripTime();
  7355. }
  7356. }
  7357. else {
  7358. if (!range.start.hasTime()) {
  7359. range.start = t.rezoneDate(range.start); // will assign a 00:00 time
  7360. }
  7361. if (range.end && !range.end.hasTime()) {
  7362. range.end = t.rezoneDate(range.end); // will assign a 00:00 time
  7363. }
  7364. }
  7365. }
  7366. // If `range` is a proper range with a start and end, returns the original object.
  7367. // If missing an end, computes a new range with an end, computing it as if it were an event.
  7368. // TODO: make this a part of the event -> eventRange system
  7369. function ensureVisibleEventRange(range) {
  7370. var allDay;
  7371. if (!range.end) {
  7372. allDay = range.allDay; // range might be more event-ish than we think
  7373. if (allDay == null) {
  7374. allDay = !range.start.hasTime();
  7375. }
  7376. range = $.extend({}, range); // make a copy, copying over other misc properties
  7377. range.end = t.getDefaultEventEnd(allDay, range.start);
  7378. }
  7379. return range;
  7380. }
  7381. // If the given event is a recurring event, break it down into an array of individual instances.
  7382. // If not a recurring event, return an array with the single original event.
  7383. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  7384. // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  7385. function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  7386. var events = [];
  7387. var dowHash;
  7388. var dow;
  7389. var i;
  7390. var date;
  7391. var startTime, endTime;
  7392. var start, end;
  7393. var event;
  7394. _rangeStart = _rangeStart || rangeStart;
  7395. _rangeEnd = _rangeEnd || rangeEnd;
  7396. if (abstractEvent) {
  7397. if (abstractEvent._recurring) {
  7398. // make a boolean hash as to whether the event occurs on each day-of-week
  7399. if ((dow = abstractEvent.dow)) {
  7400. dowHash = {};
  7401. for (i = 0; i < dow.length; i++) {
  7402. dowHash[dow[i]] = true;
  7403. }
  7404. }
  7405. // iterate through every day in the current range
  7406. date = _rangeStart.clone().stripTime(); // holds the date of the current day
  7407. while (date.isBefore(_rangeEnd)) {
  7408. if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  7409. startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  7410. endTime = abstractEvent.end; // "
  7411. start = date.clone();
  7412. end = null;
  7413. if (startTime) {
  7414. start = start.time(startTime);
  7415. }
  7416. if (endTime) {
  7417. end = date.clone().time(endTime);
  7418. }
  7419. event = $.extend({}, abstractEvent); // make a copy of the original
  7420. assignDatesToEvent(
  7421. start, end,
  7422. !startTime && !endTime, // allDay?
  7423. event
  7424. );
  7425. events.push(event);
  7426. }
  7427. date.add(1, 'days');
  7428. }
  7429. }
  7430. else {
  7431. events.push(abstractEvent); // return the original event. will be a one-item array
  7432. }
  7433. }
  7434. return events;
  7435. }
  7436. /* Event Modification Math
  7437. -----------------------------------------------------------------------------------------*/
  7438. // Modifies an event and all related events by applying the given properties.
  7439. // Special date-diffing logic is used for manipulation of dates.
  7440. // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  7441. // All date comparisons are done against the event's pristine _start and _end dates.
  7442. // Returns an object with delta information and a function to undo all operations.
  7443. // For making computations in a granularity greater than day/time, specify largeUnit.
  7444. // NOTE: The given `newProps` might be mutated for normalization purposes.
  7445. function mutateEvent(event, newProps, largeUnit) {
  7446. var miscProps = {};
  7447. var oldProps;
  7448. var clearEnd;
  7449. var startDelta;
  7450. var endDelta;
  7451. var durationDelta;
  7452. var undoFunc;
  7453. // diffs the dates in the appropriate way, returning a duration
  7454. function diffDates(date1, date0) { // date1 - date0
  7455. if (largeUnit) {
  7456. return diffByUnit(date1, date0, largeUnit);
  7457. }
  7458. else if (newProps.allDay) {
  7459. return diffDay(date1, date0);
  7460. }
  7461. else {
  7462. return diffDayTime(date1, date0);
  7463. }
  7464. }
  7465. newProps = newProps || {};
  7466. // normalize new date-related properties
  7467. if (!newProps.start) {
  7468. newProps.start = event.start.clone();
  7469. }
  7470. if (newProps.end === undefined) {
  7471. newProps.end = event.end ? event.end.clone() : null;
  7472. }
  7473. if (newProps.allDay == null) { // is null or undefined?
  7474. newProps.allDay = event.allDay;
  7475. }
  7476. normalizeEventRange(newProps);
  7477. // create normalized versions of the original props to compare against
  7478. // need a real end value, for diffing
  7479. oldProps = {
  7480. start: event._start.clone(),
  7481. end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  7482. allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  7483. };
  7484. normalizeEventRange(oldProps);
  7485. // need to clear the end date if explicitly changed to null
  7486. clearEnd = event._end !== null && newProps.end === null;
  7487. // compute the delta for moving the start date
  7488. startDelta = diffDates(newProps.start, oldProps.start);
  7489. // compute the delta for moving the end date
  7490. if (newProps.end) {
  7491. endDelta = diffDates(newProps.end, oldProps.end);
  7492. durationDelta = endDelta.subtract(startDelta);
  7493. }
  7494. else {
  7495. durationDelta = null;
  7496. }
  7497. // gather all non-date-related properties
  7498. $.each(newProps, function(name, val) {
  7499. if (isMiscEventPropName(name)) {
  7500. if (val !== undefined) {
  7501. miscProps[name] = val;
  7502. }
  7503. }
  7504. });
  7505. // apply the operations to the event and all related events
  7506. undoFunc = mutateEvents(
  7507. clientEvents(event._id), // get events with this ID
  7508. clearEnd,
  7509. newProps.allDay,
  7510. startDelta,
  7511. durationDelta,
  7512. miscProps
  7513. );
  7514. return {
  7515. dateDelta: startDelta,
  7516. durationDelta: durationDelta,
  7517. undo: undoFunc
  7518. };
  7519. }
  7520. // Modifies an array of events in the following ways (operations are in order):
  7521. // - clear the event's `end`
  7522. // - convert the event to allDay
  7523. // - add `dateDelta` to the start and end
  7524. // - add `durationDelta` to the event's duration
  7525. // - assign `miscProps` to the event
  7526. //
  7527. // Returns a function that can be called to undo all the operations.
  7528. //
  7529. // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  7530. //
  7531. function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  7532. var isAmbigTimezone = t.getIsAmbigTimezone();
  7533. var undoFunctions = [];
  7534. // normalize zero-length deltas to be null
  7535. if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  7536. if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  7537. $.each(events, function(i, event) {
  7538. var oldProps;
  7539. var newProps;
  7540. // build an object holding all the old values, both date-related and misc.
  7541. // for the undo function.
  7542. oldProps = {
  7543. start: event.start.clone(),
  7544. end: event.end ? event.end.clone() : null,
  7545. allDay: event.allDay
  7546. };
  7547. $.each(miscProps, function(name) {
  7548. oldProps[name] = event[name];
  7549. });
  7550. // new date-related properties. work off the original date snapshot.
  7551. // ok to use references because they will be thrown away when backupEventDates is called.
  7552. newProps = {
  7553. start: event._start,
  7554. end: event._end,
  7555. allDay: allDay // normalize the dates in the same regard as the new properties
  7556. };
  7557. normalizeEventRange(newProps); // massages start/end/allDay
  7558. // strip or ensure the end date
  7559. if (clearEnd) {
  7560. newProps.end = null;
  7561. }
  7562. else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  7563. newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  7564. }
  7565. if (dateDelta) {
  7566. newProps.start.add(dateDelta);
  7567. if (newProps.end) {
  7568. newProps.end.add(dateDelta);
  7569. }
  7570. }
  7571. if (durationDelta) {
  7572. newProps.end.add(durationDelta); // end already ensured above
  7573. }
  7574. // if the dates have changed, and we know it is impossible to recompute the
  7575. // timezone offsets, strip the zone.
  7576. if (
  7577. isAmbigTimezone &&
  7578. !newProps.allDay &&
  7579. (dateDelta || durationDelta)
  7580. ) {
  7581. newProps.start.stripZone();
  7582. if (newProps.end) {
  7583. newProps.end.stripZone();
  7584. }
  7585. }
  7586. $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  7587. backupEventDates(event); // regenerate internal _start/_end/_allDay
  7588. undoFunctions.push(function() {
  7589. $.extend(event, oldProps);
  7590. backupEventDates(event); // regenerate internal _start/_end/_allDay
  7591. });
  7592. });
  7593. return function() {
  7594. for (var i = 0; i < undoFunctions.length; i++) {
  7595. undoFunctions[i]();
  7596. }
  7597. };
  7598. }
  7599. /* Business Hours
  7600. -----------------------------------------------------------------------------------------*/
  7601. t.getBusinessHoursEvents = getBusinessHoursEvents;
  7602. // Returns an array of events as to when the business hours occur in the given view.
  7603. // Abuse of our event system :(
  7604. function getBusinessHoursEvents(wholeDay) {
  7605. var optionVal = options.businessHours;
  7606. var defaultVal = {
  7607. className: 'fc-nonbusiness',
  7608. start: '09:00',
  7609. end: '17:00',
  7610. dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  7611. rendering: 'inverse-background'
  7612. };
  7613. var view = t.getView();
  7614. var eventInput;
  7615. if (optionVal) { // `true` (which means "use the defaults") or an override object
  7616. eventInput = $.extend(
  7617. {}, // copy to a new object in either case
  7618. defaultVal,
  7619. typeof optionVal === 'object' ? optionVal : {} // override the defaults
  7620. );
  7621. }
  7622. if (eventInput) {
  7623. // if a whole-day series is requested, clear the start/end times
  7624. if (wholeDay) {
  7625. eventInput.start = null;
  7626. eventInput.end = null;
  7627. }
  7628. return expandEvent(
  7629. buildEventFromInput(eventInput),
  7630. view.start,
  7631. view.end
  7632. );
  7633. }
  7634. return [];
  7635. }
  7636. /* Overlapping / Constraining
  7637. -----------------------------------------------------------------------------------------*/
  7638. t.isEventRangeAllowed = isEventRangeAllowed;
  7639. t.isSelectionRangeAllowed = isSelectionRangeAllowed;
  7640. t.isExternalDropRangeAllowed = isExternalDropRangeAllowed;
  7641. function isEventRangeAllowed(range, event) {
  7642. var source = event.source || {};
  7643. var constraint = firstDefined(
  7644. event.constraint,
  7645. source.constraint,
  7646. options.eventConstraint
  7647. );
  7648. var overlap = firstDefined(
  7649. event.overlap,
  7650. source.overlap,
  7651. options.eventOverlap
  7652. );
  7653. range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed
  7654. return isRangeAllowed(range, constraint, overlap, event);
  7655. }
  7656. function isSelectionRangeAllowed(range) {
  7657. return isRangeAllowed(range, options.selectConstraint, options.selectOverlap);
  7658. }
  7659. // when `eventProps` is defined, consider this an event.
  7660. // `eventProps` can contain misc non-date-related info about the event.
  7661. function isExternalDropRangeAllowed(range, eventProps) {
  7662. var eventInput;
  7663. var event;
  7664. // note: very similar logic is in View's reportExternalDrop
  7665. if (eventProps) {
  7666. eventInput = $.extend({}, eventProps, range);
  7667. event = expandEvent(buildEventFromInput(eventInput))[0];
  7668. }
  7669. if (event) {
  7670. return isEventRangeAllowed(range, event);
  7671. }
  7672. else { // treat it as a selection
  7673. range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed
  7674. return isSelectionRangeAllowed(range);
  7675. }
  7676. }
  7677. // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
  7678. // according to the constraint/overlap settings.
  7679. // `event` is not required if checking a selection.
  7680. function isRangeAllowed(range, constraint, overlap, event) {
  7681. var constraintEvents;
  7682. var anyContainment;
  7683. var peerEvents;
  7684. var i, peerEvent;
  7685. var peerOverlap;
  7686. // normalize. fyi, we're normalizing in too many places :(
  7687. range = $.extend({}, range); // copy all properties in case there are misc non-date properties
  7688. range.start = range.start.clone().stripZone();
  7689. range.end = range.end.clone().stripZone();
  7690. // the range must be fully contained by at least one of produced constraint events
  7691. if (constraint != null) {
  7692. // not treated as an event! intermediate data structure
  7693. // TODO: use ranges in the future
  7694. constraintEvents = constraintToEvents(constraint);
  7695. anyContainment = false;
  7696. for (i = 0; i < constraintEvents.length; i++) {
  7697. if (eventContainsRange(constraintEvents[i], range)) {
  7698. anyContainment = true;
  7699. break;
  7700. }
  7701. }
  7702. if (!anyContainment) {
  7703. return false;
  7704. }
  7705. }
  7706. peerEvents = t.getPeerEvents(event, range);
  7707. for (i = 0; i < peerEvents.length; i++) {
  7708. peerEvent = peerEvents[i];
  7709. // there needs to be an actual intersection before disallowing anything
  7710. if (eventIntersectsRange(peerEvent, range)) {
  7711. // evaluate overlap for the given range and short-circuit if necessary
  7712. if (overlap === false) {
  7713. return false;
  7714. }
  7715. // if the event's overlap is a test function, pass the peer event in question as the first param
  7716. else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  7717. return false;
  7718. }
  7719. // if we are computing if the given range is allowable for an event, consider the other event's
  7720. // EventObject-specific or Source-specific `overlap` property
  7721. if (event) {
  7722. peerOverlap = firstDefined(
  7723. peerEvent.overlap,
  7724. (peerEvent.source || {}).overlap
  7725. // we already considered the global `eventOverlap`
  7726. );
  7727. if (peerOverlap === false) {
  7728. return false;
  7729. }
  7730. // if the peer event's overlap is a test function, pass the subject event as the first param
  7731. if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  7732. return false;
  7733. }
  7734. }
  7735. }
  7736. }
  7737. return true;
  7738. }
  7739. // Given an event input from the API, produces an array of event objects. Possible event inputs:
  7740. // 'businessHours'
  7741. // An event ID (number or string)
  7742. // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  7743. function constraintToEvents(constraintInput) {
  7744. if (constraintInput === 'businessHours') {
  7745. return getBusinessHoursEvents();
  7746. }
  7747. if (typeof constraintInput === 'object') {
  7748. return expandEvent(buildEventFromInput(constraintInput));
  7749. }
  7750. return clientEvents(constraintInput); // probably an ID
  7751. }
  7752. // Does the event's date range fully contain the given range?
  7753. // start/end already assumed to have stripped zones :(
  7754. function eventContainsRange(event, range) {
  7755. var eventStart = event.start.clone().stripZone();
  7756. var eventEnd = t.getEventEnd(event).stripZone();
  7757. return range.start >= eventStart && range.end <= eventEnd;
  7758. }
  7759. // Does the event's date range intersect with the given range?
  7760. // start/end already assumed to have stripped zones :(
  7761. function eventIntersectsRange(event, range) {
  7762. var eventStart = event.start.clone().stripZone();
  7763. var eventEnd = t.getEventEnd(event).stripZone();
  7764. return range.start < eventEnd && range.end > eventStart;
  7765. }
  7766. t.getEventCache = function() {
  7767. return cache;
  7768. };
  7769. }
  7770. // Returns a list of events that the given event should be compared against when being considered for a move to
  7771. // the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  7772. Calendar.prototype.getPeerEvents = function(event, range) {
  7773. var cache = this.getEventCache();
  7774. var peerEvents = [];
  7775. var i, otherEvent;
  7776. for (i = 0; i < cache.length; i++) {
  7777. otherEvent = cache[i];
  7778. if (
  7779. !event ||
  7780. event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  7781. ) {
  7782. peerEvents.push(otherEvent);
  7783. }
  7784. }
  7785. return peerEvents;
  7786. };
  7787. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  7788. function backupEventDates(event) {
  7789. event._allDay = event.allDay;
  7790. event._start = event.start.clone();
  7791. event._end = event.end ? event.end.clone() : null;
  7792. }
  7793. ;;
  7794. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  7795. ----------------------------------------------------------------------------------------------------------------------*/
  7796. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  7797. // It is responsible for managing width/height.
  7798. var BasicView = View.extend({
  7799. dayGrid: null, // the main subcomponent that does most of the heavy lifting
  7800. dayNumbersVisible: false, // display day numbers on each day cell?
  7801. weekNumbersVisible: false, // display week numbers along the side?
  7802. weekNumberWidth: null, // width of all the week-number cells running down the side
  7803. headRowEl: null, // the fake row element of the day-of-week header
  7804. initialize: function() {
  7805. this.dayGrid = new DayGrid(this);
  7806. this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
  7807. },
  7808. // Sets the display range and computes all necessary dates
  7809. setRange: function(range) {
  7810. View.prototype.setRange.call(this, range); // call the super-method
  7811. this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
  7812. this.dayGrid.setRange(range);
  7813. },
  7814. // Compute the value to feed into setRange. Overrides superclass.
  7815. computeRange: function(date) {
  7816. var range = View.prototype.computeRange.call(this, date); // get value from the super-method
  7817. // year and month views should be aligned with weeks. this is already done for week
  7818. if (/year|month/.test(range.intervalUnit)) {
  7819. range.start.startOf('week');
  7820. range.start = this.skipHiddenDays(range.start);
  7821. // make end-of-week if not already
  7822. if (range.end.weekday()) {
  7823. range.end.add(1, 'week').startOf('week');
  7824. range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
  7825. }
  7826. }
  7827. return range;
  7828. },
  7829. // Renders the view into `this.el`, which should already be assigned
  7830. renderDates: function() {
  7831. this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
  7832. this.weekNumbersVisible = this.opt('weekNumbers');
  7833. this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
  7834. this.el.addClass('fc-basic-view').html(this.renderHtml());
  7835. this.headRowEl = this.el.find('thead .fc-row');
  7836. this.scrollerEl = this.el.find('.fc-day-grid-container');
  7837. this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
  7838. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  7839. this.dayGrid.renderDates(this.hasRigidRows());
  7840. },
  7841. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  7842. // always completely kill the dayGrid's rendering.
  7843. unrenderDates: function() {
  7844. this.dayGrid.unrenderDates();
  7845. this.dayGrid.removeElement();
  7846. },
  7847. renderBusinessHours: function() {
  7848. this.dayGrid.renderBusinessHours();
  7849. },
  7850. // Builds the HTML skeleton for the view.
  7851. // The day-grid component will render inside of a container defined by this HTML.
  7852. renderHtml: function() {
  7853. return '' +
  7854. '<table>' +
  7855. '<thead class="fc-head">' +
  7856. '<tr>' +
  7857. '<td class="' + this.widgetHeaderClass + '">' +
  7858. this.dayGrid.headHtml() + // render the day-of-week headers
  7859. '</td>' +
  7860. '</tr>' +
  7861. '</thead>' +
  7862. '<tbody class="fc-body">' +
  7863. '<tr>' +
  7864. '<td class="' + this.widgetContentClass + '">' +
  7865. '<div class="fc-day-grid-container">' +
  7866. '<div class="fc-day-grid"/>' +
  7867. '</div>' +
  7868. '</td>' +
  7869. '</tr>' +
  7870. '</tbody>' +
  7871. '</table>';
  7872. },
  7873. // Generates the HTML that will go before the day-of week header cells.
  7874. // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  7875. headIntroHtml: function() {
  7876. if (this.weekNumbersVisible) {
  7877. return '' +
  7878. '<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +
  7879. '<span>' + // needed for matchCellWidths
  7880. htmlEscape(this.opt('weekNumberTitle')) +
  7881. '</span>' +
  7882. '</th>';
  7883. }
  7884. },
  7885. // Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
  7886. // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  7887. numberIntroHtml: function(row) {
  7888. if (this.weekNumbersVisible) {
  7889. return '' +
  7890. '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
  7891. '<span>' + // needed for matchCellWidths
  7892. this.dayGrid.getCell(row, 0).start.format('w') +
  7893. '</span>' +
  7894. '</td>';
  7895. }
  7896. },
  7897. // Generates the HTML that goes before the day bg cells for each day-row.
  7898. // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  7899. dayIntroHtml: function() {
  7900. if (this.weekNumbersVisible) {
  7901. return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +
  7902. this.weekNumberStyleAttr() + '></td>';
  7903. }
  7904. },
  7905. // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
  7906. // Affects helper-skeleton and highlight-skeleton rows.
  7907. introHtml: function() {
  7908. if (this.weekNumbersVisible) {
  7909. return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
  7910. }
  7911. },
  7912. // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  7913. // The number row will only exist if either day numbers or week numbers are turned on.
  7914. numberCellHtml: function(cell) {
  7915. var date = cell.start;
  7916. var classes;
  7917. if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
  7918. return '<td/>'; // will create an empty space above events :(
  7919. }
  7920. classes = this.dayGrid.getDayClasses(date);
  7921. classes.unshift('fc-day-number');
  7922. return '' +
  7923. '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
  7924. date.date() +
  7925. '</td>';
  7926. },
  7927. // Generates an HTML attribute string for setting the width of the week number column, if it is known
  7928. weekNumberStyleAttr: function() {
  7929. if (this.weekNumberWidth !== null) {
  7930. return 'style="width:' + this.weekNumberWidth + 'px"';
  7931. }
  7932. return '';
  7933. },
  7934. // Determines whether each row should have a constant height
  7935. hasRigidRows: function() {
  7936. var eventLimit = this.opt('eventLimit');
  7937. return eventLimit && typeof eventLimit !== 'number';
  7938. },
  7939. /* Dimensions
  7940. ------------------------------------------------------------------------------------------------------------------*/
  7941. // Refreshes the horizontal dimensions of the view
  7942. updateWidth: function() {
  7943. if (this.weekNumbersVisible) {
  7944. // Make sure all week number cells running down the side have the same width.
  7945. // Record the width for cells created later.
  7946. this.weekNumberWidth = matchCellWidths(
  7947. this.el.find('.fc-week-number')
  7948. );
  7949. }
  7950. },
  7951. // Adjusts the vertical dimensions of the view to the specified values
  7952. setHeight: function(totalHeight, isAuto) {
  7953. var eventLimit = this.opt('eventLimit');
  7954. var scrollerHeight;
  7955. // reset all heights to be natural
  7956. unsetScroller(this.scrollerEl);
  7957. uncompensateScroll(this.headRowEl);
  7958. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  7959. // is the event limit a constant level number?
  7960. if (eventLimit && typeof eventLimit === 'number') {
  7961. this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  7962. }
  7963. scrollerHeight = this.computeScrollerHeight(totalHeight);
  7964. this.setGridHeight(scrollerHeight, isAuto);
  7965. // is the event limit dynamically calculated?
  7966. if (eventLimit && typeof eventLimit !== 'number') {
  7967. this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  7968. }
  7969. if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  7970. compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
  7971. // doing the scrollbar compensation might have created text overflow which created more height. redo
  7972. scrollerHeight = this.computeScrollerHeight(totalHeight);
  7973. this.scrollerEl.height(scrollerHeight);
  7974. }
  7975. },
  7976. // Sets the height of just the DayGrid component in this view
  7977. setGridHeight: function(height, isAuto) {
  7978. if (isAuto) {
  7979. undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  7980. }
  7981. else {
  7982. distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  7983. }
  7984. },
  7985. /* Events
  7986. ------------------------------------------------------------------------------------------------------------------*/
  7987. // Renders the given events onto the view and populates the segments array
  7988. renderEvents: function(events) {
  7989. this.dayGrid.renderEvents(events);
  7990. this.updateHeight(); // must compensate for events that overflow the row
  7991. },
  7992. // Retrieves all segment objects that are rendered in the view
  7993. getEventSegs: function() {
  7994. return this.dayGrid.getEventSegs();
  7995. },
  7996. // Unrenders all event elements and clears internal segment data
  7997. unrenderEvents: function() {
  7998. this.dayGrid.unrenderEvents();
  7999. // we DON'T need to call updateHeight() because:
  8000. // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  8001. // B) in IE8, this causes a flash whenever events are rerendered
  8002. },
  8003. /* Dragging (for both events and external elements)
  8004. ------------------------------------------------------------------------------------------------------------------*/
  8005. // A returned value of `true` signals that a mock "helper" event has been rendered.
  8006. renderDrag: function(dropLocation, seg) {
  8007. return this.dayGrid.renderDrag(dropLocation, seg);
  8008. },
  8009. unrenderDrag: function() {
  8010. this.dayGrid.unrenderDrag();
  8011. },
  8012. /* Selection
  8013. ------------------------------------------------------------------------------------------------------------------*/
  8014. // Renders a visual indication of a selection
  8015. renderSelection: function(range) {
  8016. this.dayGrid.renderSelection(range);
  8017. },
  8018. // Unrenders a visual indications of a selection
  8019. unrenderSelection: function() {
  8020. this.dayGrid.unrenderSelection();
  8021. }
  8022. });
  8023. ;;
  8024. /* A month view with day cells running in rows (one-per-week) and columns
  8025. ----------------------------------------------------------------------------------------------------------------------*/
  8026. var MonthView = BasicView.extend({
  8027. // Produces information about what range to display
  8028. computeRange: function(date) {
  8029. var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
  8030. var rowCnt;
  8031. // ensure 6 weeks
  8032. if (this.isFixedWeeks()) {
  8033. rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
  8034. range.end.add(6 - rowCnt, 'weeks');
  8035. }
  8036. return range;
  8037. },
  8038. // Overrides the default BasicView behavior to have special multi-week auto-height logic
  8039. setGridHeight: function(height, isAuto) {
  8040. isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
  8041. // if auto, make the height of each row the height that it would be if there were 6 weeks
  8042. if (isAuto) {
  8043. height *= this.rowCnt / 6;
  8044. }
  8045. distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  8046. },
  8047. isFixedWeeks: function() {
  8048. var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
  8049. if (weekMode) {
  8050. return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
  8051. }
  8052. return this.opt('fixedWeekCount');
  8053. }
  8054. });
  8055. ;;
  8056. fcViews.basic = {
  8057. 'class': BasicView
  8058. };
  8059. fcViews.basicDay = {
  8060. type: 'basic',
  8061. duration: { days: 1 }
  8062. };
  8063. fcViews.basicWeek = {
  8064. type: 'basic',
  8065. duration: { weeks: 1 }
  8066. };
  8067. fcViews.month = {
  8068. 'class': MonthView,
  8069. duration: { months: 1 }, // important for prev/next
  8070. defaults: {
  8071. fixedWeekCount: true
  8072. }
  8073. };
  8074. ;;
  8075. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  8076. ----------------------------------------------------------------------------------------------------------------------*/
  8077. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  8078. // Responsible for managing width/height.
  8079. var AgendaView = View.extend({
  8080. timeGrid: null, // the main time-grid subcomponent of this view
  8081. dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  8082. axisWidth: null, // the width of the time axis running down the side
  8083. noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
  8084. // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  8085. bottomRuleEl: null,
  8086. bottomRuleHeight: null,
  8087. initialize: function() {
  8088. this.timeGrid = new TimeGrid(this);
  8089. if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  8090. this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
  8091. // the coordinate grid will be a combination of both subcomponents' grids
  8092. this.coordMap = new ComboCoordMap([
  8093. this.dayGrid.coordMap,
  8094. this.timeGrid.coordMap
  8095. ]);
  8096. }
  8097. else {
  8098. this.coordMap = this.timeGrid.coordMap;
  8099. }
  8100. },
  8101. /* Rendering
  8102. ------------------------------------------------------------------------------------------------------------------*/
  8103. // Sets the display range and computes all necessary dates
  8104. setRange: function(range) {
  8105. View.prototype.setRange.call(this, range); // call the super-method
  8106. this.timeGrid.setRange(range);
  8107. if (this.dayGrid) {
  8108. this.dayGrid.setRange(range);
  8109. }
  8110. },
  8111. // Renders the view into `this.el`, which has already been assigned
  8112. renderDates: function() {
  8113. this.el.addClass('fc-agenda-view').html(this.renderHtml());
  8114. // the element that wraps the time-grid that will probably scroll
  8115. this.scrollerEl = this.el.find('.fc-time-grid-container');
  8116. this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
  8117. this.timeGrid.setElement(this.el.find('.fc-time-grid'));
  8118. this.timeGrid.renderDates();
  8119. // the <hr> that sometimes displays under the time-grid
  8120. this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
  8121. .appendTo(this.timeGrid.el); // inject it into the time-grid
  8122. if (this.dayGrid) {
  8123. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  8124. this.dayGrid.renderDates();
  8125. // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  8126. this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  8127. }
  8128. this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  8129. },
  8130. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  8131. // always completely kill each grid's rendering.
  8132. unrenderDates: function() {
  8133. this.timeGrid.unrenderDates();
  8134. this.timeGrid.removeElement();
  8135. if (this.dayGrid) {
  8136. this.dayGrid.unrenderDates();
  8137. this.dayGrid.removeElement();
  8138. }
  8139. },
  8140. renderBusinessHours: function() {
  8141. this.timeGrid.renderBusinessHours();
  8142. if (this.dayGrid) {
  8143. this.dayGrid.renderBusinessHours();
  8144. }
  8145. },
  8146. // Builds the HTML skeleton for the view.
  8147. // The day-grid and time-grid components will render inside containers defined by this HTML.
  8148. renderHtml: function() {
  8149. return '' +
  8150. '<table>' +
  8151. '<thead class="fc-head">' +
  8152. '<tr>' +
  8153. '<td class="' + this.widgetHeaderClass + '">' +
  8154. this.timeGrid.headHtml() + // render the day-of-week headers
  8155. '</td>' +
  8156. '</tr>' +
  8157. '</thead>' +
  8158. '<tbody class="fc-body">' +
  8159. '<tr>' +
  8160. '<td class="' + this.widgetContentClass + '">' +
  8161. (this.dayGrid ?
  8162. '<div class="fc-day-grid"/>' +
  8163. '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
  8164. ''
  8165. ) +
  8166. '<div class="fc-time-grid-container">' +
  8167. '<div class="fc-time-grid"/>' +
  8168. '</div>' +
  8169. '</td>' +
  8170. '</tr>' +
  8171. '</tbody>' +
  8172. '</table>';
  8173. },
  8174. // Generates the HTML that will go before the day-of week header cells.
  8175. // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
  8176. headIntroHtml: function() {
  8177. var date;
  8178. var weekText;
  8179. if (this.opt('weekNumbers')) {
  8180. date = this.timeGrid.getCell(0).start;
  8181. weekText = date.format(this.opt('smallWeekFormat'));
  8182. return '' +
  8183. '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
  8184. '<span>' + // needed for matchCellWidths
  8185. htmlEscape(weekText) +
  8186. '</span>' +
  8187. '</th>';
  8188. }
  8189. else {
  8190. return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
  8191. }
  8192. },
  8193. // Generates the HTML that goes before the all-day cells.
  8194. // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  8195. dayIntroHtml: function() {
  8196. return '' +
  8197. '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
  8198. '<span>' + // needed for matchCellWidths
  8199. (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
  8200. '</span>' +
  8201. '</td>';
  8202. },
  8203. // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  8204. slotBgIntroHtml: function() {
  8205. return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
  8206. },
  8207. // Generates the HTML that goes before all other types of cells.
  8208. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  8209. // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
  8210. introHtml: function() {
  8211. return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
  8212. },
  8213. // Generates an HTML attribute string for setting the width of the axis, if it is known
  8214. axisStyleAttr: function() {
  8215. if (this.axisWidth !== null) {
  8216. return 'style="width:' + this.axisWidth + 'px"';
  8217. }
  8218. return '';
  8219. },
  8220. /* Dimensions
  8221. ------------------------------------------------------------------------------------------------------------------*/
  8222. updateSize: function(isResize) {
  8223. this.timeGrid.updateSize(isResize);
  8224. View.prototype.updateSize.call(this, isResize); // call the super-method
  8225. },
  8226. // Refreshes the horizontal dimensions of the view
  8227. updateWidth: function() {
  8228. // make all axis cells line up, and record the width so newly created axis cells will have it
  8229. this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  8230. },
  8231. // Adjusts the vertical dimensions of the view to the specified values
  8232. setHeight: function(totalHeight, isAuto) {
  8233. var eventLimit;
  8234. var scrollerHeight;
  8235. if (this.bottomRuleHeight === null) {
  8236. // calculate the height of the rule the very first time
  8237. this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
  8238. }
  8239. this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  8240. // reset all dimensions back to the original state
  8241. this.scrollerEl.css('overflow', '');
  8242. unsetScroller(this.scrollerEl);
  8243. uncompensateScroll(this.noScrollRowEls);
  8244. // limit number of events in the all-day area
  8245. if (this.dayGrid) {
  8246. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  8247. eventLimit = this.opt('eventLimit');
  8248. if (eventLimit && typeof eventLimit !== 'number') {
  8249. eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  8250. }
  8251. if (eventLimit) {
  8252. this.dayGrid.limitRows(eventLimit);
  8253. }
  8254. }
  8255. if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
  8256. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8257. if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  8258. // make the all-day and header rows lines up
  8259. compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
  8260. // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  8261. // and reapply the desired height to the scroller.
  8262. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8263. this.scrollerEl.height(scrollerHeight);
  8264. }
  8265. else { // no scrollbars
  8266. // still, force a height and display the bottom rule (marks the end of day)
  8267. this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
  8268. this.bottomRuleEl.show();
  8269. }
  8270. }
  8271. },
  8272. // Computes the initial pre-configured scroll state prior to allowing the user to change it
  8273. computeInitialScroll: function() {
  8274. var scrollTime = moment.duration(this.opt('scrollTime'));
  8275. var top = this.timeGrid.computeTimeTop(scrollTime);
  8276. // zoom can give weird floating-point values. rather scroll a little bit further
  8277. top = Math.ceil(top);
  8278. if (top) {
  8279. top++; // to overcome top border that slots beyond the first have. looks better
  8280. }
  8281. return top;
  8282. },
  8283. /* Events
  8284. ------------------------------------------------------------------------------------------------------------------*/
  8285. // Renders events onto the view and populates the View's segment array
  8286. renderEvents: function(events) {
  8287. var dayEvents = [];
  8288. var timedEvents = [];
  8289. var daySegs = [];
  8290. var timedSegs;
  8291. var i;
  8292. // separate the events into all-day and timed
  8293. for (i = 0; i < events.length; i++) {
  8294. if (events[i].allDay) {
  8295. dayEvents.push(events[i]);
  8296. }
  8297. else {
  8298. timedEvents.push(events[i]);
  8299. }
  8300. }
  8301. // render the events in the subcomponents
  8302. timedSegs = this.timeGrid.renderEvents(timedEvents);
  8303. if (this.dayGrid) {
  8304. daySegs = this.dayGrid.renderEvents(dayEvents);
  8305. }
  8306. // the all-day area is flexible and might have a lot of events, so shift the height
  8307. this.updateHeight();
  8308. },
  8309. // Retrieves all segment objects that are rendered in the view
  8310. getEventSegs: function() {
  8311. return this.timeGrid.getEventSegs().concat(
  8312. this.dayGrid ? this.dayGrid.getEventSegs() : []
  8313. );
  8314. },
  8315. // Unrenders all event elements and clears internal segment data
  8316. unrenderEvents: function() {
  8317. // unrender the events in the subcomponents
  8318. this.timeGrid.unrenderEvents();
  8319. if (this.dayGrid) {
  8320. this.dayGrid.unrenderEvents();
  8321. }
  8322. // we DON'T need to call updateHeight() because:
  8323. // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  8324. // B) in IE8, this causes a flash whenever events are rerendered
  8325. },
  8326. /* Dragging (for events and external elements)
  8327. ------------------------------------------------------------------------------------------------------------------*/
  8328. // A returned value of `true` signals that a mock "helper" event has been rendered.
  8329. renderDrag: function(dropLocation, seg) {
  8330. if (dropLocation.start.hasTime()) {
  8331. return this.timeGrid.renderDrag(dropLocation, seg);
  8332. }
  8333. else if (this.dayGrid) {
  8334. return this.dayGrid.renderDrag(dropLocation, seg);
  8335. }
  8336. },
  8337. unrenderDrag: function() {
  8338. this.timeGrid.unrenderDrag();
  8339. if (this.dayGrid) {
  8340. this.dayGrid.unrenderDrag();
  8341. }
  8342. },
  8343. /* Selection
  8344. ------------------------------------------------------------------------------------------------------------------*/
  8345. // Renders a visual indication of a selection
  8346. renderSelection: function(range) {
  8347. if (range.start.hasTime() || range.end.hasTime()) {
  8348. this.timeGrid.renderSelection(range);
  8349. }
  8350. else if (this.dayGrid) {
  8351. this.dayGrid.renderSelection(range);
  8352. }
  8353. },
  8354. // Unrenders a visual indications of a selection
  8355. unrenderSelection: function() {
  8356. this.timeGrid.unrenderSelection();
  8357. if (this.dayGrid) {
  8358. this.dayGrid.unrenderSelection();
  8359. }
  8360. }
  8361. });
  8362. ;;
  8363. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  8364. fcViews.agenda = {
  8365. 'class': AgendaView,
  8366. defaults: {
  8367. allDaySlot: true,
  8368. allDayText: 'all-day',
  8369. slotDuration: '00:30:00',
  8370. minTime: '00:00:00',
  8371. maxTime: '24:00:00',
  8372. slotEventOverlap: true // a bad name. confused with overlap/constraint system
  8373. }
  8374. };
  8375. fcViews.agendaDay = {
  8376. type: 'agenda',
  8377. duration: { days: 1 }
  8378. };
  8379. fcViews.agendaWeek = {
  8380. type: 'agenda',
  8381. duration: { weeks: 1 }
  8382. };
  8383. ;;
  8384. return fc; // export for Node/CommonJS
  8385. });