fullcalendar.js 385 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206
  1. /*!
  2. * FullCalendar v3.1.0
  3. * Docs & License: http://fullcalendar.io/
  4. * (c) 2016 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 = {
  19. version: "3.1.0",
  20. internalApiVersion: 7
  21. };
  22. var fcViews = FC.views = {};
  23. $.fn.fullCalendar = function(options) {
  24. var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  25. var res = this; // what this function will return (this jQuery object by default)
  26. this.each(function(i, _element) { // loop each DOM element involved
  27. var element = $(_element);
  28. var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  29. var singleRes; // the returned value of this single method call
  30. // a method call
  31. if (typeof options === 'string') {
  32. if (calendar && $.isFunction(calendar[options])) {
  33. singleRes = calendar[options].apply(calendar, args);
  34. if (!i) {
  35. res = singleRes; // record the first method call result
  36. }
  37. if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  38. element.removeData('fullCalendar');
  39. }
  40. }
  41. }
  42. // a new calendar initialization
  43. else if (!calendar) { // don't initialize twice
  44. calendar = new Calendar(element, options);
  45. element.data('fullCalendar', calendar);
  46. calendar.render();
  47. }
  48. });
  49. return res;
  50. };
  51. var complexOptions = [ // names of options that are objects whose properties should be combined
  52. 'header',
  53. 'footer',
  54. 'buttonText',
  55. 'buttonIcons',
  56. 'themeButtonIcons'
  57. ];
  58. // Merges an array of option objects into a single object
  59. function mergeOptions(optionObjs) {
  60. return mergeProps(optionObjs, complexOptions);
  61. }
  62. ;;
  63. // exports
  64. FC.intersectRanges = intersectRanges;
  65. FC.applyAll = applyAll;
  66. FC.debounce = debounce;
  67. FC.isInt = isInt;
  68. FC.htmlEscape = htmlEscape;
  69. FC.cssToStr = cssToStr;
  70. FC.proxy = proxy;
  71. FC.capitaliseFirstLetter = capitaliseFirstLetter;
  72. /* FullCalendar-specific DOM Utilities
  73. ----------------------------------------------------------------------------------------------------------------------*/
  74. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  75. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  76. function compensateScroll(rowEls, scrollbarWidths) {
  77. if (scrollbarWidths.left) {
  78. rowEls.css({
  79. 'border-left-width': 1,
  80. 'margin-left': scrollbarWidths.left - 1
  81. });
  82. }
  83. if (scrollbarWidths.right) {
  84. rowEls.css({
  85. 'border-right-width': 1,
  86. 'margin-right': scrollbarWidths.right - 1
  87. });
  88. }
  89. }
  90. // Undoes compensateScroll and restores all borders/margins
  91. function uncompensateScroll(rowEls) {
  92. rowEls.css({
  93. 'margin-left': '',
  94. 'margin-right': '',
  95. 'border-left-width': '',
  96. 'border-right-width': ''
  97. });
  98. }
  99. // Make the mouse cursor express that an event is not allowed in the current area
  100. function disableCursor() {
  101. $('body').addClass('fc-not-allowed');
  102. }
  103. // Returns the mouse cursor to its original look
  104. function enableCursor() {
  105. $('body').removeClass('fc-not-allowed');
  106. }
  107. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  108. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  109. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  110. // reduces the available height.
  111. function distributeHeight(els, availableHeight, shouldRedistribute) {
  112. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  113. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  114. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  115. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  116. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  117. var flexOffsets = []; // amount of vertical space it takes up
  118. var flexHeights = []; // actual css height
  119. var usedHeight = 0;
  120. undistributeHeight(els); // give all elements their natural height
  121. // find elements that are below the recommended height (expandable).
  122. // important to query for heights in a single first pass (to avoid reflow oscillation).
  123. els.each(function(i, el) {
  124. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  125. var naturalOffset = $(el).outerHeight(true);
  126. if (naturalOffset < minOffset) {
  127. flexEls.push(el);
  128. flexOffsets.push(naturalOffset);
  129. flexHeights.push($(el).height());
  130. }
  131. else {
  132. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  133. usedHeight += naturalOffset;
  134. }
  135. });
  136. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  137. if (shouldRedistribute) {
  138. availableHeight -= usedHeight;
  139. minOffset1 = Math.floor(availableHeight / flexEls.length);
  140. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  141. }
  142. // assign heights to all expandable elements
  143. $(flexEls).each(function(i, el) {
  144. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  145. var naturalOffset = flexOffsets[i];
  146. var naturalHeight = flexHeights[i];
  147. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  148. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  149. $(el).height(newHeight);
  150. }
  151. });
  152. }
  153. // Undoes distrubuteHeight, restoring all els to their natural height
  154. function undistributeHeight(els) {
  155. els.height('');
  156. }
  157. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  158. // cells to be that width.
  159. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  160. function matchCellWidths(els) {
  161. var maxInnerWidth = 0;
  162. els.find('> *').each(function(i, innerEl) {
  163. var innerWidth = $(innerEl).outerWidth();
  164. if (innerWidth > maxInnerWidth) {
  165. maxInnerWidth = innerWidth;
  166. }
  167. });
  168. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  169. els.width(maxInnerWidth);
  170. return maxInnerWidth;
  171. }
  172. // Given one element that resides inside another,
  173. // Subtracts the height of the inner element from the outer element.
  174. function subtractInnerElHeight(outerEl, innerEl) {
  175. var both = outerEl.add(innerEl);
  176. var diff;
  177. // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  178. both.css({
  179. position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  180. left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  181. });
  182. diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
  183. both.css({ position: '', left: '' }); // undo hack
  184. return diff;
  185. }
  186. /* Element Geom Utilities
  187. ----------------------------------------------------------------------------------------------------------------------*/
  188. FC.getOuterRect = getOuterRect;
  189. FC.getClientRect = getClientRect;
  190. FC.getContentRect = getContentRect;
  191. FC.getScrollbarWidths = getScrollbarWidths;
  192. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  193. function getScrollParent(el) {
  194. var position = el.css('position'),
  195. scrollParent = el.parents().filter(function() {
  196. var parent = $(this);
  197. return (/(auto|scroll)/).test(
  198. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  199. );
  200. }).eq(0);
  201. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  202. }
  203. // Queries the outer bounding area of a jQuery element.
  204. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  205. // Origin is optional.
  206. function getOuterRect(el, origin) {
  207. var offset = el.offset();
  208. var left = offset.left - (origin ? origin.left : 0);
  209. var top = offset.top - (origin ? origin.top : 0);
  210. return {
  211. left: left,
  212. right: left + el.outerWidth(),
  213. top: top,
  214. bottom: top + el.outerHeight()
  215. };
  216. }
  217. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  218. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  219. // Origin is optional.
  220. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  221. function getClientRect(el, origin) {
  222. var offset = el.offset();
  223. var scrollbarWidths = getScrollbarWidths(el);
  224. var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
  225. var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
  226. return {
  227. left: left,
  228. right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  229. top: top,
  230. bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  231. };
  232. }
  233. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  234. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  235. // Origin is optional.
  236. function getContentRect(el, origin) {
  237. var offset = el.offset(); // just outside of border, margin not included
  238. var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
  239. (origin ? origin.left : 0);
  240. var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
  241. (origin ? origin.top : 0);
  242. return {
  243. left: left,
  244. right: left + el.width(),
  245. top: top,
  246. bottom: top + el.height()
  247. };
  248. }
  249. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  250. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  251. function getScrollbarWidths(el) {
  252. var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
  253. var widths = {
  254. left: 0,
  255. right: 0,
  256. top: 0,
  257. bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
  258. };
  259. if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  260. widths.left = leftRightWidth;
  261. }
  262. else {
  263. widths.right = leftRightWidth;
  264. }
  265. return widths;
  266. }
  267. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  268. var _isLeftRtlScrollbars = null;
  269. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  270. if (_isLeftRtlScrollbars === null) {
  271. _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  272. }
  273. return _isLeftRtlScrollbars;
  274. }
  275. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  276. var el = $('<div><div/></div>')
  277. .css({
  278. position: 'absolute',
  279. top: -1000,
  280. left: 0,
  281. border: 0,
  282. padding: 0,
  283. overflow: 'scroll',
  284. direction: 'rtl'
  285. })
  286. .appendTo('body');
  287. var innerEl = el.children();
  288. var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  289. el.remove();
  290. return res;
  291. }
  292. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  293. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  294. function getCssFloat(el, prop) {
  295. return parseFloat(el.css(prop)) || 0;
  296. }
  297. /* Mouse / Touch Utilities
  298. ----------------------------------------------------------------------------------------------------------------------*/
  299. FC.preventDefault = preventDefault;
  300. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  301. function isPrimaryMouseButton(ev) {
  302. return ev.which == 1 && !ev.ctrlKey;
  303. }
  304. function getEvX(ev) {
  305. if (ev.pageX !== undefined) {
  306. return ev.pageX;
  307. }
  308. var touches = ev.originalEvent.touches;
  309. if (touches) {
  310. return touches[0].pageX;
  311. }
  312. }
  313. function getEvY(ev) {
  314. if (ev.pageY !== undefined) {
  315. return ev.pageY;
  316. }
  317. var touches = ev.originalEvent.touches;
  318. if (touches) {
  319. return touches[0].pageY;
  320. }
  321. }
  322. function getEvIsTouch(ev) {
  323. return /^touch/.test(ev.type);
  324. }
  325. function preventSelection(el) {
  326. el.addClass('fc-unselectable')
  327. .on('selectstart', preventDefault);
  328. }
  329. // Stops a mouse/touch event from doing it's native browser action
  330. function preventDefault(ev) {
  331. ev.preventDefault();
  332. }
  333. // attach a handler to get called when ANY scroll action happens on the page.
  334. // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
  335. // http://stackoverflow.com/a/32954565/96342
  336. // returns `true` on success.
  337. function bindAnyScroll(handler) {
  338. if (window.addEventListener) {
  339. window.addEventListener('scroll', handler, true); // useCapture=true
  340. return true;
  341. }
  342. return false;
  343. }
  344. // undoes bindAnyScroll. must pass in the original function.
  345. // returns `true` on success.
  346. function unbindAnyScroll(handler) {
  347. if (window.removeEventListener) {
  348. window.removeEventListener('scroll', handler, true); // useCapture=true
  349. return true;
  350. }
  351. return false;
  352. }
  353. /* General Geometry Utils
  354. ----------------------------------------------------------------------------------------------------------------------*/
  355. FC.intersectRects = intersectRects;
  356. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  357. function intersectRects(rect1, rect2) {
  358. var res = {
  359. left: Math.max(rect1.left, rect2.left),
  360. right: Math.min(rect1.right, rect2.right),
  361. top: Math.max(rect1.top, rect2.top),
  362. bottom: Math.min(rect1.bottom, rect2.bottom)
  363. };
  364. if (res.left < res.right && res.top < res.bottom) {
  365. return res;
  366. }
  367. return false;
  368. }
  369. // Returns a new point that will have been moved to reside within the given rectangle
  370. function constrainPoint(point, rect) {
  371. return {
  372. left: Math.min(Math.max(point.left, rect.left), rect.right),
  373. top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  374. };
  375. }
  376. // Returns a point that is the center of the given rectangle
  377. function getRectCenter(rect) {
  378. return {
  379. left: (rect.left + rect.right) / 2,
  380. top: (rect.top + rect.bottom) / 2
  381. };
  382. }
  383. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  384. function diffPoints(point1, point2) {
  385. return {
  386. left: point1.left - point2.left,
  387. top: point1.top - point2.top
  388. };
  389. }
  390. /* Object Ordering by Field
  391. ----------------------------------------------------------------------------------------------------------------------*/
  392. FC.parseFieldSpecs = parseFieldSpecs;
  393. FC.compareByFieldSpecs = compareByFieldSpecs;
  394. FC.compareByFieldSpec = compareByFieldSpec;
  395. FC.flexibleCompare = flexibleCompare;
  396. function parseFieldSpecs(input) {
  397. var specs = [];
  398. var tokens = [];
  399. var i, token;
  400. if (typeof input === 'string') {
  401. tokens = input.split(/\s*,\s*/);
  402. }
  403. else if (typeof input === 'function') {
  404. tokens = [ input ];
  405. }
  406. else if ($.isArray(input)) {
  407. tokens = input;
  408. }
  409. for (i = 0; i < tokens.length; i++) {
  410. token = tokens[i];
  411. if (typeof token === 'string') {
  412. specs.push(
  413. token.charAt(0) == '-' ?
  414. { field: token.substring(1), order: -1 } :
  415. { field: token, order: 1 }
  416. );
  417. }
  418. else if (typeof token === 'function') {
  419. specs.push({ func: token });
  420. }
  421. }
  422. return specs;
  423. }
  424. function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
  425. var i;
  426. var cmp;
  427. for (i = 0; i < fieldSpecs.length; i++) {
  428. cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
  429. if (cmp) {
  430. return cmp;
  431. }
  432. }
  433. return 0;
  434. }
  435. function compareByFieldSpec(obj1, obj2, fieldSpec) {
  436. if (fieldSpec.func) {
  437. return fieldSpec.func(obj1, obj2);
  438. }
  439. return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
  440. (fieldSpec.order || 1);
  441. }
  442. function flexibleCompare(a, b) {
  443. if (!a && !b) {
  444. return 0;
  445. }
  446. if (b == null) {
  447. return -1;
  448. }
  449. if (a == null) {
  450. return 1;
  451. }
  452. if ($.type(a) === 'string' || $.type(b) === 'string') {
  453. return String(a).localeCompare(String(b));
  454. }
  455. return a - b;
  456. }
  457. /* FullCalendar-specific Misc Utilities
  458. ----------------------------------------------------------------------------------------------------------------------*/
  459. // Computes the intersection of the two ranges. Will return fresh date clones in a range.
  460. // Returns undefined if no intersection.
  461. // Expects all dates to be normalized to the same timezone beforehand.
  462. // TODO: move to date section?
  463. function intersectRanges(subjectRange, constraintRange) {
  464. var subjectStart = subjectRange.start;
  465. var subjectEnd = subjectRange.end;
  466. var constraintStart = constraintRange.start;
  467. var constraintEnd = constraintRange.end;
  468. var segStart, segEnd;
  469. var isStart, isEnd;
  470. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  471. if (subjectStart >= constraintStart) {
  472. segStart = subjectStart.clone();
  473. isStart = true;
  474. }
  475. else {
  476. segStart = constraintStart.clone();
  477. isStart = false;
  478. }
  479. if (subjectEnd <= constraintEnd) {
  480. segEnd = subjectEnd.clone();
  481. isEnd = true;
  482. }
  483. else {
  484. segEnd = constraintEnd.clone();
  485. isEnd = false;
  486. }
  487. return {
  488. start: segStart,
  489. end: segEnd,
  490. isStart: isStart,
  491. isEnd: isEnd
  492. };
  493. }
  494. }
  495. /* Date Utilities
  496. ----------------------------------------------------------------------------------------------------------------------*/
  497. FC.computeIntervalUnit = computeIntervalUnit;
  498. FC.divideRangeByDuration = divideRangeByDuration;
  499. FC.divideDurationByDuration = divideDurationByDuration;
  500. FC.multiplyDuration = multiplyDuration;
  501. FC.durationHasTime = durationHasTime;
  502. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  503. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  504. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  505. // Moments will have their timezones normalized.
  506. function diffDayTime(a, b) {
  507. return moment.duration({
  508. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  509. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  510. });
  511. }
  512. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  513. function diffDay(a, b) {
  514. return moment.duration({
  515. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  516. });
  517. }
  518. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  519. function diffByUnit(a, b, unit) {
  520. return moment.duration(
  521. Math.round(a.diff(b, unit, true)), // returnFloat=true
  522. unit
  523. );
  524. }
  525. // Computes the unit name of the largest whole-unit period of time.
  526. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  527. // Accepts start/end, a range object, or an original duration object.
  528. function computeIntervalUnit(start, end) {
  529. var i, unit;
  530. var val;
  531. for (i = 0; i < intervalUnits.length; i++) {
  532. unit = intervalUnits[i];
  533. val = computeRangeAs(unit, start, end);
  534. if (val >= 1 && isInt(val)) {
  535. break;
  536. }
  537. }
  538. return unit; // will be "milliseconds" if nothing else matches
  539. }
  540. // Computes the number of units (like "hours") in the given range.
  541. // Range can be a {start,end} object, separate start/end args, or a Duration.
  542. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  543. // of month-diffing logic (which tends to vary from version to version).
  544. function computeRangeAs(unit, start, end) {
  545. if (end != null) { // given start, end
  546. return end.diff(start, unit, true);
  547. }
  548. else if (moment.isDuration(start)) { // given duration
  549. return start.as(unit);
  550. }
  551. else { // given { start, end } range object
  552. return start.end.diff(start.start, unit, true);
  553. }
  554. }
  555. // Intelligently divides a range (specified by a start/end params) by a duration
  556. function divideRangeByDuration(start, end, dur) {
  557. var months;
  558. if (durationHasTime(dur)) {
  559. return (end - start) / dur;
  560. }
  561. months = dur.asMonths();
  562. if (Math.abs(months) >= 1 && isInt(months)) {
  563. return end.diff(start, 'months', true) / months;
  564. }
  565. return end.diff(start, 'days', true) / dur.asDays();
  566. }
  567. // Intelligently divides one duration by another
  568. function divideDurationByDuration(dur1, dur2) {
  569. var months1, months2;
  570. if (durationHasTime(dur1) || durationHasTime(dur2)) {
  571. return dur1 / dur2;
  572. }
  573. months1 = dur1.asMonths();
  574. months2 = dur2.asMonths();
  575. if (
  576. Math.abs(months1) >= 1 && isInt(months1) &&
  577. Math.abs(months2) >= 1 && isInt(months2)
  578. ) {
  579. return months1 / months2;
  580. }
  581. return dur1.asDays() / dur2.asDays();
  582. }
  583. // Intelligently multiplies a duration by a number
  584. function multiplyDuration(dur, n) {
  585. var months;
  586. if (durationHasTime(dur)) {
  587. return moment.duration(dur * n);
  588. }
  589. months = dur.asMonths();
  590. if (Math.abs(months) >= 1 && isInt(months)) {
  591. return moment.duration({ months: months * n });
  592. }
  593. return moment.duration({ days: dur.asDays() * n });
  594. }
  595. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  596. function durationHasTime(dur) {
  597. return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  598. }
  599. function isNativeDate(input) {
  600. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  601. }
  602. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  603. function isTimeString(str) {
  604. return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  605. }
  606. /* Logging and Debug
  607. ----------------------------------------------------------------------------------------------------------------------*/
  608. FC.log = function() {
  609. var console = window.console;
  610. if (console && console.log) {
  611. return console.log.apply(console, arguments);
  612. }
  613. };
  614. FC.warn = function() {
  615. var console = window.console;
  616. if (console && console.warn) {
  617. return console.warn.apply(console, arguments);
  618. }
  619. else {
  620. return FC.log.apply(FC, arguments);
  621. }
  622. };
  623. /* General Utilities
  624. ----------------------------------------------------------------------------------------------------------------------*/
  625. var hasOwnPropMethod = {}.hasOwnProperty;
  626. // Merges an array of objects into a single object.
  627. // The second argument allows for an array of property names who's object values will be merged together.
  628. function mergeProps(propObjs, complexProps) {
  629. var dest = {};
  630. var i, name;
  631. var complexObjs;
  632. var j, val;
  633. var props;
  634. if (complexProps) {
  635. for (i = 0; i < complexProps.length; i++) {
  636. name = complexProps[i];
  637. complexObjs = [];
  638. // collect the trailing object values, stopping when a non-object is discovered
  639. for (j = propObjs.length - 1; j >= 0; j--) {
  640. val = propObjs[j][name];
  641. if (typeof val === 'object') {
  642. complexObjs.unshift(val);
  643. }
  644. else if (val !== undefined) {
  645. dest[name] = val; // if there were no objects, this value will be used
  646. break;
  647. }
  648. }
  649. // if the trailing values were objects, use the merged value
  650. if (complexObjs.length) {
  651. dest[name] = mergeProps(complexObjs);
  652. }
  653. }
  654. }
  655. // copy values into the destination, going from last to first
  656. for (i = propObjs.length - 1; i >= 0; i--) {
  657. props = propObjs[i];
  658. for (name in props) {
  659. if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  660. dest[name] = props[name];
  661. }
  662. }
  663. }
  664. return dest;
  665. }
  666. // Create an object that has the given prototype. Just like Object.create
  667. function createObject(proto) {
  668. var f = function() {};
  669. f.prototype = proto;
  670. return new f();
  671. }
  672. FC.createObject = createObject;
  673. function copyOwnProps(src, dest) {
  674. for (var name in src) {
  675. if (hasOwnProp(src, name)) {
  676. dest[name] = src[name];
  677. }
  678. }
  679. }
  680. function hasOwnProp(obj, name) {
  681. return hasOwnPropMethod.call(obj, name);
  682. }
  683. // Is the given value a non-object non-function value?
  684. function isAtomic(val) {
  685. return /undefined|null|boolean|number|string/.test($.type(val));
  686. }
  687. function applyAll(functions, thisObj, args) {
  688. if ($.isFunction(functions)) {
  689. functions = [ functions ];
  690. }
  691. if (functions) {
  692. var i;
  693. var ret;
  694. for (i=0; i<functions.length; i++) {
  695. ret = functions[i].apply(thisObj, args) || ret;
  696. }
  697. return ret;
  698. }
  699. }
  700. function firstDefined() {
  701. for (var i=0; i<arguments.length; i++) {
  702. if (arguments[i] !== undefined) {
  703. return arguments[i];
  704. }
  705. }
  706. }
  707. function htmlEscape(s) {
  708. return (s + '').replace(/&/g, '&amp;')
  709. .replace(/</g, '&lt;')
  710. .replace(/>/g, '&gt;')
  711. .replace(/'/g, '&#039;')
  712. .replace(/"/g, '&quot;')
  713. .replace(/\n/g, '<br />');
  714. }
  715. function stripHtmlEntities(text) {
  716. return text.replace(/&.*?;/g, '');
  717. }
  718. // Given a hash of CSS properties, returns a string of CSS.
  719. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  720. function cssToStr(cssProps) {
  721. var statements = [];
  722. $.each(cssProps, function(name, val) {
  723. if (val != null) {
  724. statements.push(name + ':' + val);
  725. }
  726. });
  727. return statements.join(';');
  728. }
  729. // Given an object hash of HTML attribute names to values,
  730. // generates a string that can be injected between < > in HTML
  731. function attrsToStr(attrs) {
  732. var parts = [];
  733. $.each(attrs, function(name, val) {
  734. if (val != null) {
  735. parts.push(name + '="' + htmlEscape(val) + '"');
  736. }
  737. });
  738. return parts.join(' ');
  739. }
  740. function capitaliseFirstLetter(str) {
  741. return str.charAt(0).toUpperCase() + str.slice(1);
  742. }
  743. function compareNumbers(a, b) { // for .sort()
  744. return a - b;
  745. }
  746. function isInt(n) {
  747. return n % 1 === 0;
  748. }
  749. // Returns a method bound to the given object context.
  750. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  751. // different contexts as identical when binding/unbinding events.
  752. function proxy(obj, methodName) {
  753. var method = obj[methodName];
  754. return function() {
  755. return method.apply(obj, arguments);
  756. };
  757. }
  758. // Returns a function, that, as long as it continues to be invoked, will not
  759. // be triggered. The function will be called after it stops being called for
  760. // N milliseconds. If `immediate` is passed, trigger the function on the
  761. // leading edge, instead of the trailing.
  762. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  763. function debounce(func, wait, immediate) {
  764. var timeout, args, context, timestamp, result;
  765. var later = function() {
  766. var last = +new Date() - timestamp;
  767. if (last < wait) {
  768. timeout = setTimeout(later, wait - last);
  769. }
  770. else {
  771. timeout = null;
  772. if (!immediate) {
  773. result = func.apply(context, args);
  774. context = args = null;
  775. }
  776. }
  777. };
  778. return function() {
  779. context = this;
  780. args = arguments;
  781. timestamp = +new Date();
  782. var callNow = immediate && !timeout;
  783. if (!timeout) {
  784. timeout = setTimeout(later, wait);
  785. }
  786. if (callNow) {
  787. result = func.apply(context, args);
  788. context = args = null;
  789. }
  790. return result;
  791. };
  792. }
  793. ;;
  794. /*
  795. GENERAL NOTE on moments throughout the *entire rest* of the codebase:
  796. All moments are assumed to be ambiguously-zoned unless otherwise noted,
  797. with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
  798. Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
  799. */
  800. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  801. var ambigTimeOrZoneRegex =
  802. /^\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+)?)?)?)?)?$/;
  803. var newMomentProto = moment.fn; // where we will attach our new methods
  804. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  805. // tell momentjs to transfer these properties upon clone
  806. var momentProperties = moment.momentProperties;
  807. momentProperties.push('_fullCalendar');
  808. momentProperties.push('_ambigTime');
  809. momentProperties.push('_ambigZone');
  810. // Creating
  811. // -------------------------------------------------------------------------------------------------
  812. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  813. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  814. // it will function as a clone (and retain the zone of the moment). Anything else will
  815. // result in a moment in the local zone.
  816. FC.moment = function() {
  817. return makeMoment(arguments);
  818. };
  819. // Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
  820. FC.moment.utc = function() {
  821. var mom = makeMoment(arguments, true);
  822. // Force it into UTC because makeMoment doesn't guarantee it
  823. // (if given a pre-existing moment for example)
  824. if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  825. mom.utc();
  826. }
  827. return mom;
  828. };
  829. // Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
  830. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  831. FC.moment.parseZone = function() {
  832. return makeMoment(arguments, true, true);
  833. };
  834. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  835. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  836. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  837. // parseAsUTC - if there is no zone information, should we parse the input in UTC?
  838. // parseZone - if there is zone information, should we force the zone of the moment?
  839. function makeMoment(args, parseAsUTC, parseZone) {
  840. var input = args[0];
  841. var isSingleString = args.length == 1 && typeof input === 'string';
  842. var isAmbigTime;
  843. var isAmbigZone;
  844. var ambigMatch;
  845. var mom;
  846. if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
  847. mom = moment.apply(null, args);
  848. }
  849. else { // "parsing" is required
  850. isAmbigTime = false;
  851. isAmbigZone = false;
  852. if (isSingleString) {
  853. if (ambigDateOfMonthRegex.test(input)) {
  854. // accept strings like '2014-05', but convert to the first of the month
  855. input += '-01';
  856. args = [ input ]; // for when we pass it on to moment's constructor
  857. isAmbigTime = true;
  858. isAmbigZone = true;
  859. }
  860. else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  861. isAmbigTime = !ambigMatch[5]; // no time part?
  862. isAmbigZone = true;
  863. }
  864. }
  865. else if ($.isArray(input)) {
  866. // arrays have no timezone information, so assume ambiguous zone
  867. isAmbigZone = true;
  868. }
  869. // otherwise, probably a string with a format
  870. if (parseAsUTC || isAmbigTime) {
  871. mom = moment.utc.apply(moment, args);
  872. }
  873. else {
  874. mom = moment.apply(null, args);
  875. }
  876. if (isAmbigTime) {
  877. mom._ambigTime = true;
  878. mom._ambigZone = true; // ambiguous time always means ambiguous zone
  879. }
  880. else if (parseZone) { // let's record the inputted zone somehow
  881. if (isAmbigZone) {
  882. mom._ambigZone = true;
  883. }
  884. else if (isSingleString) {
  885. mom.utcOffset(input); // if not a valid zone, will assign UTC
  886. }
  887. }
  888. }
  889. mom._fullCalendar = true; // flag for extended functionality
  890. return mom;
  891. }
  892. // Week Number
  893. // -------------------------------------------------------------------------------------------------
  894. // Returns the week number, considering the locale's custom week number calcuation
  895. // `weeks` is an alias for `week`
  896. newMomentProto.week = newMomentProto.weeks = function(input) {
  897. var weekCalc = this._locale._fullCalendar_weekCalc;
  898. if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
  899. return weekCalc(this);
  900. }
  901. else if (weekCalc === 'ISO') {
  902. return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
  903. }
  904. return oldMomentProto.week.apply(this, arguments); // local getter/setter
  905. };
  906. // Time-of-day
  907. // -------------------------------------------------------------------------------------------------
  908. // GETTER
  909. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  910. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  911. //
  912. // SETTER
  913. // You can supply a Duration, a Moment, or a Duration-like argument.
  914. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  915. newMomentProto.time = function(time) {
  916. // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  917. // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  918. if (!this._fullCalendar) {
  919. return oldMomentProto.time.apply(this, arguments);
  920. }
  921. if (time == null) { // getter
  922. return moment.duration({
  923. hours: this.hours(),
  924. minutes: this.minutes(),
  925. seconds: this.seconds(),
  926. milliseconds: this.milliseconds()
  927. });
  928. }
  929. else { // setter
  930. this._ambigTime = false; // mark that the moment now has a time
  931. if (!moment.isDuration(time) && !moment.isMoment(time)) {
  932. time = moment.duration(time);
  933. }
  934. // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  935. // Only for Duration times, not Moment times.
  936. var dayHours = 0;
  937. if (moment.isDuration(time)) {
  938. dayHours = Math.floor(time.asDays()) * 24;
  939. }
  940. // We need to set the individual fields.
  941. // Can't use startOf('day') then add duration. In case of DST at start of day.
  942. return this.hours(dayHours + time.hours())
  943. .minutes(time.minutes())
  944. .seconds(time.seconds())
  945. .milliseconds(time.milliseconds());
  946. }
  947. };
  948. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  949. // but preserving its YMD. A moment with a stripped time will display no time
  950. // nor timezone offset when .format() is called.
  951. newMomentProto.stripTime = function() {
  952. if (!this._ambigTime) {
  953. this.utc(true); // keepLocalTime=true (for keeping *date* value)
  954. // set time to zero
  955. this.set({
  956. hours: 0,
  957. minutes: 0,
  958. seconds: 0,
  959. ms: 0
  960. });
  961. // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  962. // which clears all ambig flags.
  963. this._ambigTime = true;
  964. this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  965. }
  966. return this; // for chaining
  967. };
  968. // Returns if the moment has a non-ambiguous time (boolean)
  969. newMomentProto.hasTime = function() {
  970. return !this._ambigTime;
  971. };
  972. // Timezone
  973. // -------------------------------------------------------------------------------------------------
  974. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  975. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  976. // timezone offset when .format() is called.
  977. newMomentProto.stripZone = function() {
  978. var wasAmbigTime;
  979. if (!this._ambigZone) {
  980. wasAmbigTime = this._ambigTime;
  981. this.utc(true); // keepLocalTime=true (for keeping date and time values)
  982. // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
  983. this._ambigTime = wasAmbigTime || false;
  984. // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  985. // which clears the ambig flags.
  986. this._ambigZone = true;
  987. }
  988. return this; // for chaining
  989. };
  990. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  991. newMomentProto.hasZone = function() {
  992. return !this._ambigZone;
  993. };
  994. // implicitly marks a zone
  995. newMomentProto.local = function(keepLocalTime) {
  996. // for when converting from ambiguously-zoned to local,
  997. // keep the time values when converting from UTC -> local
  998. oldMomentProto.local.call(this, this._ambigZone || keepLocalTime);
  999. // ensure non-ambiguous
  1000. // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
  1001. this._ambigTime = false;
  1002. this._ambigZone = false;
  1003. return this; // for chaining
  1004. };
  1005. // implicitly marks a zone
  1006. newMomentProto.utc = function(keepLocalTime) {
  1007. oldMomentProto.utc.call(this, keepLocalTime);
  1008. // ensure non-ambiguous
  1009. // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
  1010. this._ambigTime = false;
  1011. this._ambigZone = false;
  1012. return this;
  1013. };
  1014. // implicitly marks a zone (will probably get called upon .utc() and .local())
  1015. newMomentProto.utcOffset = function(tzo) {
  1016. if (tzo != null) { // setter
  1017. // these assignments needs to happen before the original zone method is called.
  1018. // I forget why, something to do with a browser crash.
  1019. this._ambigTime = false;
  1020. this._ambigZone = false;
  1021. }
  1022. return oldMomentProto.utcOffset.apply(this, arguments);
  1023. };
  1024. // Formatting
  1025. // -------------------------------------------------------------------------------------------------
  1026. newMomentProto.format = function() {
  1027. if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  1028. return formatDate(this, arguments[0]); // our extended formatting
  1029. }
  1030. if (this._ambigTime) {
  1031. return oldMomentFormat(this, 'YYYY-MM-DD');
  1032. }
  1033. if (this._ambigZone) {
  1034. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1035. }
  1036. return oldMomentProto.format.apply(this, arguments);
  1037. };
  1038. newMomentProto.toISOString = function() {
  1039. if (this._ambigTime) {
  1040. return oldMomentFormat(this, 'YYYY-MM-DD');
  1041. }
  1042. if (this._ambigZone) {
  1043. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1044. }
  1045. return oldMomentProto.toISOString.apply(this, arguments);
  1046. };
  1047. ;;
  1048. // Single Date Formatting
  1049. // -------------------------------------------------------------------------------------------------
  1050. // call this if you want Moment's original format method to be used
  1051. function oldMomentFormat(mom, formatStr) {
  1052. return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  1053. }
  1054. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1055. // additional token.
  1056. function formatDate(date, formatStr) {
  1057. return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1058. }
  1059. function formatDateWithChunks(date, chunks) {
  1060. var s = '';
  1061. var i;
  1062. for (i=0; i<chunks.length; i++) {
  1063. s += formatDateWithChunk(date, chunks[i]);
  1064. }
  1065. return s;
  1066. }
  1067. // addition formatting tokens we want recognized
  1068. var tokenOverrides = {
  1069. t: function(date) { // "a" or "p"
  1070. return oldMomentFormat(date, 'a').charAt(0);
  1071. },
  1072. T: function(date) { // "A" or "P"
  1073. return oldMomentFormat(date, 'A').charAt(0);
  1074. }
  1075. };
  1076. function formatDateWithChunk(date, chunk) {
  1077. var token;
  1078. var maybeStr;
  1079. if (typeof chunk === 'string') { // a literal string
  1080. return chunk;
  1081. }
  1082. else if ((token = chunk.token)) { // a token, like "YYYY"
  1083. if (tokenOverrides[token]) {
  1084. return tokenOverrides[token](date); // use our custom token
  1085. }
  1086. return oldMomentFormat(date, token);
  1087. }
  1088. else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  1089. maybeStr = formatDateWithChunks(date, chunk.maybe);
  1090. if (maybeStr.match(/[1-9]/)) {
  1091. return maybeStr;
  1092. }
  1093. }
  1094. return '';
  1095. }
  1096. // Date Range Formatting
  1097. // -------------------------------------------------------------------------------------------------
  1098. // TODO: make it work with timezone offset
  1099. // Using a formatting string meant for a single date, generate a range string, like
  1100. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  1101. // If the dates are the same as far as the format string is concerned, just return a single
  1102. // rendering of one date, without any separator.
  1103. function formatRange(date1, date2, formatStr, separator, isRTL) {
  1104. var localeData;
  1105. date1 = FC.moment.parseZone(date1);
  1106. date2 = FC.moment.parseZone(date2);
  1107. localeData = date1.localeData();
  1108. // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  1109. formatStr = localeData.longDateFormat(formatStr) || formatStr;
  1110. // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  1111. // or non-zero areas in Moment's localized format strings.
  1112. separator = separator || ' - ';
  1113. return formatRangeWithChunks(
  1114. date1,
  1115. date2,
  1116. getFormatStringChunks(formatStr),
  1117. separator,
  1118. isRTL
  1119. );
  1120. }
  1121. FC.formatRange = formatRange; // expose
  1122. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  1123. var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk
  1124. var unzonedDate2 = date2.clone().stripZone(); // "
  1125. var chunkStr; // the rendering of the chunk
  1126. var leftI;
  1127. var leftStr = '';
  1128. var rightI;
  1129. var rightStr = '';
  1130. var middleI;
  1131. var middleStr1 = '';
  1132. var middleStr2 = '';
  1133. var middleStr = '';
  1134. // Start at the leftmost side of the formatting string and continue until you hit a token
  1135. // that is not the same between dates.
  1136. for (leftI=0; leftI<chunks.length; leftI++) {
  1137. chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]);
  1138. if (chunkStr === false) {
  1139. break;
  1140. }
  1141. leftStr += chunkStr;
  1142. }
  1143. // Similarly, start at the rightmost side of the formatting string and move left
  1144. for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  1145. chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]);
  1146. if (chunkStr === false) {
  1147. break;
  1148. }
  1149. rightStr = chunkStr + rightStr;
  1150. }
  1151. // The area in the middle is different for both of the dates.
  1152. // Collect them distinctly so we can jam them together later.
  1153. for (middleI=leftI; middleI<=rightI; middleI++) {
  1154. middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  1155. middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  1156. }
  1157. if (middleStr1 || middleStr2) {
  1158. if (isRTL) {
  1159. middleStr = middleStr2 + separator + middleStr1;
  1160. }
  1161. else {
  1162. middleStr = middleStr1 + separator + middleStr2;
  1163. }
  1164. }
  1165. return leftStr + middleStr + rightStr;
  1166. }
  1167. var similarUnitMap = {
  1168. Y: 'year',
  1169. M: 'month',
  1170. D: 'day', // day of month
  1171. d: 'day', // day of week
  1172. // prevents a separator between anything time-related...
  1173. A: 'second', // AM/PM
  1174. a: 'second', // am/pm
  1175. T: 'second', // A/P
  1176. t: 'second', // a/p
  1177. H: 'second', // hour (24)
  1178. h: 'second', // hour (12)
  1179. m: 'second', // minute
  1180. s: 'second' // second
  1181. };
  1182. // TODO: week maybe?
  1183. // Given a formatting chunk, and given that both dates are similar in the regard the
  1184. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  1185. function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) {
  1186. var token;
  1187. var unit;
  1188. if (typeof chunk === 'string') { // a literal string
  1189. return chunk;
  1190. }
  1191. else if ((token = chunk.token)) {
  1192. unit = similarUnitMap[token.charAt(0)];
  1193. // are the dates the same for this unit of measurement?
  1194. // use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
  1195. if (unit && unzonedDate1.isSame(unzonedDate2, unit)) {
  1196. return oldMomentFormat(date1, token); // would be the same if we used `date2`
  1197. // BTW, don't support custom tokens
  1198. }
  1199. }
  1200. return false; // the chunk is NOT the same for the two dates
  1201. // BTW, don't support splitting on non-zero areas
  1202. }
  1203. // Chunking Utils
  1204. // -------------------------------------------------------------------------------------------------
  1205. var formatStringChunkCache = {};
  1206. function getFormatStringChunks(formatStr) {
  1207. if (formatStr in formatStringChunkCache) {
  1208. return formatStringChunkCache[formatStr];
  1209. }
  1210. return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  1211. }
  1212. // Break the formatting string into an array of chunks
  1213. function chunkFormatString(formatStr) {
  1214. var chunks = [];
  1215. var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  1216. var match;
  1217. while ((match = chunker.exec(formatStr))) {
  1218. if (match[1]) { // a literal string inside [ ... ]
  1219. chunks.push(match[1]);
  1220. }
  1221. else if (match[2]) { // non-zero formatting inside ( ... )
  1222. chunks.push({ maybe: chunkFormatString(match[2]) });
  1223. }
  1224. else if (match[3]) { // a formatting token
  1225. chunks.push({ token: match[3] });
  1226. }
  1227. else if (match[5]) { // an unenclosed literal string
  1228. chunks.push(match[5]);
  1229. }
  1230. }
  1231. return chunks;
  1232. }
  1233. // Misc Utils
  1234. // -------------------------------------------------------------------------------------------------
  1235. // granularity only goes up until day
  1236. // TODO: unify with similarUnitMap
  1237. var tokenGranularities = {
  1238. Y: { value: 1, unit: 'year' },
  1239. M: { value: 2, unit: 'month' },
  1240. W: { value: 3, unit: 'week' },
  1241. w: { value: 3, unit: 'week' },
  1242. D: { value: 4, unit: 'day' }, // day of month
  1243. d: { value: 4, unit: 'day' } // day of week
  1244. };
  1245. // returns a unit string, either 'year', 'month', 'day', or null
  1246. // for the most granular formatting token in the string.
  1247. FC.queryMostGranularFormatUnit = function(formatStr) {
  1248. var chunks = getFormatStringChunks(formatStr);
  1249. var i, chunk;
  1250. var candidate;
  1251. var best;
  1252. for (i = 0; i < chunks.length; i++) {
  1253. chunk = chunks[i];
  1254. if (chunk.token) {
  1255. candidate = tokenGranularities[chunk.token.charAt(0)];
  1256. if (candidate) {
  1257. if (!best || candidate.value > best.value) {
  1258. best = candidate;
  1259. }
  1260. }
  1261. }
  1262. }
  1263. if (best) {
  1264. return best.unit;
  1265. }
  1266. return null;
  1267. };
  1268. ;;
  1269. FC.Class = Class; // export
  1270. // Class that all other classes will inherit from
  1271. function Class() { }
  1272. // Called on a class to create a subclass.
  1273. // Last argument contains instance methods. Any argument before the last are considered mixins.
  1274. Class.extend = function() {
  1275. var len = arguments.length;
  1276. var i;
  1277. var members;
  1278. for (i = 0; i < len; i++) {
  1279. members = arguments[i];
  1280. if (i < len - 1) { // not the last argument?
  1281. mixIntoClass(this, members);
  1282. }
  1283. }
  1284. return extendClass(this, members || {}); // members will be undefined if no arguments
  1285. };
  1286. // Adds new member variables/methods to the class's prototype.
  1287. // Can be called with another class, or a plain object hash containing new members.
  1288. Class.mixin = function(members) {
  1289. mixIntoClass(this, members);
  1290. };
  1291. function extendClass(superClass, members) {
  1292. var subClass;
  1293. // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
  1294. if (hasOwnProp(members, 'constructor')) {
  1295. subClass = members.constructor;
  1296. }
  1297. if (typeof subClass !== 'function') {
  1298. subClass = members.constructor = function() {
  1299. superClass.apply(this, arguments);
  1300. };
  1301. }
  1302. // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
  1303. subClass.prototype = createObject(superClass.prototype);
  1304. // copy each member variable/method onto the the subclass's prototype
  1305. copyOwnProps(members, subClass.prototype);
  1306. // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
  1307. copyOwnProps(superClass, subClass);
  1308. return subClass;
  1309. }
  1310. function mixIntoClass(theClass, members) {
  1311. copyOwnProps(members, theClass.prototype);
  1312. }
  1313. ;;
  1314. /*
  1315. Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
  1316. With the added non-standard feature of synchronously executing handlers on resolved promises,
  1317. which doesn't always happen otherwise (esp with nested .then handlers!?),
  1318. so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
  1319. TODO: write tests and more comments
  1320. */
  1321. function Promise(executor) {
  1322. var deferred = $.Deferred();
  1323. var promise = deferred.promise();
  1324. if (typeof executor === 'function') {
  1325. executor(
  1326. function(value) { // resolve
  1327. if (Promise.immediate) {
  1328. promise._value = value;
  1329. }
  1330. deferred.resolve(value);
  1331. },
  1332. function() { // reject
  1333. deferred.reject();
  1334. }
  1335. );
  1336. }
  1337. if (Promise.immediate) {
  1338. var origThen = promise.then;
  1339. promise.then = function(onFulfilled, onRejected) {
  1340. var state = promise.state();
  1341. if (state === 'resolved') {
  1342. if (typeof onFulfilled === 'function') {
  1343. return Promise.resolve(onFulfilled(promise._value));
  1344. }
  1345. }
  1346. else if (state === 'rejected') {
  1347. if (typeof onRejected === 'function') {
  1348. onRejected();
  1349. return promise; // already rejected
  1350. }
  1351. }
  1352. return origThen.call(promise, onFulfilled, onRejected);
  1353. };
  1354. }
  1355. return promise; // instanceof Promise will break :( TODO: make Promise a real class
  1356. }
  1357. FC.Promise = Promise;
  1358. Promise.immediate = true;
  1359. Promise.resolve = function(value) {
  1360. if (value && typeof value.resolve === 'function') {
  1361. return value.promise();
  1362. }
  1363. if (value && typeof value.then === 'function') {
  1364. return value;
  1365. }
  1366. else {
  1367. var deferred = $.Deferred().resolve(value);
  1368. var promise = deferred.promise();
  1369. if (Promise.immediate) {
  1370. var origThen = promise.then;
  1371. promise._value = value;
  1372. promise.then = function(onFulfilled, onRejected) {
  1373. if (typeof onFulfilled === 'function') {
  1374. return Promise.resolve(onFulfilled(value));
  1375. }
  1376. return origThen.call(promise, onFulfilled, onRejected);
  1377. };
  1378. }
  1379. return promise;
  1380. }
  1381. };
  1382. Promise.reject = function() {
  1383. return $.Deferred().reject().promise();
  1384. };
  1385. Promise.all = function(inputs) {
  1386. var hasAllValues = false;
  1387. var values;
  1388. var i, input;
  1389. if (Promise.immediate) {
  1390. hasAllValues = true;
  1391. values = [];
  1392. for (i = 0; i < inputs.length; i++) {
  1393. input = inputs[i];
  1394. if (input && typeof input.state === 'function' && input.state() === 'resolved' && ('_value' in input)) {
  1395. values.push(input._value);
  1396. }
  1397. else if (input && typeof input.then === 'function') {
  1398. hasAllValues = false;
  1399. break;
  1400. }
  1401. else {
  1402. values.push(input);
  1403. }
  1404. }
  1405. }
  1406. if (hasAllValues) {
  1407. return Promise.resolve(values);
  1408. }
  1409. else {
  1410. return $.when.apply($.when, inputs).then(function() {
  1411. return $.when($.makeArray(arguments));
  1412. });
  1413. }
  1414. };
  1415. ;;
  1416. // TODO: write tests and clean up code
  1417. function TaskQueue(debounceWait) {
  1418. var q = []; // array of runFuncs
  1419. function addTask(taskFunc) {
  1420. return new Promise(function(resolve) {
  1421. // should run this function when it's taskFunc's turn to run.
  1422. // responsible for popping itself off the queue.
  1423. var runFunc = function() {
  1424. Promise.resolve(taskFunc()) // result might be async, coerce to promise
  1425. .then(resolve) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
  1426. .then(function() {
  1427. q.shift(); // pop itself off
  1428. // run the next task, if any
  1429. if (q.length) {
  1430. q[0]();
  1431. }
  1432. });
  1433. };
  1434. // always put the task at the end of the queue, BEFORE running the task
  1435. q.push(runFunc);
  1436. // if it's the only task in the queue, run immediately
  1437. if (q.length === 1) {
  1438. runFunc();
  1439. }
  1440. });
  1441. }
  1442. this.add = // potentially debounce, for the public method
  1443. typeof debounceWait === 'number' ?
  1444. debounce(addTask, debounceWait) :
  1445. addTask; // if not a number (null/undefined/false), no debounce at all
  1446. this.addQuickly = addTask; // guaranteed no debounce
  1447. }
  1448. FC.TaskQueue = TaskQueue;
  1449. /*
  1450. q = new TaskQueue();
  1451. function work(i) {
  1452. return q.push(function() {
  1453. trigger();
  1454. console.log('work' + i);
  1455. });
  1456. }
  1457. var cnt = 0;
  1458. function trigger() {
  1459. if (cnt < 5) {
  1460. cnt++;
  1461. work(cnt);
  1462. }
  1463. }
  1464. work(9);
  1465. */
  1466. ;;
  1467. var EmitterMixin = FC.EmitterMixin = {
  1468. // jQuery-ification via $(this) allows a non-DOM object to have
  1469. // the same event handling capabilities (including namespaces).
  1470. on: function(types, handler) {
  1471. $(this).on(types, this._prepareIntercept(handler));
  1472. return this; // for chaining
  1473. },
  1474. one: function(types, handler) {
  1475. $(this).one(types, this._prepareIntercept(handler));
  1476. return this; // for chaining
  1477. },
  1478. _prepareIntercept: function(handler) {
  1479. // handlers are always called with an "event" object as their first param.
  1480. // sneak the `this` context and arguments into the extra parameter object
  1481. // and forward them on to the original handler.
  1482. var intercept = function(ev, extra) {
  1483. return handler.apply(
  1484. extra.context || this,
  1485. extra.args || []
  1486. );
  1487. };
  1488. // mimick jQuery's internal "proxy" system (risky, I know)
  1489. // causing all functions with the same .guid to appear to be the same.
  1490. // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
  1491. // this is needed for calling .off with the original non-intercept handler.
  1492. if (!handler.guid) {
  1493. handler.guid = $.guid++;
  1494. }
  1495. intercept.guid = handler.guid;
  1496. return intercept;
  1497. },
  1498. off: function(types, handler) {
  1499. $(this).off(types, handler);
  1500. return this; // for chaining
  1501. },
  1502. trigger: function(types) {
  1503. var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
  1504. // pass in "extra" info to the intercept
  1505. $(this).triggerHandler(types, { args: args });
  1506. return this; // for chaining
  1507. },
  1508. triggerWith: function(types, context, args) {
  1509. // `triggerHandler` is less reliant on the DOM compared to `trigger`.
  1510. // pass in "extra" info to the intercept.
  1511. $(this).triggerHandler(types, { context: context, args: args });
  1512. return this; // for chaining
  1513. }
  1514. };
  1515. ;;
  1516. /*
  1517. Utility methods for easily listening to events on another object,
  1518. and more importantly, easily unlistening from them.
  1519. */
  1520. var ListenerMixin = FC.ListenerMixin = (function() {
  1521. var guid = 0;
  1522. var ListenerMixin = {
  1523. listenerId: null,
  1524. /*
  1525. Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
  1526. The `callback` will be called with the `this` context of the object that .listenTo is being called on.
  1527. Can be called:
  1528. .listenTo(other, eventName, callback)
  1529. OR
  1530. .listenTo(other, {
  1531. eventName1: callback1,
  1532. eventName2: callback2
  1533. })
  1534. */
  1535. listenTo: function(other, arg, callback) {
  1536. if (typeof arg === 'object') { // given dictionary of callbacks
  1537. for (var eventName in arg) {
  1538. if (arg.hasOwnProperty(eventName)) {
  1539. this.listenTo(other, eventName, arg[eventName]);
  1540. }
  1541. }
  1542. }
  1543. else if (typeof arg === 'string') {
  1544. other.on(
  1545. arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
  1546. $.proxy(callback, this) // always use `this` context
  1547. // the usually-undesired jQuery guid behavior doesn't matter,
  1548. // because we always unbind via namespace
  1549. );
  1550. }
  1551. },
  1552. /*
  1553. Causes the current object to stop listening to events on the `other` object.
  1554. `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
  1555. */
  1556. stopListeningTo: function(other, eventName) {
  1557. other.off((eventName || '') + '.' + this.getListenerNamespace());
  1558. },
  1559. /*
  1560. Returns a string, unique to this object, to be used for event namespacing
  1561. */
  1562. getListenerNamespace: function() {
  1563. if (this.listenerId == null) {
  1564. this.listenerId = guid++;
  1565. }
  1566. return '_listener' + this.listenerId;
  1567. }
  1568. };
  1569. return ListenerMixin;
  1570. })();
  1571. ;;
  1572. // simple class for toggle a `isIgnoringMouse` flag on delay
  1573. // initMouseIgnoring must first be called, with a millisecond delay setting.
  1574. var MouseIgnorerMixin = {
  1575. isIgnoringMouse: false, // bool
  1576. delayUnignoreMouse: null, // method
  1577. initMouseIgnoring: function(delay) {
  1578. this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
  1579. },
  1580. // temporarily ignore mouse actions on segments
  1581. tempIgnoreMouse: function() {
  1582. this.isIgnoringMouse = true;
  1583. this.delayUnignoreMouse();
  1584. },
  1585. // delayUnignoreMouse eventually calls this
  1586. unignoreMouse: function() {
  1587. this.isIgnoringMouse = false;
  1588. }
  1589. };
  1590. ;;
  1591. /* A rectangular panel that is absolutely positioned over other content
  1592. ------------------------------------------------------------------------------------------------------------------------
  1593. Options:
  1594. - className (string)
  1595. - content (HTML string or jQuery element set)
  1596. - parentEl
  1597. - top
  1598. - left
  1599. - right (the x coord of where the right edge should be. not a "CSS" right)
  1600. - autoHide (boolean)
  1601. - show (callback)
  1602. - hide (callback)
  1603. */
  1604. var Popover = Class.extend(ListenerMixin, {
  1605. isHidden: true,
  1606. options: null,
  1607. el: null, // the container element for the popover. generated by this object
  1608. margin: 10, // the space required between the popover and the edges of the scroll container
  1609. constructor: function(options) {
  1610. this.options = options || {};
  1611. },
  1612. // Shows the popover on the specified position. Renders it if not already
  1613. show: function() {
  1614. if (this.isHidden) {
  1615. if (!this.el) {
  1616. this.render();
  1617. }
  1618. this.el.show();
  1619. this.position();
  1620. this.isHidden = false;
  1621. this.trigger('show');
  1622. }
  1623. },
  1624. // Hides the popover, through CSS, but does not remove it from the DOM
  1625. hide: function() {
  1626. if (!this.isHidden) {
  1627. this.el.hide();
  1628. this.isHidden = true;
  1629. this.trigger('hide');
  1630. }
  1631. },
  1632. // Creates `this.el` and renders content inside of it
  1633. render: function() {
  1634. var _this = this;
  1635. var options = this.options;
  1636. this.el = $('<div class="fc-popover"/>')
  1637. .addClass(options.className || '')
  1638. .css({
  1639. // position initially to the top left to avoid creating scrollbars
  1640. top: 0,
  1641. left: 0
  1642. })
  1643. .append(options.content)
  1644. .appendTo(options.parentEl);
  1645. // when a click happens on anything inside with a 'fc-close' className, hide the popover
  1646. this.el.on('click', '.fc-close', function() {
  1647. _this.hide();
  1648. });
  1649. if (options.autoHide) {
  1650. this.listenTo($(document), 'mousedown', this.documentMousedown);
  1651. }
  1652. },
  1653. // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  1654. documentMousedown: function(ev) {
  1655. // only hide the popover if the click happened outside the popover
  1656. if (this.el && !$(ev.target).closest(this.el).length) {
  1657. this.hide();
  1658. }
  1659. },
  1660. // Hides and unregisters any handlers
  1661. removeElement: function() {
  1662. this.hide();
  1663. if (this.el) {
  1664. this.el.remove();
  1665. this.el = null;
  1666. }
  1667. this.stopListeningTo($(document), 'mousedown');
  1668. },
  1669. // Positions the popover optimally, using the top/left/right options
  1670. position: function() {
  1671. var options = this.options;
  1672. var origin = this.el.offsetParent().offset();
  1673. var width = this.el.outerWidth();
  1674. var height = this.el.outerHeight();
  1675. var windowEl = $(window);
  1676. var viewportEl = getScrollParent(this.el);
  1677. var viewportTop;
  1678. var viewportLeft;
  1679. var viewportOffset;
  1680. var top; // the "position" (not "offset") values for the popover
  1681. var left; //
  1682. // compute top and left
  1683. top = options.top || 0;
  1684. if (options.left !== undefined) {
  1685. left = options.left;
  1686. }
  1687. else if (options.right !== undefined) {
  1688. left = options.right - width; // derive the left value from the right value
  1689. }
  1690. else {
  1691. left = 0;
  1692. }
  1693. if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  1694. viewportEl = windowEl;
  1695. viewportTop = 0; // the window is always at the top left
  1696. viewportLeft = 0; // (and .offset() won't work if called here)
  1697. }
  1698. else {
  1699. viewportOffset = viewportEl.offset();
  1700. viewportTop = viewportOffset.top;
  1701. viewportLeft = viewportOffset.left;
  1702. }
  1703. // if the window is scrolled, it causes the visible area to be further down
  1704. viewportTop += windowEl.scrollTop();
  1705. viewportLeft += windowEl.scrollLeft();
  1706. // constrain to the view port. if constrained by two edges, give precedence to top/left
  1707. if (options.viewportConstrain !== false) {
  1708. top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  1709. top = Math.max(top, viewportTop + this.margin);
  1710. left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  1711. left = Math.max(left, viewportLeft + this.margin);
  1712. }
  1713. this.el.css({
  1714. top: top - origin.top,
  1715. left: left - origin.left
  1716. });
  1717. },
  1718. // Triggers a callback. Calls a function in the option hash of the same name.
  1719. // Arguments beyond the first `name` are forwarded on.
  1720. // TODO: better code reuse for this. Repeat code
  1721. trigger: function(name) {
  1722. if (this.options[name]) {
  1723. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1724. }
  1725. }
  1726. });
  1727. ;;
  1728. /*
  1729. A cache for the left/right/top/bottom/width/height values for one or more elements.
  1730. Works with both offset (from topleft document) and position (from offsetParent).
  1731. options:
  1732. - els
  1733. - isHorizontal
  1734. - isVertical
  1735. */
  1736. var CoordCache = FC.CoordCache = Class.extend({
  1737. els: null, // jQuery set (assumed to be siblings)
  1738. forcedOffsetParentEl: null, // options can override the natural offsetParent
  1739. origin: null, // {left,top} position of offsetParent of els
  1740. boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
  1741. isHorizontal: false, // whether to query for left/right/width
  1742. isVertical: false, // whether to query for top/bottom/height
  1743. // arrays of coordinates (offsets from topleft of document)
  1744. lefts: null,
  1745. rights: null,
  1746. tops: null,
  1747. bottoms: null,
  1748. constructor: function(options) {
  1749. this.els = $(options.els);
  1750. this.isHorizontal = options.isHorizontal;
  1751. this.isVertical = options.isVertical;
  1752. this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
  1753. },
  1754. // Queries the els for coordinates and stores them.
  1755. // Call this method before using and of the get* methods below.
  1756. build: function() {
  1757. var offsetParentEl = this.forcedOffsetParentEl;
  1758. if (!offsetParentEl && this.els.length > 0) {
  1759. offsetParentEl = this.els.eq(0).offsetParent();
  1760. }
  1761. this.origin = offsetParentEl ?
  1762. offsetParentEl.offset() :
  1763. null;
  1764. this.boundingRect = this.queryBoundingRect();
  1765. if (this.isHorizontal) {
  1766. this.buildElHorizontals();
  1767. }
  1768. if (this.isVertical) {
  1769. this.buildElVerticals();
  1770. }
  1771. },
  1772. // Destroys all internal data about coordinates, freeing memory
  1773. clear: function() {
  1774. this.origin = null;
  1775. this.boundingRect = null;
  1776. this.lefts = null;
  1777. this.rights = null;
  1778. this.tops = null;
  1779. this.bottoms = null;
  1780. },
  1781. // When called, if coord caches aren't built, builds them
  1782. ensureBuilt: function() {
  1783. if (!this.origin) {
  1784. this.build();
  1785. }
  1786. },
  1787. // Populates the left/right internal coordinate arrays
  1788. buildElHorizontals: function() {
  1789. var lefts = [];
  1790. var rights = [];
  1791. this.els.each(function(i, node) {
  1792. var el = $(node);
  1793. var left = el.offset().left;
  1794. var width = el.outerWidth();
  1795. lefts.push(left);
  1796. rights.push(left + width);
  1797. });
  1798. this.lefts = lefts;
  1799. this.rights = rights;
  1800. },
  1801. // Populates the top/bottom internal coordinate arrays
  1802. buildElVerticals: function() {
  1803. var tops = [];
  1804. var bottoms = [];
  1805. this.els.each(function(i, node) {
  1806. var el = $(node);
  1807. var top = el.offset().top;
  1808. var height = el.outerHeight();
  1809. tops.push(top);
  1810. bottoms.push(top + height);
  1811. });
  1812. this.tops = tops;
  1813. this.bottoms = bottoms;
  1814. },
  1815. // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
  1816. // If no intersection is made, returns undefined.
  1817. getHorizontalIndex: function(leftOffset) {
  1818. this.ensureBuilt();
  1819. var lefts = this.lefts;
  1820. var rights = this.rights;
  1821. var len = lefts.length;
  1822. var i;
  1823. for (i = 0; i < len; i++) {
  1824. if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
  1825. return i;
  1826. }
  1827. }
  1828. },
  1829. // Given a top offset (from document top), returns the index of the el that it vertically intersects.
  1830. // If no intersection is made, returns undefined.
  1831. getVerticalIndex: function(topOffset) {
  1832. this.ensureBuilt();
  1833. var tops = this.tops;
  1834. var bottoms = this.bottoms;
  1835. var len = tops.length;
  1836. var i;
  1837. for (i = 0; i < len; i++) {
  1838. if (topOffset >= tops[i] && topOffset < bottoms[i]) {
  1839. return i;
  1840. }
  1841. }
  1842. },
  1843. // Gets the left offset (from document left) of the element at the given index
  1844. getLeftOffset: function(leftIndex) {
  1845. this.ensureBuilt();
  1846. return this.lefts[leftIndex];
  1847. },
  1848. // Gets the left position (from offsetParent left) of the element at the given index
  1849. getLeftPosition: function(leftIndex) {
  1850. this.ensureBuilt();
  1851. return this.lefts[leftIndex] - this.origin.left;
  1852. },
  1853. // Gets the right offset (from document left) of the element at the given index.
  1854. // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
  1855. getRightOffset: function(leftIndex) {
  1856. this.ensureBuilt();
  1857. return this.rights[leftIndex];
  1858. },
  1859. // Gets the right position (from offsetParent left) of the element at the given index.
  1860. // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
  1861. getRightPosition: function(leftIndex) {
  1862. this.ensureBuilt();
  1863. return this.rights[leftIndex] - this.origin.left;
  1864. },
  1865. // Gets the width of the element at the given index
  1866. getWidth: function(leftIndex) {
  1867. this.ensureBuilt();
  1868. return this.rights[leftIndex] - this.lefts[leftIndex];
  1869. },
  1870. // Gets the top offset (from document top) of the element at the given index
  1871. getTopOffset: function(topIndex) {
  1872. this.ensureBuilt();
  1873. return this.tops[topIndex];
  1874. },
  1875. // Gets the top position (from offsetParent top) of the element at the given position
  1876. getTopPosition: function(topIndex) {
  1877. this.ensureBuilt();
  1878. return this.tops[topIndex] - this.origin.top;
  1879. },
  1880. // Gets the bottom offset (from the document top) of the element at the given index.
  1881. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
  1882. getBottomOffset: function(topIndex) {
  1883. this.ensureBuilt();
  1884. return this.bottoms[topIndex];
  1885. },
  1886. // Gets the bottom position (from the offsetParent top) of the element at the given index.
  1887. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
  1888. getBottomPosition: function(topIndex) {
  1889. this.ensureBuilt();
  1890. return this.bottoms[topIndex] - this.origin.top;
  1891. },
  1892. // Gets the height of the element at the given index
  1893. getHeight: function(topIndex) {
  1894. this.ensureBuilt();
  1895. return this.bottoms[topIndex] - this.tops[topIndex];
  1896. },
  1897. // Bounding Rect
  1898. // TODO: decouple this from CoordCache
  1899. // Compute and return what the elements' bounding rectangle is, from the user's perspective.
  1900. // Right now, only returns a rectangle if constrained by an overflow:scroll element.
  1901. // Returns null if there are no elements
  1902. queryBoundingRect: function() {
  1903. var scrollParentEl;
  1904. if (this.els.length > 0) {
  1905. scrollParentEl = getScrollParent(this.els.eq(0));
  1906. if (!scrollParentEl.is(document)) {
  1907. return getClientRect(scrollParentEl);
  1908. }
  1909. }
  1910. return null;
  1911. },
  1912. isPointInBounds: function(leftOffset, topOffset) {
  1913. return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
  1914. },
  1915. isLeftInBounds: function(leftOffset) {
  1916. return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
  1917. },
  1918. isTopInBounds: function(topOffset) {
  1919. return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
  1920. }
  1921. });
  1922. ;;
  1923. /* Tracks a drag's mouse movement, firing various handlers
  1924. ----------------------------------------------------------------------------------------------------------------------*/
  1925. // TODO: use Emitter
  1926. var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
  1927. options: null,
  1928. subjectEl: null,
  1929. // coordinates of the initial mousedown
  1930. originX: null,
  1931. originY: null,
  1932. // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
  1933. // TODO: do this for wrappers that have overflow:hidden as well.
  1934. scrollEl: null,
  1935. isInteracting: false,
  1936. isDistanceSurpassed: false,
  1937. isDelayEnded: false,
  1938. isDragging: false,
  1939. isTouch: false,
  1940. delay: null,
  1941. delayTimeoutId: null,
  1942. minDistance: null,
  1943. handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
  1944. constructor: function(options) {
  1945. this.options = options || {};
  1946. this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
  1947. this.initMouseIgnoring(500);
  1948. },
  1949. // Interaction (high-level)
  1950. // -----------------------------------------------------------------------------------------------------------------
  1951. startInteraction: function(ev, extraOptions) {
  1952. var isTouch = getEvIsTouch(ev);
  1953. if (ev.type === 'mousedown') {
  1954. if (this.isIgnoringMouse) {
  1955. return;
  1956. }
  1957. else if (!isPrimaryMouseButton(ev)) {
  1958. return;
  1959. }
  1960. else {
  1961. ev.preventDefault(); // prevents native selection in most browsers
  1962. }
  1963. }
  1964. if (!this.isInteracting) {
  1965. // process options
  1966. extraOptions = extraOptions || {};
  1967. this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
  1968. this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
  1969. this.subjectEl = this.options.subjectEl;
  1970. this.isInteracting = true;
  1971. this.isTouch = isTouch;
  1972. this.isDelayEnded = false;
  1973. this.isDistanceSurpassed = false;
  1974. this.originX = getEvX(ev);
  1975. this.originY = getEvY(ev);
  1976. this.scrollEl = getScrollParent($(ev.target));
  1977. this.bindHandlers();
  1978. this.initAutoScroll();
  1979. this.handleInteractionStart(ev);
  1980. this.startDelay(ev);
  1981. if (!this.minDistance) {
  1982. this.handleDistanceSurpassed(ev);
  1983. }
  1984. }
  1985. },
  1986. handleInteractionStart: function(ev) {
  1987. this.trigger('interactionStart', ev);
  1988. },
  1989. endInteraction: function(ev, isCancelled) {
  1990. if (this.isInteracting) {
  1991. this.endDrag(ev);
  1992. if (this.delayTimeoutId) {
  1993. clearTimeout(this.delayTimeoutId);
  1994. this.delayTimeoutId = null;
  1995. }
  1996. this.destroyAutoScroll();
  1997. this.unbindHandlers();
  1998. this.isInteracting = false;
  1999. this.handleInteractionEnd(ev, isCancelled);
  2000. // a touchstart+touchend on the same element will result in the following addition simulated events:
  2001. // mouseover + mouseout + click
  2002. // let's ignore these bogus events
  2003. if (this.isTouch) {
  2004. this.tempIgnoreMouse();
  2005. }
  2006. }
  2007. },
  2008. handleInteractionEnd: function(ev, isCancelled) {
  2009. this.trigger('interactionEnd', ev, isCancelled || false);
  2010. },
  2011. // Binding To DOM
  2012. // -----------------------------------------------------------------------------------------------------------------
  2013. bindHandlers: function() {
  2014. var _this = this;
  2015. var touchStartIgnores = 1;
  2016. if (this.isTouch) {
  2017. this.listenTo($(document), {
  2018. touchmove: this.handleTouchMove,
  2019. touchend: this.endInteraction,
  2020. touchcancel: this.endInteraction,
  2021. // Sometimes touchend doesn't fire
  2022. // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
  2023. // If another touchstart happens, we know it's bogus, so cancel the drag.
  2024. // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
  2025. touchstart: function(ev) {
  2026. if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
  2027. touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
  2028. }
  2029. else {
  2030. _this.endInteraction(ev, true); // isCancelled=true
  2031. }
  2032. }
  2033. });
  2034. // listen to ALL scroll actions on the page
  2035. if (
  2036. !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
  2037. this.scrollEl // otherwise, attach a single handler to this
  2038. ) {
  2039. this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
  2040. }
  2041. }
  2042. else {
  2043. this.listenTo($(document), {
  2044. mousemove: this.handleMouseMove,
  2045. mouseup: this.endInteraction
  2046. });
  2047. }
  2048. this.listenTo($(document), {
  2049. selectstart: preventDefault, // don't allow selection while dragging
  2050. contextmenu: preventDefault // long taps would open menu on Chrome dev tools
  2051. });
  2052. },
  2053. unbindHandlers: function() {
  2054. this.stopListeningTo($(document));
  2055. // unbind scroll listening
  2056. unbindAnyScroll(this.handleTouchScrollProxy);
  2057. if (this.scrollEl) {
  2058. this.stopListeningTo(this.scrollEl, 'scroll');
  2059. }
  2060. },
  2061. // Drag (high-level)
  2062. // -----------------------------------------------------------------------------------------------------------------
  2063. // extraOptions ignored if drag already started
  2064. startDrag: function(ev, extraOptions) {
  2065. this.startInteraction(ev, extraOptions); // ensure interaction began
  2066. if (!this.isDragging) {
  2067. this.isDragging = true;
  2068. this.handleDragStart(ev);
  2069. }
  2070. },
  2071. handleDragStart: function(ev) {
  2072. this.trigger('dragStart', ev);
  2073. },
  2074. handleMove: function(ev) {
  2075. var dx = getEvX(ev) - this.originX;
  2076. var dy = getEvY(ev) - this.originY;
  2077. var minDistance = this.minDistance;
  2078. var distanceSq; // current distance from the origin, squared
  2079. if (!this.isDistanceSurpassed) {
  2080. distanceSq = dx * dx + dy * dy;
  2081. if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  2082. this.handleDistanceSurpassed(ev);
  2083. }
  2084. }
  2085. if (this.isDragging) {
  2086. this.handleDrag(dx, dy, ev);
  2087. }
  2088. },
  2089. // Called while the mouse is being moved and when we know a legitimate drag is taking place
  2090. handleDrag: function(dx, dy, ev) {
  2091. this.trigger('drag', dx, dy, ev);
  2092. this.updateAutoScroll(ev); // will possibly cause scrolling
  2093. },
  2094. endDrag: function(ev) {
  2095. if (this.isDragging) {
  2096. this.isDragging = false;
  2097. this.handleDragEnd(ev);
  2098. }
  2099. },
  2100. handleDragEnd: function(ev) {
  2101. this.trigger('dragEnd', ev);
  2102. },
  2103. // Delay
  2104. // -----------------------------------------------------------------------------------------------------------------
  2105. startDelay: function(initialEv) {
  2106. var _this = this;
  2107. if (this.delay) {
  2108. this.delayTimeoutId = setTimeout(function() {
  2109. _this.handleDelayEnd(initialEv);
  2110. }, this.delay);
  2111. }
  2112. else {
  2113. this.handleDelayEnd(initialEv);
  2114. }
  2115. },
  2116. handleDelayEnd: function(initialEv) {
  2117. this.isDelayEnded = true;
  2118. if (this.isDistanceSurpassed) {
  2119. this.startDrag(initialEv);
  2120. }
  2121. },
  2122. // Distance
  2123. // -----------------------------------------------------------------------------------------------------------------
  2124. handleDistanceSurpassed: function(ev) {
  2125. this.isDistanceSurpassed = true;
  2126. if (this.isDelayEnded) {
  2127. this.startDrag(ev);
  2128. }
  2129. },
  2130. // Mouse / Touch
  2131. // -----------------------------------------------------------------------------------------------------------------
  2132. handleTouchMove: function(ev) {
  2133. // prevent inertia and touchmove-scrolling while dragging
  2134. if (this.isDragging) {
  2135. ev.preventDefault();
  2136. }
  2137. this.handleMove(ev);
  2138. },
  2139. handleMouseMove: function(ev) {
  2140. this.handleMove(ev);
  2141. },
  2142. // Scrolling (unrelated to auto-scroll)
  2143. // -----------------------------------------------------------------------------------------------------------------
  2144. handleTouchScroll: function(ev) {
  2145. // if the drag is being initiated by touch, but a scroll happens before
  2146. // the drag-initiating delay is over, cancel the drag
  2147. if (!this.isDragging) {
  2148. this.endInteraction(ev, true); // isCancelled=true
  2149. }
  2150. },
  2151. // Utils
  2152. // -----------------------------------------------------------------------------------------------------------------
  2153. // Triggers a callback. Calls a function in the option hash of the same name.
  2154. // Arguments beyond the first `name` are forwarded on.
  2155. trigger: function(name) {
  2156. if (this.options[name]) {
  2157. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  2158. }
  2159. // makes _methods callable by event name. TODO: kill this
  2160. if (this['_' + name]) {
  2161. this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
  2162. }
  2163. }
  2164. });
  2165. ;;
  2166. /*
  2167. this.scrollEl is set in DragListener
  2168. */
  2169. DragListener.mixin({
  2170. isAutoScroll: false,
  2171. scrollBounds: null, // { top, bottom, left, right }
  2172. scrollTopVel: null, // pixels per second
  2173. scrollLeftVel: null, // pixels per second
  2174. scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  2175. // defaults
  2176. scrollSensitivity: 30, // pixels from edge for scrolling to start
  2177. scrollSpeed: 200, // pixels per second, at maximum speed
  2178. scrollIntervalMs: 50, // millisecond wait between scroll increment
  2179. initAutoScroll: function() {
  2180. var scrollEl = this.scrollEl;
  2181. this.isAutoScroll =
  2182. this.options.scroll &&
  2183. scrollEl &&
  2184. !scrollEl.is(window) &&
  2185. !scrollEl.is(document);
  2186. if (this.isAutoScroll) {
  2187. // debounce makes sure rapid calls don't happen
  2188. this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
  2189. }
  2190. },
  2191. destroyAutoScroll: function() {
  2192. this.endAutoScroll(); // kill any animation loop
  2193. // remove the scroll handler if there is a scrollEl
  2194. if (this.isAutoScroll) {
  2195. this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
  2196. }
  2197. },
  2198. // Computes and stores the bounding rectangle of scrollEl
  2199. computeScrollBounds: function() {
  2200. if (this.isAutoScroll) {
  2201. this.scrollBounds = getOuterRect(this.scrollEl);
  2202. // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  2203. }
  2204. },
  2205. // Called when the dragging is in progress and scrolling should be updated
  2206. updateAutoScroll: function(ev) {
  2207. var sensitivity = this.scrollSensitivity;
  2208. var bounds = this.scrollBounds;
  2209. var topCloseness, bottomCloseness;
  2210. var leftCloseness, rightCloseness;
  2211. var topVel = 0;
  2212. var leftVel = 0;
  2213. if (bounds) { // only scroll if scrollEl exists
  2214. // compute closeness to edges. valid range is from 0.0 - 1.0
  2215. topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
  2216. bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
  2217. leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
  2218. rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
  2219. // translate vertical closeness into velocity.
  2220. // mouse must be completely in bounds for velocity to happen.
  2221. if (topCloseness >= 0 && topCloseness <= 1) {
  2222. topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  2223. }
  2224. else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  2225. topVel = bottomCloseness * this.scrollSpeed;
  2226. }
  2227. // translate horizontal closeness into velocity
  2228. if (leftCloseness >= 0 && leftCloseness <= 1) {
  2229. leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  2230. }
  2231. else if (rightCloseness >= 0 && rightCloseness <= 1) {
  2232. leftVel = rightCloseness * this.scrollSpeed;
  2233. }
  2234. }
  2235. this.setScrollVel(topVel, leftVel);
  2236. },
  2237. // Sets the speed-of-scrolling for the scrollEl
  2238. setScrollVel: function(topVel, leftVel) {
  2239. this.scrollTopVel = topVel;
  2240. this.scrollLeftVel = leftVel;
  2241. this.constrainScrollVel(); // massages into realistic values
  2242. // if there is non-zero velocity, and an animation loop hasn't already started, then START
  2243. if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  2244. this.scrollIntervalId = setInterval(
  2245. proxy(this, 'scrollIntervalFunc'), // scope to `this`
  2246. this.scrollIntervalMs
  2247. );
  2248. }
  2249. },
  2250. // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  2251. constrainScrollVel: function() {
  2252. var el = this.scrollEl;
  2253. if (this.scrollTopVel < 0) { // scrolling up?
  2254. if (el.scrollTop() <= 0) { // already scrolled all the way up?
  2255. this.scrollTopVel = 0;
  2256. }
  2257. }
  2258. else if (this.scrollTopVel > 0) { // scrolling down?
  2259. if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  2260. this.scrollTopVel = 0;
  2261. }
  2262. }
  2263. if (this.scrollLeftVel < 0) { // scrolling left?
  2264. if (el.scrollLeft() <= 0) { // already scrolled all the left?
  2265. this.scrollLeftVel = 0;
  2266. }
  2267. }
  2268. else if (this.scrollLeftVel > 0) { // scrolling right?
  2269. if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  2270. this.scrollLeftVel = 0;
  2271. }
  2272. }
  2273. },
  2274. // This function gets called during every iteration of the scrolling animation loop
  2275. scrollIntervalFunc: function() {
  2276. var el = this.scrollEl;
  2277. var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  2278. // change the value of scrollEl's scroll
  2279. if (this.scrollTopVel) {
  2280. el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  2281. }
  2282. if (this.scrollLeftVel) {
  2283. el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  2284. }
  2285. this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  2286. // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  2287. if (!this.scrollTopVel && !this.scrollLeftVel) {
  2288. this.endAutoScroll();
  2289. }
  2290. },
  2291. // Kills any existing scrolling animation loop
  2292. endAutoScroll: function() {
  2293. if (this.scrollIntervalId) {
  2294. clearInterval(this.scrollIntervalId);
  2295. this.scrollIntervalId = null;
  2296. this.handleScrollEnd();
  2297. }
  2298. },
  2299. // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  2300. handleDebouncedScroll: function() {
  2301. // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  2302. if (!this.scrollIntervalId) {
  2303. this.handleScrollEnd();
  2304. }
  2305. },
  2306. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2307. handleScrollEnd: function() {
  2308. }
  2309. });
  2310. ;;
  2311. /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
  2312. ------------------------------------------------------------------------------------------------------------------------
  2313. options:
  2314. - subjectEl
  2315. - subjectCenter
  2316. */
  2317. var HitDragListener = DragListener.extend({
  2318. component: null, // converts coordinates to hits
  2319. // methods: prepareHits, releaseHits, queryHit
  2320. origHit: null, // the hit the mouse was over when listening started
  2321. hit: null, // the hit the mouse is over
  2322. coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
  2323. constructor: function(component, options) {
  2324. DragListener.call(this, options); // call the super-constructor
  2325. this.component = component;
  2326. },
  2327. // Called when drag listening starts (but a real drag has not necessarily began).
  2328. // ev might be undefined if dragging was started manually.
  2329. handleInteractionStart: function(ev) {
  2330. var subjectEl = this.subjectEl;
  2331. var subjectRect;
  2332. var origPoint;
  2333. var point;
  2334. this.computeCoords();
  2335. if (ev) {
  2336. origPoint = { left: getEvX(ev), top: getEvY(ev) };
  2337. point = origPoint;
  2338. // constrain the point to bounds of the element being dragged
  2339. if (subjectEl) {
  2340. subjectRect = getOuterRect(subjectEl); // used for centering as well
  2341. point = constrainPoint(point, subjectRect);
  2342. }
  2343. this.origHit = this.queryHit(point.left, point.top);
  2344. // treat the center of the subject as the collision point?
  2345. if (subjectEl && this.options.subjectCenter) {
  2346. // only consider the area the subject overlaps the hit. best for large subjects.
  2347. // TODO: skip this if hit didn't supply left/right/top/bottom
  2348. if (this.origHit) {
  2349. subjectRect = intersectRects(this.origHit, subjectRect) ||
  2350. subjectRect; // in case there is no intersection
  2351. }
  2352. point = getRectCenter(subjectRect);
  2353. }
  2354. this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
  2355. }
  2356. else {
  2357. this.origHit = null;
  2358. this.coordAdjust = null;
  2359. }
  2360. // call the super-method. do it after origHit has been computed
  2361. DragListener.prototype.handleInteractionStart.apply(this, arguments);
  2362. },
  2363. // Recomputes the drag-critical positions of elements
  2364. computeCoords: function() {
  2365. this.component.prepareHits();
  2366. this.computeScrollBounds(); // why is this here??????
  2367. },
  2368. // Called when the actual drag has started
  2369. handleDragStart: function(ev) {
  2370. var hit;
  2371. DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
  2372. // might be different from this.origHit if the min-distance is large
  2373. hit = this.queryHit(getEvX(ev), getEvY(ev));
  2374. // report the initial hit the mouse is over
  2375. // especially important if no min-distance and drag starts immediately
  2376. if (hit) {
  2377. this.handleHitOver(hit);
  2378. }
  2379. },
  2380. // Called when the drag moves
  2381. handleDrag: function(dx, dy, ev) {
  2382. var hit;
  2383. DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
  2384. hit = this.queryHit(getEvX(ev), getEvY(ev));
  2385. if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
  2386. if (this.hit) {
  2387. this.handleHitOut();
  2388. }
  2389. if (hit) {
  2390. this.handleHitOver(hit);
  2391. }
  2392. }
  2393. },
  2394. // Called when dragging has been stopped
  2395. handleDragEnd: function() {
  2396. this.handleHitDone();
  2397. DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
  2398. },
  2399. // Called when a the mouse has just moved over a new hit
  2400. handleHitOver: function(hit) {
  2401. var isOrig = isHitsEqual(hit, this.origHit);
  2402. this.hit = hit;
  2403. this.trigger('hitOver', this.hit, isOrig, this.origHit);
  2404. },
  2405. // Called when the mouse has just moved out of a hit
  2406. handleHitOut: function() {
  2407. if (this.hit) {
  2408. this.trigger('hitOut', this.hit);
  2409. this.handleHitDone();
  2410. this.hit = null;
  2411. }
  2412. },
  2413. // Called after a hitOut. Also called before a dragStop
  2414. handleHitDone: function() {
  2415. if (this.hit) {
  2416. this.trigger('hitDone', this.hit);
  2417. }
  2418. },
  2419. // Called when the interaction ends, whether there was a real drag or not
  2420. handleInteractionEnd: function() {
  2421. DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
  2422. this.origHit = null;
  2423. this.hit = null;
  2424. this.component.releaseHits();
  2425. },
  2426. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2427. handleScrollEnd: function() {
  2428. DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
  2429. this.computeCoords(); // hits' absolute positions will be in new places. recompute
  2430. },
  2431. // Gets the hit underneath the coordinates for the given mouse event
  2432. queryHit: function(left, top) {
  2433. if (this.coordAdjust) {
  2434. left += this.coordAdjust.left;
  2435. top += this.coordAdjust.top;
  2436. }
  2437. return this.component.queryHit(left, top);
  2438. }
  2439. });
  2440. // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
  2441. // Two null values will be considered equal, as two "out of the component" states are the same.
  2442. function isHitsEqual(hit0, hit1) {
  2443. if (!hit0 && !hit1) {
  2444. return true;
  2445. }
  2446. if (hit0 && hit1) {
  2447. return hit0.component === hit1.component &&
  2448. isHitPropsWithin(hit0, hit1) &&
  2449. isHitPropsWithin(hit1, hit0); // ensures all props are identical
  2450. }
  2451. return false;
  2452. }
  2453. // Returns true if all of subHit's non-standard properties are within superHit
  2454. function isHitPropsWithin(subHit, superHit) {
  2455. for (var propName in subHit) {
  2456. if (!/^(component|left|right|top|bottom)$/.test(propName)) {
  2457. if (subHit[propName] !== superHit[propName]) {
  2458. return false;
  2459. }
  2460. }
  2461. }
  2462. return true;
  2463. }
  2464. ;;
  2465. /* Creates a clone of an element and lets it track the mouse as it moves
  2466. ----------------------------------------------------------------------------------------------------------------------*/
  2467. var MouseFollower = Class.extend(ListenerMixin, {
  2468. options: null,
  2469. sourceEl: null, // the element that will be cloned and made to look like it is dragging
  2470. el: null, // the clone of `sourceEl` that will track the mouse
  2471. parentEl: null, // the element that `el` (the clone) will be attached to
  2472. // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  2473. top0: null,
  2474. left0: null,
  2475. // the absolute coordinates of the initiating touch/mouse action
  2476. y0: null,
  2477. x0: null,
  2478. // the number of pixels the mouse has moved from its initial position
  2479. topDelta: null,
  2480. leftDelta: null,
  2481. isFollowing: false,
  2482. isHidden: false,
  2483. isAnimating: false, // doing the revert animation?
  2484. constructor: function(sourceEl, options) {
  2485. this.options = options = options || {};
  2486. this.sourceEl = sourceEl;
  2487. this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  2488. },
  2489. // Causes the element to start following the mouse
  2490. start: function(ev) {
  2491. if (!this.isFollowing) {
  2492. this.isFollowing = true;
  2493. this.y0 = getEvY(ev);
  2494. this.x0 = getEvX(ev);
  2495. this.topDelta = 0;
  2496. this.leftDelta = 0;
  2497. if (!this.isHidden) {
  2498. this.updatePosition();
  2499. }
  2500. if (getEvIsTouch(ev)) {
  2501. this.listenTo($(document), 'touchmove', this.handleMove);
  2502. }
  2503. else {
  2504. this.listenTo($(document), 'mousemove', this.handleMove);
  2505. }
  2506. }
  2507. },
  2508. // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  2509. // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  2510. stop: function(shouldRevert, callback) {
  2511. var _this = this;
  2512. var revertDuration = this.options.revertDuration;
  2513. function complete() { // might be called by .animate(), which might change `this` context
  2514. _this.isAnimating = false;
  2515. _this.removeElement();
  2516. _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
  2517. if (callback) {
  2518. callback();
  2519. }
  2520. }
  2521. if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  2522. this.isFollowing = false;
  2523. this.stopListeningTo($(document));
  2524. if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  2525. this.isAnimating = true;
  2526. this.el.animate({
  2527. top: this.top0,
  2528. left: this.left0
  2529. }, {
  2530. duration: revertDuration,
  2531. complete: complete
  2532. });
  2533. }
  2534. else {
  2535. complete();
  2536. }
  2537. }
  2538. },
  2539. // Gets the tracking element. Create it if necessary
  2540. getEl: function() {
  2541. var el = this.el;
  2542. if (!el) {
  2543. el = this.el = this.sourceEl.clone()
  2544. .addClass(this.options.additionalClass || '')
  2545. .css({
  2546. position: 'absolute',
  2547. visibility: '', // in case original element was hidden (commonly through hideEvents())
  2548. display: this.isHidden ? 'none' : '', // for when initially hidden
  2549. margin: 0,
  2550. right: 'auto', // erase and set width instead
  2551. bottom: 'auto', // erase and set height instead
  2552. width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  2553. height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  2554. opacity: this.options.opacity || '',
  2555. zIndex: this.options.zIndex
  2556. });
  2557. // we don't want long taps or any mouse interaction causing selection/menus.
  2558. // would use preventSelection(), but that prevents selectstart, causing problems.
  2559. el.addClass('fc-unselectable');
  2560. el.appendTo(this.parentEl);
  2561. }
  2562. return el;
  2563. },
  2564. // Removes the tracking element if it has already been created
  2565. removeElement: function() {
  2566. if (this.el) {
  2567. this.el.remove();
  2568. this.el = null;
  2569. }
  2570. },
  2571. // Update the CSS position of the tracking element
  2572. updatePosition: function() {
  2573. var sourceOffset;
  2574. var origin;
  2575. this.getEl(); // ensure this.el
  2576. // make sure origin info was computed
  2577. if (this.top0 === null) {
  2578. sourceOffset = this.sourceEl.offset();
  2579. origin = this.el.offsetParent().offset();
  2580. this.top0 = sourceOffset.top - origin.top;
  2581. this.left0 = sourceOffset.left - origin.left;
  2582. }
  2583. this.el.css({
  2584. top: this.top0 + this.topDelta,
  2585. left: this.left0 + this.leftDelta
  2586. });
  2587. },
  2588. // Gets called when the user moves the mouse
  2589. handleMove: function(ev) {
  2590. this.topDelta = getEvY(ev) - this.y0;
  2591. this.leftDelta = getEvX(ev) - this.x0;
  2592. if (!this.isHidden) {
  2593. this.updatePosition();
  2594. }
  2595. },
  2596. // Temporarily makes the tracking element invisible. Can be called before following starts
  2597. hide: function() {
  2598. if (!this.isHidden) {
  2599. this.isHidden = true;
  2600. if (this.el) {
  2601. this.el.hide();
  2602. }
  2603. }
  2604. },
  2605. // Show the tracking element after it has been temporarily hidden
  2606. show: function() {
  2607. if (this.isHidden) {
  2608. this.isHidden = false;
  2609. this.updatePosition();
  2610. this.getEl().show();
  2611. }
  2612. }
  2613. });
  2614. ;;
  2615. /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
  2616. ----------------------------------------------------------------------------------------------------------------------*/
  2617. var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
  2618. // self-config, overridable by subclasses
  2619. hasDayInteractions: true, // can user click/select ranges of time?
  2620. view: null, // a View object
  2621. isRTL: null, // shortcut to the view's isRTL option
  2622. start: null,
  2623. end: null,
  2624. el: null, // the containing element
  2625. elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  2626. // derived from options
  2627. eventTimeFormat: null,
  2628. displayEventTime: null,
  2629. displayEventEnd: null,
  2630. minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
  2631. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  2632. // of the date areas. if not defined, assumes to be day and time granularity.
  2633. // TODO: port isTimeScale into same system?
  2634. largeUnit: null,
  2635. dayDragListener: null,
  2636. segDragListener: null,
  2637. segResizeListener: null,
  2638. externalDragListener: null,
  2639. constructor: function(view) {
  2640. this.view = view;
  2641. this.isRTL = view.opt('isRTL');
  2642. this.elsByFill = {};
  2643. this.dayDragListener = this.buildDayDragListener();
  2644. this.initMouseIgnoring();
  2645. },
  2646. /* Options
  2647. ------------------------------------------------------------------------------------------------------------------*/
  2648. // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  2649. computeEventTimeFormat: function() {
  2650. return this.view.opt('smallTimeFormat');
  2651. },
  2652. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
  2653. // Only applies to non-all-day events.
  2654. computeDisplayEventTime: function() {
  2655. return true;
  2656. },
  2657. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  2658. computeDisplayEventEnd: function() {
  2659. return true;
  2660. },
  2661. /* Dates
  2662. ------------------------------------------------------------------------------------------------------------------*/
  2663. // Tells the grid about what period of time to display.
  2664. // Any date-related internal data should be generated.
  2665. setRange: function(range) {
  2666. this.start = range.start.clone();
  2667. this.end = range.end.clone();
  2668. this.rangeUpdated();
  2669. this.processRangeOptions();
  2670. },
  2671. // Called when internal variables that rely on the range should be updated
  2672. rangeUpdated: function() {
  2673. },
  2674. // Updates values that rely on options and also relate to range
  2675. processRangeOptions: function() {
  2676. var view = this.view;
  2677. var displayEventTime;
  2678. var displayEventEnd;
  2679. this.eventTimeFormat =
  2680. view.opt('eventTimeFormat') ||
  2681. view.opt('timeFormat') || // deprecated
  2682. this.computeEventTimeFormat();
  2683. displayEventTime = view.opt('displayEventTime');
  2684. if (displayEventTime == null) {
  2685. displayEventTime = this.computeDisplayEventTime(); // might be based off of range
  2686. }
  2687. displayEventEnd = view.opt('displayEventEnd');
  2688. if (displayEventEnd == null) {
  2689. displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
  2690. }
  2691. this.displayEventTime = displayEventTime;
  2692. this.displayEventEnd = displayEventEnd;
  2693. },
  2694. // Converts a span (has unzoned start/end and any other grid-specific location information)
  2695. // into an array of segments (pieces of events whose format is decided by the grid).
  2696. spanToSegs: function(span) {
  2697. // subclasses must implement
  2698. },
  2699. // Diffs the two dates, returning a duration, based on granularity of the grid
  2700. // TODO: port isTimeScale into this system?
  2701. diffDates: function(a, b) {
  2702. if (this.largeUnit) {
  2703. return diffByUnit(a, b, this.largeUnit);
  2704. }
  2705. else {
  2706. return diffDayTime(a, b);
  2707. }
  2708. },
  2709. /* Hit Area
  2710. ------------------------------------------------------------------------------------------------------------------*/
  2711. // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
  2712. prepareHits: function() {
  2713. },
  2714. // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
  2715. releaseHits: function() {
  2716. },
  2717. // Given coordinates from the topleft of the document, return data about the date-related area underneath.
  2718. // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
  2719. // Must have a `grid` property, a reference to this current grid. TODO: avoid this
  2720. // The returned object will be processed by getHitSpan and getHitEl.
  2721. queryHit: function(leftOffset, topOffset) {
  2722. },
  2723. // Given position-level information about a date-related area within the grid,
  2724. // should return an object with at least a start/end date. Can provide other information as well.
  2725. getHitSpan: function(hit) {
  2726. },
  2727. // Given position-level information about a date-related area within the grid,
  2728. // should return a jQuery element that best represents it. passed to dayClick callback.
  2729. getHitEl: function(hit) {
  2730. },
  2731. /* Rendering
  2732. ------------------------------------------------------------------------------------------------------------------*/
  2733. // Sets the container element that the grid should render inside of.
  2734. // Does other DOM-related initializations.
  2735. setElement: function(el) {
  2736. this.el = el;
  2737. if (this.hasDayInteractions) {
  2738. preventSelection(el);
  2739. this.bindDayHandler('touchstart', this.dayTouchStart);
  2740. this.bindDayHandler('mousedown', this.dayMousedown);
  2741. }
  2742. // attach event-element-related handlers. in Grid.events
  2743. // same garbage collection note as above.
  2744. this.bindSegHandlers();
  2745. this.bindGlobalHandlers();
  2746. },
  2747. bindDayHandler: function(name, handler) {
  2748. var _this = this;
  2749. // attach a handler to the grid's root element.
  2750. // jQuery will take care of unregistering them when removeElement gets called.
  2751. this.el.on(name, function(ev) {
  2752. if (
  2753. !$(ev.target).is(
  2754. _this.segSelector + ',' + // directly on an event element
  2755. _this.segSelector + ' *,' + // within an event element
  2756. '.fc-more,' + // a "more.." link
  2757. 'a[data-goto]' // a clickable nav link
  2758. )
  2759. ) {
  2760. return handler.call(_this, ev);
  2761. }
  2762. });
  2763. },
  2764. // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
  2765. // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
  2766. removeElement: function() {
  2767. this.unbindGlobalHandlers();
  2768. this.clearDragListeners();
  2769. this.el.remove();
  2770. // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
  2771. },
  2772. // Renders the basic structure of grid view before any content is rendered
  2773. renderSkeleton: function() {
  2774. // subclasses should implement
  2775. },
  2776. // Renders the grid's date-related content (like areas that represent days/times).
  2777. // Assumes setRange has already been called and the skeleton has already been rendered.
  2778. renderDates: function() {
  2779. // subclasses should implement
  2780. },
  2781. // Unrenders the grid's date-related content
  2782. unrenderDates: function() {
  2783. // subclasses should implement
  2784. },
  2785. /* Handlers
  2786. ------------------------------------------------------------------------------------------------------------------*/
  2787. // Binds DOM handlers to elements that reside outside the grid, such as the document
  2788. bindGlobalHandlers: function() {
  2789. this.listenTo($(document), {
  2790. dragstart: this.externalDragStart, // jqui
  2791. sortstart: this.externalDragStart // jqui
  2792. });
  2793. },
  2794. // Unbinds DOM handlers from elements that reside outside the grid
  2795. unbindGlobalHandlers: function() {
  2796. this.stopListeningTo($(document));
  2797. },
  2798. // Process a mousedown on an element that represents a day. For day clicking and selecting.
  2799. dayMousedown: function(ev) {
  2800. if (!this.isIgnoringMouse) {
  2801. this.dayDragListener.startInteraction(ev, {
  2802. //distance: 5, // needs more work if we want dayClick to fire correctly
  2803. });
  2804. }
  2805. },
  2806. dayTouchStart: function(ev) {
  2807. var view = this.view;
  2808. var selectLongPressDelay = view.opt('selectLongPressDelay');
  2809. // HACK to prevent a user's clickaway for unselecting a range or an event
  2810. // from causing a dayClick.
  2811. if (view.isSelected || view.selectedEvent) {
  2812. this.tempIgnoreMouse();
  2813. }
  2814. if (selectLongPressDelay == null) {
  2815. selectLongPressDelay = view.opt('longPressDelay'); // fallback
  2816. }
  2817. this.dayDragListener.startInteraction(ev, {
  2818. delay: selectLongPressDelay
  2819. });
  2820. },
  2821. // Creates a listener that tracks the user's drag across day elements.
  2822. // For day clicking and selecting.
  2823. buildDayDragListener: function() {
  2824. var _this = this;
  2825. var view = this.view;
  2826. var isSelectable = view.opt('selectable');
  2827. var dayClickHit; // null if invalid dayClick
  2828. var selectionSpan; // null if invalid selection
  2829. // this listener tracks a mousedown on a day element, and a subsequent drag.
  2830. // if the drag ends on the same day, it is a 'dayClick'.
  2831. // if 'selectable' is enabled, this listener also detects selections.
  2832. var dragListener = new HitDragListener(this, {
  2833. scroll: view.opt('dragScroll'),
  2834. interactionStart: function() {
  2835. dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
  2836. selectionSpan = null;
  2837. },
  2838. dragStart: function() {
  2839. view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  2840. },
  2841. hitOver: function(hit, isOrig, origHit) {
  2842. if (origHit) { // click needs to have started on a hit
  2843. // if user dragged to another cell at any point, it can no longer be a dayClick
  2844. if (!isOrig) {
  2845. dayClickHit = null;
  2846. }
  2847. if (isSelectable) {
  2848. selectionSpan = _this.computeSelection(
  2849. _this.getHitSpan(origHit),
  2850. _this.getHitSpan(hit)
  2851. );
  2852. if (selectionSpan) {
  2853. _this.renderSelection(selectionSpan);
  2854. }
  2855. else if (selectionSpan === false) {
  2856. disableCursor();
  2857. }
  2858. }
  2859. }
  2860. },
  2861. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  2862. dayClickHit = null;
  2863. selectionSpan = null;
  2864. _this.unrenderSelection();
  2865. },
  2866. hitDone: function() { // called after a hitOut OR before a dragEnd
  2867. enableCursor();
  2868. },
  2869. interactionEnd: function(ev, isCancelled) {
  2870. if (!isCancelled) {
  2871. if (
  2872. dayClickHit &&
  2873. !_this.isIgnoringMouse // see hack in dayTouchStart
  2874. ) {
  2875. view.triggerDayClick(
  2876. _this.getHitSpan(dayClickHit),
  2877. _this.getHitEl(dayClickHit),
  2878. ev
  2879. );
  2880. }
  2881. if (selectionSpan) {
  2882. // the selection will already have been rendered. just report it
  2883. view.reportSelection(selectionSpan, ev);
  2884. }
  2885. }
  2886. }
  2887. });
  2888. return dragListener;
  2889. },
  2890. // Kills all in-progress dragging.
  2891. // Useful for when public API methods that result in re-rendering are invoked during a drag.
  2892. // Also useful for when touch devices misbehave and don't fire their touchend.
  2893. clearDragListeners: function() {
  2894. this.dayDragListener.endInteraction();
  2895. if (this.segDragListener) {
  2896. this.segDragListener.endInteraction(); // will clear this.segDragListener
  2897. }
  2898. if (this.segResizeListener) {
  2899. this.segResizeListener.endInteraction(); // will clear this.segResizeListener
  2900. }
  2901. if (this.externalDragListener) {
  2902. this.externalDragListener.endInteraction(); // will clear this.externalDragListener
  2903. }
  2904. },
  2905. /* Event Helper
  2906. ------------------------------------------------------------------------------------------------------------------*/
  2907. // TODO: should probably move this to Grid.events, like we did event dragging / resizing
  2908. // Renders a mock event at the given event location, which contains zoned start/end properties.
  2909. // Returns all mock event elements.
  2910. renderEventLocationHelper: function(eventLocation, sourceSeg) {
  2911. var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
  2912. return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  2913. },
  2914. // Builds a fake event given zoned event date properties and a segment is should be inspired from.
  2915. // The range's end can be null, in which case the mock event that is rendered will have a null end time.
  2916. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  2917. fabricateHelperEvent: function(eventLocation, sourceSeg) {
  2918. var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  2919. fakeEvent.start = eventLocation.start.clone();
  2920. fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
  2921. fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
  2922. this.view.calendar.normalizeEventDates(fakeEvent);
  2923. // this extra className will be useful for differentiating real events from mock events in CSS
  2924. fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  2925. // if something external is being dragged in, don't render a resizer
  2926. if (!sourceSeg) {
  2927. fakeEvent.editable = false;
  2928. }
  2929. return fakeEvent;
  2930. },
  2931. // Renders a mock event. Given zoned event date properties.
  2932. // Must return all mock event elements.
  2933. renderHelper: function(eventLocation, sourceSeg) {
  2934. // subclasses must implement
  2935. },
  2936. // Unrenders a mock event
  2937. unrenderHelper: function() {
  2938. // subclasses must implement
  2939. },
  2940. /* Selection
  2941. ------------------------------------------------------------------------------------------------------------------*/
  2942. // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  2943. // Given a span (unzoned start/end and other misc data)
  2944. renderSelection: function(span) {
  2945. this.renderHighlight(span);
  2946. },
  2947. // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  2948. unrenderSelection: function() {
  2949. this.unrenderHighlight();
  2950. },
  2951. // Given the first and last date-spans of a selection, returns another date-span object.
  2952. // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
  2953. // Will return false if the selection is invalid and this should be indicated to the user.
  2954. // Will return null/undefined if a selection invalid but no error should be reported.
  2955. computeSelection: function(span0, span1) {
  2956. var span = this.computeSelectionSpan(span0, span1);
  2957. if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
  2958. return false;
  2959. }
  2960. return span;
  2961. },
  2962. // Given two spans, must return the combination of the two.
  2963. // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
  2964. computeSelectionSpan: function(span0, span1) {
  2965. var dates = [ span0.start, span0.end, span1.start, span1.end ];
  2966. dates.sort(compareNumbers); // sorts chronologically. works with Moments
  2967. return { start: dates[0].clone(), end: dates[3].clone() };
  2968. },
  2969. /* Highlight
  2970. ------------------------------------------------------------------------------------------------------------------*/
  2971. // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
  2972. renderHighlight: function(span) {
  2973. this.renderFill('highlight', this.spanToSegs(span));
  2974. },
  2975. // Unrenders the emphasis on a date range
  2976. unrenderHighlight: function() {
  2977. this.unrenderFill('highlight');
  2978. },
  2979. // Generates an array of classNames for rendering the highlight. Used by the fill system.
  2980. highlightSegClasses: function() {
  2981. return [ 'fc-highlight' ];
  2982. },
  2983. /* Business Hours
  2984. ------------------------------------------------------------------------------------------------------------------*/
  2985. renderBusinessHours: function() {
  2986. },
  2987. unrenderBusinessHours: function() {
  2988. },
  2989. /* Now Indicator
  2990. ------------------------------------------------------------------------------------------------------------------*/
  2991. getNowIndicatorUnit: function() {
  2992. },
  2993. renderNowIndicator: function(date) {
  2994. },
  2995. unrenderNowIndicator: function() {
  2996. },
  2997. /* Fill System (highlight, background events, business hours)
  2998. --------------------------------------------------------------------------------------------------------------------
  2999. TODO: remove this system. like we did in TimeGrid
  3000. */
  3001. // Renders a set of rectangles over the given segments of time.
  3002. // MUST RETURN a subset of segs, the segs that were actually rendered.
  3003. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  3004. renderFill: function(type, segs) {
  3005. // subclasses must implement
  3006. },
  3007. // Unrenders a specific type of fill that is currently rendered on the grid
  3008. unrenderFill: function(type) {
  3009. var el = this.elsByFill[type];
  3010. if (el) {
  3011. el.remove();
  3012. delete this.elsByFill[type];
  3013. }
  3014. },
  3015. // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  3016. // Only returns segments that successfully rendered.
  3017. // To be harnessed by renderFill (implemented by subclasses).
  3018. // Analagous to renderFgSegEls.
  3019. renderFillSegEls: function(type, segs) {
  3020. var _this = this;
  3021. var segElMethod = this[type + 'SegEl'];
  3022. var html = '';
  3023. var renderedSegs = [];
  3024. var i;
  3025. if (segs.length) {
  3026. // build a large concatenation of segment HTML
  3027. for (i = 0; i < segs.length; i++) {
  3028. html += this.fillSegHtml(type, segs[i]);
  3029. }
  3030. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3031. // Then, compute the 'el' for each segment.
  3032. $(html).each(function(i, node) {
  3033. var seg = segs[i];
  3034. var el = $(node);
  3035. // allow custom filter methods per-type
  3036. if (segElMethod) {
  3037. el = segElMethod.call(_this, seg, el);
  3038. }
  3039. if (el) { // custom filters did not cancel the render
  3040. el = $(el); // allow custom filter to return raw DOM node
  3041. // correct element type? (would be bad if a non-TD were inserted into a table for example)
  3042. if (el.is(_this.fillSegTag)) {
  3043. seg.el = el;
  3044. renderedSegs.push(seg);
  3045. }
  3046. }
  3047. });
  3048. }
  3049. return renderedSegs;
  3050. },
  3051. fillSegTag: 'div', // subclasses can override
  3052. // Builds the HTML needed for one fill segment. Generic enough to work with different types.
  3053. fillSegHtml: function(type, seg) {
  3054. // custom hooks per-type
  3055. var classesMethod = this[type + 'SegClasses'];
  3056. var cssMethod = this[type + 'SegCss'];
  3057. var classes = classesMethod ? classesMethod.call(this, seg) : [];
  3058. var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
  3059. return '<' + this.fillSegTag +
  3060. (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  3061. (css ? ' style="' + css + '"' : '') +
  3062. ' />';
  3063. },
  3064. /* Generic rendering utilities for subclasses
  3065. ------------------------------------------------------------------------------------------------------------------*/
  3066. // Computes HTML classNames for a single-day element
  3067. getDayClasses: function(date, noThemeHighlight) {
  3068. var view = this.view;
  3069. var today = view.calendar.getNow();
  3070. var classes = [ 'fc-' + dayIDs[date.day()] ];
  3071. if (
  3072. view.intervalDuration.as('months') == 1 &&
  3073. date.month() != view.intervalStart.month()
  3074. ) {
  3075. classes.push('fc-other-month');
  3076. }
  3077. if (date.isSame(today, 'day')) {
  3078. classes.push('fc-today');
  3079. if (noThemeHighlight !== true) {
  3080. classes.push(view.highlightStateClass);
  3081. }
  3082. }
  3083. else if (date < today) {
  3084. classes.push('fc-past');
  3085. }
  3086. else {
  3087. classes.push('fc-future');
  3088. }
  3089. return classes;
  3090. }
  3091. });
  3092. ;;
  3093. /* Event-rendering and event-interaction methods for the abstract Grid class
  3094. ----------------------------------------------------------------------------------------------------------------------*/
  3095. Grid.mixin({
  3096. // self-config, overridable by subclasses
  3097. segSelector: '.fc-event-container > *', // what constitutes an event element?
  3098. mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  3099. isDraggingSeg: false, // is a segment being dragged? boolean
  3100. isResizingSeg: false, // is a segment being resized? boolean
  3101. isDraggingExternal: false, // jqui-dragging an external element? boolean
  3102. segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
  3103. // Renders the given events onto the grid
  3104. renderEvents: function(events) {
  3105. var bgEvents = [];
  3106. var fgEvents = [];
  3107. var i;
  3108. for (i = 0; i < events.length; i++) {
  3109. (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]);
  3110. }
  3111. this.segs = [].concat( // record all segs
  3112. this.renderBgEvents(bgEvents),
  3113. this.renderFgEvents(fgEvents)
  3114. );
  3115. },
  3116. renderBgEvents: function(events) {
  3117. var segs = this.eventsToSegs(events);
  3118. // renderBgSegs might return a subset of segs, segs that were actually rendered
  3119. return this.renderBgSegs(segs) || segs;
  3120. },
  3121. renderFgEvents: function(events) {
  3122. var segs = this.eventsToSegs(events);
  3123. // renderFgSegs might return a subset of segs, segs that were actually rendered
  3124. return this.renderFgSegs(segs) || segs;
  3125. },
  3126. // Unrenders all events currently rendered on the grid
  3127. unrenderEvents: function() {
  3128. this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  3129. this.clearDragListeners();
  3130. this.unrenderFgSegs();
  3131. this.unrenderBgSegs();
  3132. this.segs = null;
  3133. },
  3134. // Retrieves all rendered segment objects currently rendered on the grid
  3135. getEventSegs: function() {
  3136. return this.segs || [];
  3137. },
  3138. /* Foreground Segment Rendering
  3139. ------------------------------------------------------------------------------------------------------------------*/
  3140. // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  3141. renderFgSegs: function(segs) {
  3142. // subclasses must implement
  3143. },
  3144. // Unrenders all currently rendered foreground segments
  3145. unrenderFgSegs: function() {
  3146. // subclasses must implement
  3147. },
  3148. // Renders and assigns an `el` property for each foreground event segment.
  3149. // Only returns segments that successfully rendered.
  3150. // A utility that subclasses may use.
  3151. renderFgSegEls: function(segs, disableResizing) {
  3152. var view = this.view;
  3153. var html = '';
  3154. var renderedSegs = [];
  3155. var i;
  3156. if (segs.length) { // don't build an empty html string
  3157. // build a large concatenation of event segment HTML
  3158. for (i = 0; i < segs.length; i++) {
  3159. html += this.fgSegHtml(segs[i], disableResizing);
  3160. }
  3161. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3162. // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  3163. $(html).each(function(i, node) {
  3164. var seg = segs[i];
  3165. var el = view.resolveEventEl(seg.event, $(node));
  3166. if (el) {
  3167. el.data('fc-seg', seg); // used by handlers
  3168. seg.el = el;
  3169. renderedSegs.push(seg);
  3170. }
  3171. });
  3172. }
  3173. return renderedSegs;
  3174. },
  3175. // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  3176. fgSegHtml: function(seg, disableResizing) {
  3177. // subclasses should implement
  3178. },
  3179. /* Background Segment Rendering
  3180. ------------------------------------------------------------------------------------------------------------------*/
  3181. // Renders the given background event segments onto the grid.
  3182. // Returns a subset of the segs that were actually rendered.
  3183. renderBgSegs: function(segs) {
  3184. return this.renderFill('bgEvent', segs);
  3185. },
  3186. // Unrenders all the currently rendered background event segments
  3187. unrenderBgSegs: function() {
  3188. this.unrenderFill('bgEvent');
  3189. },
  3190. // Renders a background event element, given the default rendering. Called by the fill system.
  3191. bgEventSegEl: function(seg, el) {
  3192. return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  3193. },
  3194. // Generates an array of classNames to be used for the default rendering of a background event.
  3195. // Called by fillSegHtml.
  3196. bgEventSegClasses: function(seg) {
  3197. var event = seg.event;
  3198. var source = event.source || {};
  3199. return [ 'fc-bgevent' ].concat(
  3200. event.className,
  3201. source.className || []
  3202. );
  3203. },
  3204. // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  3205. // Called by fillSegHtml.
  3206. bgEventSegCss: function(seg) {
  3207. return {
  3208. 'background-color': this.getSegSkinCss(seg)['background-color']
  3209. };
  3210. },
  3211. // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  3212. // Called by fillSegHtml.
  3213. businessHoursSegClasses: function(seg) {
  3214. return [ 'fc-nonbusiness', 'fc-bgevent' ];
  3215. },
  3216. /* Business Hours
  3217. ------------------------------------------------------------------------------------------------------------------*/
  3218. // Compute business hour segs for the grid's current date range.
  3219. // Caller must ask if whole-day business hours are needed.
  3220. // If no `businessHours` configuration value is specified, assumes the calendar default.
  3221. buildBusinessHourSegs: function(wholeDay, businessHours) {
  3222. return this.eventsToSegs(
  3223. this.buildBusinessHourEvents(wholeDay, businessHours)
  3224. );
  3225. },
  3226. // Compute business hour *events* for the grid's current date range.
  3227. // Caller must ask if whole-day business hours are needed.
  3228. // If no `businessHours` configuration value is specified, assumes the calendar default.
  3229. buildBusinessHourEvents: function(wholeDay, businessHours) {
  3230. var calendar = this.view.calendar;
  3231. var events;
  3232. if (businessHours == null) {
  3233. // fallback
  3234. // access from calendawr. don't access from view. doesn't update with dynamic options.
  3235. businessHours = calendar.options.businessHours;
  3236. }
  3237. events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
  3238. // HACK. Eventually refactor business hours "events" system.
  3239. // If no events are given, but businessHours is activated, this means the entire visible range should be
  3240. // marked as *not* business-hours, via inverse-background rendering.
  3241. if (!events.length && businessHours) {
  3242. events = [
  3243. $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
  3244. start: this.view.end, // guaranteed out-of-range
  3245. end: this.view.end, // "
  3246. dow: null
  3247. })
  3248. ];
  3249. }
  3250. return events;
  3251. },
  3252. /* Handlers
  3253. ------------------------------------------------------------------------------------------------------------------*/
  3254. // Attaches event-element-related handlers for *all* rendered event segments of the view.
  3255. bindSegHandlers: function() {
  3256. this.bindSegHandlersToEl(this.el);
  3257. },
  3258. // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
  3259. bindSegHandlersToEl: function(el) {
  3260. this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
  3261. this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
  3262. this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
  3263. this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
  3264. this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
  3265. this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
  3266. },
  3267. // Executes a handler for any a user-interaction on a segment.
  3268. // Handler gets called with (seg, ev), and with the `this` context of the Grid
  3269. bindSegHandlerToEl: function(el, name, handler) {
  3270. var _this = this;
  3271. el.on(name, this.segSelector, function(ev) {
  3272. var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  3273. // only call the handlers if there is not a drag/resize in progress
  3274. if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  3275. return handler.call(_this, seg, ev); // context will be the Grid
  3276. }
  3277. });
  3278. },
  3279. handleSegClick: function(seg, ev) {
  3280. var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
  3281. if (res === false) {
  3282. ev.preventDefault();
  3283. }
  3284. },
  3285. // Updates internal state and triggers handlers for when an event element is moused over
  3286. handleSegMouseover: function(seg, ev) {
  3287. if (
  3288. !this.isIgnoringMouse &&
  3289. !this.mousedOverSeg
  3290. ) {
  3291. this.mousedOverSeg = seg;
  3292. if (this.view.isEventResizable(seg.event)) {
  3293. seg.el.addClass('fc-allow-mouse-resize');
  3294. }
  3295. this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
  3296. }
  3297. },
  3298. // Updates internal state and triggers handlers for when an event element is moused out.
  3299. // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  3300. handleSegMouseout: function(seg, ev) {
  3301. ev = ev || {}; // if given no args, make a mock mouse event
  3302. if (this.mousedOverSeg) {
  3303. seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  3304. this.mousedOverSeg = null;
  3305. if (this.view.isEventResizable(seg.event)) {
  3306. seg.el.removeClass('fc-allow-mouse-resize');
  3307. }
  3308. this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
  3309. }
  3310. },
  3311. handleSegMousedown: function(seg, ev) {
  3312. var isResizing = this.startSegResize(seg, ev, { distance: 5 });
  3313. if (!isResizing && this.view.isEventDraggable(seg.event)) {
  3314. this.buildSegDragListener(seg)
  3315. .startInteraction(ev, {
  3316. distance: 5
  3317. });
  3318. }
  3319. },
  3320. handleSegTouchStart: function(seg, ev) {
  3321. var view = this.view;
  3322. var event = seg.event;
  3323. var isSelected = view.isEventSelected(event);
  3324. var isDraggable = view.isEventDraggable(event);
  3325. var isResizable = view.isEventResizable(event);
  3326. var isResizing = false;
  3327. var dragListener;
  3328. var eventLongPressDelay;
  3329. if (isSelected && isResizable) {
  3330. // only allow resizing of the event is selected
  3331. isResizing = this.startSegResize(seg, ev);
  3332. }
  3333. if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
  3334. eventLongPressDelay = view.opt('eventLongPressDelay');
  3335. if (eventLongPressDelay == null) {
  3336. eventLongPressDelay = view.opt('longPressDelay'); // fallback
  3337. }
  3338. dragListener = isDraggable ?
  3339. this.buildSegDragListener(seg) :
  3340. this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
  3341. dragListener.startInteraction(ev, { // won't start if already started
  3342. delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
  3343. });
  3344. }
  3345. // a long tap simulates a mouseover. ignore this bogus mouseover.
  3346. this.tempIgnoreMouse();
  3347. },
  3348. handleSegTouchEnd: function(seg, ev) {
  3349. // touchstart+touchend = click, which simulates a mouseover.
  3350. // ignore this bogus mouseover.
  3351. this.tempIgnoreMouse();
  3352. },
  3353. // returns boolean whether resizing actually started or not.
  3354. // assumes the seg allows resizing.
  3355. // `dragOptions` are optional.
  3356. startSegResize: function(seg, ev, dragOptions) {
  3357. if ($(ev.target).is('.fc-resizer')) {
  3358. this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
  3359. .startInteraction(ev, dragOptions);
  3360. return true;
  3361. }
  3362. return false;
  3363. },
  3364. /* Event Dragging
  3365. ------------------------------------------------------------------------------------------------------------------*/
  3366. // Builds a listener that will track user-dragging on an event segment.
  3367. // Generic enough to work with any type of Grid.
  3368. // Has side effect of setting/unsetting `segDragListener`
  3369. buildSegDragListener: function(seg) {
  3370. var _this = this;
  3371. var view = this.view;
  3372. var calendar = view.calendar;
  3373. var el = seg.el;
  3374. var event = seg.event;
  3375. var isDragging;
  3376. var mouseFollower; // A clone of the original element that will move with the mouse
  3377. var dropLocation; // zoned event date properties
  3378. if (this.segDragListener) {
  3379. return this.segDragListener;
  3380. }
  3381. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  3382. // of the view.
  3383. var dragListener = this.segDragListener = new HitDragListener(view, {
  3384. scroll: view.opt('dragScroll'),
  3385. subjectEl: el,
  3386. subjectCenter: true,
  3387. interactionStart: function(ev) {
  3388. seg.component = _this; // for renderDrag
  3389. isDragging = false;
  3390. mouseFollower = new MouseFollower(seg.el, {
  3391. additionalClass: 'fc-dragging',
  3392. parentEl: view.el,
  3393. opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
  3394. revertDuration: view.opt('dragRevertDuration'),
  3395. zIndex: 2 // one above the .fc-view
  3396. });
  3397. mouseFollower.hide(); // don't show until we know this is a real drag
  3398. mouseFollower.start(ev);
  3399. },
  3400. dragStart: function(ev) {
  3401. if (dragListener.isTouch && !view.isEventSelected(event)) {
  3402. // if not previously selected, will fire after a delay. then, select the event
  3403. view.selectEvent(event);
  3404. }
  3405. isDragging = true;
  3406. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3407. _this.segDragStart(seg, ev);
  3408. view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  3409. },
  3410. hitOver: function(hit, isOrig, origHit) {
  3411. var dragHelperEls;
  3412. // starting hit could be forced (DayGrid.limit)
  3413. if (seg.hit) {
  3414. origHit = seg.hit;
  3415. }
  3416. // since we are querying the parent view, might not belong to this grid
  3417. dropLocation = _this.computeEventDrop(
  3418. origHit.component.getHitSpan(origHit),
  3419. hit.component.getHitSpan(hit),
  3420. event
  3421. );
  3422. if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
  3423. disableCursor();
  3424. dropLocation = null;
  3425. }
  3426. // if a valid drop location, have the subclass render a visual indication
  3427. if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
  3428. dragHelperEls.addClass('fc-dragging');
  3429. if (!dragListener.isTouch) {
  3430. _this.applyDragOpacity(dragHelperEls);
  3431. }
  3432. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  3433. }
  3434. else {
  3435. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  3436. }
  3437. if (isOrig) {
  3438. dropLocation = null; // needs to have moved hits to be a valid drop
  3439. }
  3440. },
  3441. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  3442. view.unrenderDrag(); // unrender whatever was done in renderDrag
  3443. mouseFollower.show(); // show in case we are moving out of all hits
  3444. dropLocation = null;
  3445. },
  3446. hitDone: function() { // Called after a hitOut OR before a dragEnd
  3447. enableCursor();
  3448. },
  3449. interactionEnd: function(ev) {
  3450. delete seg.component; // prevent side effects
  3451. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  3452. mouseFollower.stop(!dropLocation, function() {
  3453. if (isDragging) {
  3454. view.unrenderDrag();
  3455. _this.segDragStop(seg, ev);
  3456. }
  3457. if (dropLocation) {
  3458. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  3459. view.reportEventDrop(event, dropLocation, _this.largeUnit, el, ev);
  3460. }
  3461. else {
  3462. view.showEvent(event);
  3463. }
  3464. });
  3465. _this.segDragListener = null;
  3466. }
  3467. });
  3468. return dragListener;
  3469. },
  3470. // seg isn't draggable, but let's use a generic DragListener
  3471. // simply for the delay, so it can be selected.
  3472. // Has side effect of setting/unsetting `segDragListener`
  3473. buildSegSelectListener: function(seg) {
  3474. var _this = this;
  3475. var view = this.view;
  3476. var event = seg.event;
  3477. if (this.segDragListener) {
  3478. return this.segDragListener;
  3479. }
  3480. var dragListener = this.segDragListener = new DragListener({
  3481. dragStart: function(ev) {
  3482. if (dragListener.isTouch && !view.isEventSelected(event)) {
  3483. // if not previously selected, will fire after a delay. then, select the event
  3484. view.selectEvent(event);
  3485. }
  3486. },
  3487. interactionEnd: function(ev) {
  3488. _this.segDragListener = null;
  3489. }
  3490. });
  3491. return dragListener;
  3492. },
  3493. // Called before event segment dragging starts
  3494. segDragStart: function(seg, ev) {
  3495. this.isDraggingSeg = true;
  3496. this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3497. },
  3498. // Called after event segment dragging stops
  3499. segDragStop: function(seg, ev) {
  3500. this.isDraggingSeg = false;
  3501. this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3502. },
  3503. // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
  3504. // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
  3505. // A falsy returned value indicates an invalid drop.
  3506. // DOES NOT consider overlap/constraint.
  3507. computeEventDrop: function(startSpan, endSpan, event) {
  3508. var calendar = this.view.calendar;
  3509. var dragStart = startSpan.start;
  3510. var dragEnd = endSpan.start;
  3511. var delta;
  3512. var dropLocation; // zoned event date properties
  3513. if (dragStart.hasTime() === dragEnd.hasTime()) {
  3514. delta = this.diffDates(dragEnd, dragStart);
  3515. // if an all-day event was in a timed area and it was dragged to a different time,
  3516. // guarantee an end and adjust start/end to have times
  3517. if (event.allDay && durationHasTime(delta)) {
  3518. dropLocation = {
  3519. start: event.start.clone(),
  3520. end: calendar.getEventEnd(event), // will be an ambig day
  3521. allDay: false // for normalizeEventTimes
  3522. };
  3523. calendar.normalizeEventTimes(dropLocation);
  3524. }
  3525. // othewise, work off existing values
  3526. else {
  3527. dropLocation = pluckEventDateProps(event);
  3528. }
  3529. dropLocation.start.add(delta);
  3530. if (dropLocation.end) {
  3531. dropLocation.end.add(delta);
  3532. }
  3533. }
  3534. else {
  3535. // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  3536. dropLocation = {
  3537. start: dragEnd.clone(),
  3538. end: null, // end should be cleared
  3539. allDay: !dragEnd.hasTime()
  3540. };
  3541. }
  3542. return dropLocation;
  3543. },
  3544. // Utility for apply dragOpacity to a jQuery set
  3545. applyDragOpacity: function(els) {
  3546. var opacity = this.view.opt('dragOpacity');
  3547. if (opacity != null) {
  3548. els.css('opacity', opacity);
  3549. }
  3550. },
  3551. /* External Element Dragging
  3552. ------------------------------------------------------------------------------------------------------------------*/
  3553. // Called when a jQuery UI drag is initiated anywhere in the DOM
  3554. externalDragStart: function(ev, ui) {
  3555. var view = this.view;
  3556. var el;
  3557. var accept;
  3558. if (view.opt('droppable')) { // only listen if this setting is on
  3559. el = $((ui ? ui.item : null) || ev.target);
  3560. // Test that the dragged element passes the dropAccept selector or filter function.
  3561. // FYI, the default is "*" (matches all)
  3562. accept = view.opt('dropAccept');
  3563. if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  3564. if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  3565. this.listenToExternalDrag(el, ev, ui);
  3566. }
  3567. }
  3568. }
  3569. },
  3570. // Called when a jQuery UI drag starts and it needs to be monitored for dropping
  3571. listenToExternalDrag: function(el, ev, ui) {
  3572. var _this = this;
  3573. var calendar = this.view.calendar;
  3574. var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  3575. var dropLocation; // a null value signals an unsuccessful drag
  3576. // listener that tracks mouse movement over date-associated pixel regions
  3577. var dragListener = _this.externalDragListener = new HitDragListener(this, {
  3578. interactionStart: function() {
  3579. _this.isDraggingExternal = true;
  3580. },
  3581. hitOver: function(hit) {
  3582. dropLocation = _this.computeExternalDrop(
  3583. hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
  3584. meta
  3585. );
  3586. if ( // invalid hit?
  3587. dropLocation &&
  3588. !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps)
  3589. ) {
  3590. disableCursor();
  3591. dropLocation = null;
  3592. }
  3593. if (dropLocation) {
  3594. _this.renderDrag(dropLocation); // called without a seg parameter
  3595. }
  3596. },
  3597. hitOut: function() {
  3598. dropLocation = null; // signal unsuccessful
  3599. },
  3600. hitDone: function() { // Called after a hitOut OR before a dragEnd
  3601. enableCursor();
  3602. _this.unrenderDrag();
  3603. },
  3604. interactionEnd: function(ev) {
  3605. if (dropLocation) { // element was dropped on a valid hit
  3606. _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
  3607. }
  3608. _this.isDraggingExternal = false;
  3609. _this.externalDragListener = null;
  3610. }
  3611. });
  3612. dragListener.startDrag(ev); // start listening immediately
  3613. },
  3614. // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  3615. // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
  3616. // Returning a null value signals an invalid drop hit.
  3617. // DOES NOT consider overlap/constraint.
  3618. computeExternalDrop: function(span, meta) {
  3619. var calendar = this.view.calendar;
  3620. var dropLocation = {
  3621. start: calendar.applyTimezone(span.start), // simulate a zoned event start date
  3622. end: null
  3623. };
  3624. // if dropped on an all-day span, and element's metadata specified a time, set it
  3625. if (meta.startTime && !dropLocation.start.hasTime()) {
  3626. dropLocation.start.time(meta.startTime);
  3627. }
  3628. if (meta.duration) {
  3629. dropLocation.end = dropLocation.start.clone().add(meta.duration);
  3630. }
  3631. return dropLocation;
  3632. },
  3633. /* Drag Rendering (for both events and an external elements)
  3634. ------------------------------------------------------------------------------------------------------------------*/
  3635. // Renders a visual indication of an event or external element being dragged.
  3636. // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
  3637. // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
  3638. // A truthy returned value indicates this method has rendered a helper element.
  3639. // Must return elements used for any mock events.
  3640. renderDrag: function(dropLocation, seg) {
  3641. // subclasses must implement
  3642. },
  3643. // Unrenders a visual indication of an event or external element being dragged
  3644. unrenderDrag: function() {
  3645. // subclasses must implement
  3646. },
  3647. /* Resizing
  3648. ------------------------------------------------------------------------------------------------------------------*/
  3649. // Creates a listener that tracks the user as they resize an event segment.
  3650. // Generic enough to work with any type of Grid.
  3651. buildSegResizeListener: function(seg, isStart) {
  3652. var _this = this;
  3653. var view = this.view;
  3654. var calendar = view.calendar;
  3655. var el = seg.el;
  3656. var event = seg.event;
  3657. var eventEnd = calendar.getEventEnd(event);
  3658. var isDragging;
  3659. var resizeLocation; // zoned event date properties. falsy if invalid resize
  3660. // Tracks mouse movement over the *grid's* coordinate map
  3661. var dragListener = this.segResizeListener = new HitDragListener(this, {
  3662. scroll: view.opt('dragScroll'),
  3663. subjectEl: el,
  3664. interactionStart: function() {
  3665. isDragging = false;
  3666. },
  3667. dragStart: function(ev) {
  3668. isDragging = true;
  3669. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3670. _this.segResizeStart(seg, ev);
  3671. },
  3672. hitOver: function(hit, isOrig, origHit) {
  3673. var origHitSpan = _this.getHitSpan(origHit);
  3674. var hitSpan = _this.getHitSpan(hit);
  3675. resizeLocation = isStart ?
  3676. _this.computeEventStartResize(origHitSpan, hitSpan, event) :
  3677. _this.computeEventEndResize(origHitSpan, hitSpan, event);
  3678. if (resizeLocation) {
  3679. if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
  3680. disableCursor();
  3681. resizeLocation = null;
  3682. }
  3683. // no change? (FYI, event dates might have zones)
  3684. else if (
  3685. resizeLocation.start.isSame(event.start.clone().stripZone()) &&
  3686. resizeLocation.end.isSame(eventEnd.clone().stripZone())
  3687. ) {
  3688. resizeLocation = null;
  3689. }
  3690. }
  3691. if (resizeLocation) {
  3692. view.hideEvent(event);
  3693. _this.renderEventResize(resizeLocation, seg);
  3694. }
  3695. },
  3696. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  3697. resizeLocation = null;
  3698. view.showEvent(event); // for when out-of-bounds. show original
  3699. },
  3700. hitDone: function() { // resets the rendering to show the original event
  3701. _this.unrenderEventResize();
  3702. enableCursor();
  3703. },
  3704. interactionEnd: function(ev) {
  3705. if (isDragging) {
  3706. _this.segResizeStop(seg, ev);
  3707. }
  3708. if (resizeLocation) { // valid date to resize to?
  3709. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  3710. view.reportEventResize(event, resizeLocation, _this.largeUnit, el, ev);
  3711. }
  3712. else {
  3713. view.showEvent(event);
  3714. }
  3715. _this.segResizeListener = null;
  3716. }
  3717. });
  3718. return dragListener;
  3719. },
  3720. // Called before event segment resizing starts
  3721. segResizeStart: function(seg, ev) {
  3722. this.isResizingSeg = true;
  3723. this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3724. },
  3725. // Called after event segment resizing stops
  3726. segResizeStop: function(seg, ev) {
  3727. this.isResizingSeg = false;
  3728. this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3729. },
  3730. // Returns new date-information for an event segment being resized from its start
  3731. computeEventStartResize: function(startSpan, endSpan, event) {
  3732. return this.computeEventResize('start', startSpan, endSpan, event);
  3733. },
  3734. // Returns new date-information for an event segment being resized from its end
  3735. computeEventEndResize: function(startSpan, endSpan, event) {
  3736. return this.computeEventResize('end', startSpan, endSpan, event);
  3737. },
  3738. // Returns new zoned date information for an event segment being resized from its start OR end
  3739. // `type` is either 'start' or 'end'.
  3740. // DOES NOT consider overlap/constraint.
  3741. computeEventResize: function(type, startSpan, endSpan, event) {
  3742. var calendar = this.view.calendar;
  3743. var delta = this.diffDates(endSpan[type], startSpan[type]);
  3744. var resizeLocation; // zoned event date properties
  3745. var defaultDuration;
  3746. // build original values to work from, guaranteeing a start and end
  3747. resizeLocation = {
  3748. start: event.start.clone(),
  3749. end: calendar.getEventEnd(event),
  3750. allDay: event.allDay
  3751. };
  3752. // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
  3753. if (resizeLocation.allDay && durationHasTime(delta)) {
  3754. resizeLocation.allDay = false;
  3755. calendar.normalizeEventTimes(resizeLocation);
  3756. }
  3757. resizeLocation[type].add(delta); // apply delta to start or end
  3758. // if the event was compressed too small, find a new reasonable duration for it
  3759. if (!resizeLocation.start.isBefore(resizeLocation.end)) {
  3760. defaultDuration =
  3761. this.minResizeDuration || // TODO: hack
  3762. (event.allDay ?
  3763. calendar.defaultAllDayEventDuration :
  3764. calendar.defaultTimedEventDuration);
  3765. if (type == 'start') { // resizing the start?
  3766. resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration);
  3767. }
  3768. else { // resizing the end?
  3769. resizeLocation.end = resizeLocation.start.clone().add(defaultDuration);
  3770. }
  3771. }
  3772. return resizeLocation;
  3773. },
  3774. // Renders a visual indication of an event being resized.
  3775. // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
  3776. // Must return elements used for any mock events.
  3777. renderEventResize: function(range, seg) {
  3778. // subclasses must implement
  3779. },
  3780. // Unrenders a visual indication of an event being resized.
  3781. unrenderEventResize: function() {
  3782. // subclasses must implement
  3783. },
  3784. /* Rendering Utils
  3785. ------------------------------------------------------------------------------------------------------------------*/
  3786. // Compute the text that should be displayed on an event's element.
  3787. // `range` can be the Event object itself, or something range-like, with at least a `start`.
  3788. // If event times are disabled, or the event has no time, will return a blank string.
  3789. // If not specified, formatStr will default to the eventTimeFormat setting,
  3790. // and displayEnd will default to the displayEventEnd setting.
  3791. getEventTimeText: function(range, formatStr, displayEnd) {
  3792. if (formatStr == null) {
  3793. formatStr = this.eventTimeFormat;
  3794. }
  3795. if (displayEnd == null) {
  3796. displayEnd = this.displayEventEnd;
  3797. }
  3798. if (this.displayEventTime && range.start.hasTime()) {
  3799. if (displayEnd && range.end) {
  3800. return this.view.formatRange(range, formatStr);
  3801. }
  3802. else {
  3803. return range.start.format(formatStr);
  3804. }
  3805. }
  3806. return '';
  3807. },
  3808. // Generic utility for generating the HTML classNames for an event segment's element
  3809. getSegClasses: function(seg, isDraggable, isResizable) {
  3810. var view = this.view;
  3811. var classes = [
  3812. 'fc-event',
  3813. seg.isStart ? 'fc-start' : 'fc-not-start',
  3814. seg.isEnd ? 'fc-end' : 'fc-not-end'
  3815. ].concat(this.getSegCustomClasses(seg));
  3816. if (isDraggable) {
  3817. classes.push('fc-draggable');
  3818. }
  3819. if (isResizable) {
  3820. classes.push('fc-resizable');
  3821. }
  3822. // event is currently selected? attach a className.
  3823. if (view.isEventSelected(seg.event)) {
  3824. classes.push('fc-selected');
  3825. }
  3826. return classes;
  3827. },
  3828. // List of classes that were defined by the caller of the API in some way
  3829. getSegCustomClasses: function(seg) {
  3830. var event = seg.event;
  3831. return [].concat(
  3832. event.className, // guaranteed to be an array
  3833. event.source ? event.source.className : []
  3834. );
  3835. },
  3836. // Utility for generating event skin-related CSS properties
  3837. getSegSkinCss: function(seg) {
  3838. return {
  3839. 'background-color': this.getSegBackgroundColor(seg),
  3840. 'border-color': this.getSegBorderColor(seg),
  3841. color: this.getSegTextColor(seg)
  3842. };
  3843. },
  3844. // Queries for caller-specified color, then falls back to default
  3845. getSegBackgroundColor: function(seg) {
  3846. return seg.event.backgroundColor ||
  3847. seg.event.color ||
  3848. this.getSegDefaultBackgroundColor(seg);
  3849. },
  3850. getSegDefaultBackgroundColor: function(seg) {
  3851. var source = seg.event.source || {};
  3852. return source.backgroundColor ||
  3853. source.color ||
  3854. this.view.opt('eventBackgroundColor') ||
  3855. this.view.opt('eventColor');
  3856. },
  3857. // Queries for caller-specified color, then falls back to default
  3858. getSegBorderColor: function(seg) {
  3859. return seg.event.borderColor ||
  3860. seg.event.color ||
  3861. this.getSegDefaultBorderColor(seg);
  3862. },
  3863. getSegDefaultBorderColor: function(seg) {
  3864. var source = seg.event.source || {};
  3865. return source.borderColor ||
  3866. source.color ||
  3867. this.view.opt('eventBorderColor') ||
  3868. this.view.opt('eventColor');
  3869. },
  3870. // Queries for caller-specified color, then falls back to default
  3871. getSegTextColor: function(seg) {
  3872. return seg.event.textColor ||
  3873. this.getSegDefaultTextColor(seg);
  3874. },
  3875. getSegDefaultTextColor: function(seg) {
  3876. var source = seg.event.source || {};
  3877. return source.textColor ||
  3878. this.view.opt('eventTextColor');
  3879. },
  3880. /* Converting events -> eventRange -> eventSpan -> eventSegs
  3881. ------------------------------------------------------------------------------------------------------------------*/
  3882. // Generates an array of segments for the given single event
  3883. // Can accept an event "location" as well (which only has start/end and no allDay)
  3884. eventToSegs: function(event) {
  3885. return this.eventsToSegs([ event ]);
  3886. },
  3887. eventToSpan: function(event) {
  3888. return this.eventToSpans(event)[0];
  3889. },
  3890. // Generates spans (always unzoned) for the given event.
  3891. // Does not do any inverting for inverse-background events.
  3892. // Can accept an event "location" as well (which only has start/end and no allDay)
  3893. eventToSpans: function(event) {
  3894. var range = this.eventToRange(event);
  3895. return this.eventRangeToSpans(range, event);
  3896. },
  3897. // Converts an array of event objects into an array of event segment objects.
  3898. // A custom `segSliceFunc` may be given for arbitrarily slicing up events.
  3899. // Doesn't guarantee an order for the resulting array.
  3900. eventsToSegs: function(allEvents, segSliceFunc) {
  3901. var _this = this;
  3902. var eventsById = groupEventsById(allEvents);
  3903. var segs = [];
  3904. $.each(eventsById, function(id, events) {
  3905. var ranges = [];
  3906. var i;
  3907. for (i = 0; i < events.length; i++) {
  3908. ranges.push(_this.eventToRange(events[i]));
  3909. }
  3910. // inverse-background events (utilize only the first event in calculations)
  3911. if (isInverseBgEvent(events[0])) {
  3912. ranges = _this.invertRanges(ranges);
  3913. for (i = 0; i < ranges.length; i++) {
  3914. segs.push.apply(segs, // append to
  3915. _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc));
  3916. }
  3917. }
  3918. // normal event ranges
  3919. else {
  3920. for (i = 0; i < ranges.length; i++) {
  3921. segs.push.apply(segs, // append to
  3922. _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc));
  3923. }
  3924. }
  3925. });
  3926. return segs;
  3927. },
  3928. // Generates the unzoned start/end dates an event appears to occupy
  3929. // Can accept an event "location" as well (which only has start/end and no allDay)
  3930. eventToRange: function(event) {
  3931. var calendar = this.view.calendar;
  3932. var start = event.start.clone().stripZone();
  3933. var end = (
  3934. event.end ?
  3935. event.end.clone() :
  3936. // derive the end from the start and allDay. compute allDay if necessary
  3937. calendar.getDefaultEventEnd(
  3938. event.allDay != null ?
  3939. event.allDay :
  3940. !event.start.hasTime(),
  3941. event.start
  3942. )
  3943. ).stripZone();
  3944. // hack: dynamic locale change forgets to upate stored event localed
  3945. calendar.localizeMoment(start);
  3946. calendar.localizeMoment(end);
  3947. return { start: start, end: end };
  3948. },
  3949. // Given an event's range (unzoned start/end), and the event itself,
  3950. // slice into segments (using the segSliceFunc function if specified)
  3951. eventRangeToSegs: function(range, event, segSliceFunc) {
  3952. var spans = this.eventRangeToSpans(range, event);
  3953. var segs = [];
  3954. var i;
  3955. for (i = 0; i < spans.length; i++) {
  3956. segs.push.apply(segs, // append to
  3957. this.eventSpanToSegs(spans[i], event, segSliceFunc));
  3958. }
  3959. return segs;
  3960. },
  3961. // Given an event's unzoned date range, return an array of "span" objects.
  3962. // Subclasses can override.
  3963. eventRangeToSpans: function(range, event) {
  3964. return [ $.extend({}, range) ]; // copy into a single-item array
  3965. },
  3966. // Given an event's span (unzoned start/end and other misc data), and the event itself,
  3967. // slices into segments and attaches event-derived properties to them.
  3968. eventSpanToSegs: function(span, event, segSliceFunc) {
  3969. var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span);
  3970. var i, seg;
  3971. for (i = 0; i < segs.length; i++) {
  3972. seg = segs[i];
  3973. seg.event = event;
  3974. seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned
  3975. seg.eventDurationMS = span.end - span.start;
  3976. }
  3977. return segs;
  3978. },
  3979. // Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
  3980. // SIDE EFFECT: will mutate the given array and will use its date references.
  3981. invertRanges: function(ranges) {
  3982. var view = this.view;
  3983. var viewStart = view.start.clone(); // need a copy
  3984. var viewEnd = view.end.clone(); // need a copy
  3985. var inverseRanges = [];
  3986. var start = viewStart; // the end of the previous range. the start of the new range
  3987. var i, range;
  3988. // ranges need to be in order. required for our date-walking algorithm
  3989. ranges.sort(compareRanges);
  3990. for (i = 0; i < ranges.length; i++) {
  3991. range = ranges[i];
  3992. // add the span of time before the event (if there is any)
  3993. if (range.start > start) { // compare millisecond time (skip any ambig logic)
  3994. inverseRanges.push({
  3995. start: start,
  3996. end: range.start
  3997. });
  3998. }
  3999. start = range.end;
  4000. }
  4001. // add the span of time after the last event (if there is any)
  4002. if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  4003. inverseRanges.push({
  4004. start: start,
  4005. end: viewEnd
  4006. });
  4007. }
  4008. return inverseRanges;
  4009. },
  4010. sortEventSegs: function(segs) {
  4011. segs.sort(proxy(this, 'compareEventSegs'));
  4012. },
  4013. // A cmp function for determining which segments should take visual priority
  4014. compareEventSegs: function(seg1, seg2) {
  4015. return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  4016. seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  4017. seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  4018. compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
  4019. }
  4020. });
  4021. /* Utilities
  4022. ----------------------------------------------------------------------------------------------------------------------*/
  4023. function pluckEventDateProps(event) {
  4024. return {
  4025. start: event.start.clone(),
  4026. end: event.end ? event.end.clone() : null,
  4027. allDay: event.allDay // keep it the same
  4028. };
  4029. }
  4030. FC.pluckEventDateProps = pluckEventDateProps;
  4031. function isBgEvent(event) { // returns true if background OR inverse-background
  4032. var rendering = getEventRendering(event);
  4033. return rendering === 'background' || rendering === 'inverse-background';
  4034. }
  4035. FC.isBgEvent = isBgEvent; // export
  4036. function isInverseBgEvent(event) {
  4037. return getEventRendering(event) === 'inverse-background';
  4038. }
  4039. function getEventRendering(event) {
  4040. return firstDefined((event.source || {}).rendering, event.rendering);
  4041. }
  4042. function groupEventsById(events) {
  4043. var eventsById = {};
  4044. var i, event;
  4045. for (i = 0; i < events.length; i++) {
  4046. event = events[i];
  4047. (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  4048. }
  4049. return eventsById;
  4050. }
  4051. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  4052. function compareRanges(range1, range2) {
  4053. return range1.start - range2.start; // earlier ranges go first
  4054. }
  4055. /* External-Dragging-Element Data
  4056. ----------------------------------------------------------------------------------------------------------------------*/
  4057. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  4058. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  4059. FC.dataAttrPrefix = '';
  4060. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  4061. // to be used for Event Object creation.
  4062. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  4063. function getDraggedElMeta(el) {
  4064. var prefix = FC.dataAttrPrefix;
  4065. var eventProps; // properties for creating the event, not related to date/time
  4066. var startTime; // a Duration
  4067. var duration;
  4068. var stick;
  4069. if (prefix) { prefix += '-'; }
  4070. eventProps = el.data(prefix + 'event') || null;
  4071. if (eventProps) {
  4072. if (typeof eventProps === 'object') {
  4073. eventProps = $.extend({}, eventProps); // make a copy
  4074. }
  4075. else { // something like 1 or true. still signal event creation
  4076. eventProps = {};
  4077. }
  4078. // pluck special-cased date/time properties
  4079. startTime = eventProps.start;
  4080. if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  4081. duration = eventProps.duration;
  4082. stick = eventProps.stick;
  4083. delete eventProps.start;
  4084. delete eventProps.time;
  4085. delete eventProps.duration;
  4086. delete eventProps.stick;
  4087. }
  4088. // fallback to standalone attribute values for each of the date/time properties
  4089. if (startTime == null) { startTime = el.data(prefix + 'start'); }
  4090. if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  4091. if (duration == null) { duration = el.data(prefix + 'duration'); }
  4092. if (stick == null) { stick = el.data(prefix + 'stick'); }
  4093. // massage into correct data types
  4094. startTime = startTime != null ? moment.duration(startTime) : null;
  4095. duration = duration != null ? moment.duration(duration) : null;
  4096. stick = Boolean(stick);
  4097. return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  4098. }
  4099. ;;
  4100. /*
  4101. A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
  4102. Prerequisite: the object being mixed into needs to be a *Grid*
  4103. */
  4104. var DayTableMixin = FC.DayTableMixin = {
  4105. breakOnWeeks: false, // should create a new row for each week?
  4106. dayDates: null, // whole-day dates for each column. left to right
  4107. dayIndices: null, // for each day from start, the offset
  4108. daysPerRow: null,
  4109. rowCnt: null,
  4110. colCnt: null,
  4111. colHeadFormat: null,
  4112. // Populates internal variables used for date calculation and rendering
  4113. updateDayTable: function() {
  4114. var view = this.view;
  4115. var date = this.start.clone();
  4116. var dayIndex = -1;
  4117. var dayIndices = [];
  4118. var dayDates = [];
  4119. var daysPerRow;
  4120. var firstDay;
  4121. var rowCnt;
  4122. while (date.isBefore(this.end)) { // loop each day from start to end
  4123. if (view.isHiddenDay(date)) {
  4124. dayIndices.push(dayIndex + 0.5); // mark that it's between indices
  4125. }
  4126. else {
  4127. dayIndex++;
  4128. dayIndices.push(dayIndex);
  4129. dayDates.push(date.clone());
  4130. }
  4131. date.add(1, 'days');
  4132. }
  4133. if (this.breakOnWeeks) {
  4134. // count columns until the day-of-week repeats
  4135. firstDay = dayDates[0].day();
  4136. for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
  4137. if (dayDates[daysPerRow].day() == firstDay) {
  4138. break;
  4139. }
  4140. }
  4141. rowCnt = Math.ceil(dayDates.length / daysPerRow);
  4142. }
  4143. else {
  4144. rowCnt = 1;
  4145. daysPerRow = dayDates.length;
  4146. }
  4147. this.dayDates = dayDates;
  4148. this.dayIndices = dayIndices;
  4149. this.daysPerRow = daysPerRow;
  4150. this.rowCnt = rowCnt;
  4151. this.updateDayTableCols();
  4152. },
  4153. // Computes and assigned the colCnt property and updates any options that may be computed from it
  4154. updateDayTableCols: function() {
  4155. this.colCnt = this.computeColCnt();
  4156. this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat();
  4157. },
  4158. // Determines how many columns there should be in the table
  4159. computeColCnt: function() {
  4160. return this.daysPerRow;
  4161. },
  4162. // Computes the ambiguously-timed moment for the given cell
  4163. getCellDate: function(row, col) {
  4164. return this.dayDates[
  4165. this.getCellDayIndex(row, col)
  4166. ].clone();
  4167. },
  4168. // Computes the ambiguously-timed date range for the given cell
  4169. getCellRange: function(row, col) {
  4170. var start = this.getCellDate(row, col);
  4171. var end = start.clone().add(1, 'days');
  4172. return { start: start, end: end };
  4173. },
  4174. // Returns the number of day cells, chronologically, from the first of the grid (0-based)
  4175. getCellDayIndex: function(row, col) {
  4176. return row * this.daysPerRow + this.getColDayIndex(col);
  4177. },
  4178. // Returns the numner of day cells, chronologically, from the first cell in *any given row*
  4179. getColDayIndex: function(col) {
  4180. if (this.isRTL) {
  4181. return this.colCnt - 1 - col;
  4182. }
  4183. else {
  4184. return col;
  4185. }
  4186. },
  4187. // Given a date, returns its chronolocial cell-index from the first cell of the grid.
  4188. // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
  4189. // If before the first offset, returns a negative number.
  4190. // If after the last offset, returns an offset past the last cell offset.
  4191. // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
  4192. getDateDayIndex: function(date) {
  4193. var dayIndices = this.dayIndices;
  4194. var dayOffset = date.diff(this.start, 'days');
  4195. if (dayOffset < 0) {
  4196. return dayIndices[0] - 1;
  4197. }
  4198. else if (dayOffset >= dayIndices.length) {
  4199. return dayIndices[dayIndices.length - 1] + 1;
  4200. }
  4201. else {
  4202. return dayIndices[dayOffset];
  4203. }
  4204. },
  4205. /* Options
  4206. ------------------------------------------------------------------------------------------------------------------*/
  4207. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  4208. computeColHeadFormat: function() {
  4209. // if more than one week row, or if there are a lot of columns with not much space,
  4210. // put just the day numbers will be in each cell
  4211. if (this.rowCnt > 1 || this.colCnt > 10) {
  4212. return 'ddd'; // "Sat"
  4213. }
  4214. // multiple days, so full single date string WON'T be in title text
  4215. else if (this.colCnt > 1) {
  4216. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  4217. }
  4218. // single day, so full single date string will probably be in title text
  4219. else {
  4220. return 'dddd'; // "Saturday"
  4221. }
  4222. },
  4223. /* Slicing
  4224. ------------------------------------------------------------------------------------------------------------------*/
  4225. // Slices up a date range into a segment for every week-row it intersects with
  4226. sliceRangeByRow: function(range) {
  4227. var daysPerRow = this.daysPerRow;
  4228. var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  4229. var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
  4230. var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
  4231. var segs = [];
  4232. var row;
  4233. var rowFirst, rowLast; // inclusive day-index range for current row
  4234. var segFirst, segLast; // inclusive day-index range for segment
  4235. for (row = 0; row < this.rowCnt; row++) {
  4236. rowFirst = row * daysPerRow;
  4237. rowLast = rowFirst + daysPerRow - 1;
  4238. // intersect segment's offset range with the row's
  4239. segFirst = Math.max(rangeFirst, rowFirst);
  4240. segLast = Math.min(rangeLast, rowLast);
  4241. // deal with in-between indices
  4242. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  4243. segLast = Math.floor(segLast); // in-between ends round to prev cell
  4244. if (segFirst <= segLast) { // was there any intersection with the current row?
  4245. segs.push({
  4246. row: row,
  4247. // normalize to start of row
  4248. firstRowDayIndex: segFirst - rowFirst,
  4249. lastRowDayIndex: segLast - rowFirst,
  4250. // must be matching integers to be the segment's start/end
  4251. isStart: segFirst === rangeFirst,
  4252. isEnd: segLast === rangeLast
  4253. });
  4254. }
  4255. }
  4256. return segs;
  4257. },
  4258. // Slices up a date range into a segment for every day-cell it intersects with.
  4259. // TODO: make more DRY with sliceRangeByRow somehow.
  4260. sliceRangeByDay: function(range) {
  4261. var daysPerRow = this.daysPerRow;
  4262. var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  4263. var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
  4264. var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
  4265. var segs = [];
  4266. var row;
  4267. var rowFirst, rowLast; // inclusive day-index range for current row
  4268. var i;
  4269. var segFirst, segLast; // inclusive day-index range for segment
  4270. for (row = 0; row < this.rowCnt; row++) {
  4271. rowFirst = row * daysPerRow;
  4272. rowLast = rowFirst + daysPerRow - 1;
  4273. for (i = rowFirst; i <= rowLast; i++) {
  4274. // intersect segment's offset range with the row's
  4275. segFirst = Math.max(rangeFirst, i);
  4276. segLast = Math.min(rangeLast, i);
  4277. // deal with in-between indices
  4278. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  4279. segLast = Math.floor(segLast); // in-between ends round to prev cell
  4280. if (segFirst <= segLast) { // was there any intersection with the current row?
  4281. segs.push({
  4282. row: row,
  4283. // normalize to start of row
  4284. firstRowDayIndex: segFirst - rowFirst,
  4285. lastRowDayIndex: segLast - rowFirst,
  4286. // must be matching integers to be the segment's start/end
  4287. isStart: segFirst === rangeFirst,
  4288. isEnd: segLast === rangeLast
  4289. });
  4290. }
  4291. }
  4292. }
  4293. return segs;
  4294. },
  4295. /* Header Rendering
  4296. ------------------------------------------------------------------------------------------------------------------*/
  4297. renderHeadHtml: function() {
  4298. var view = this.view;
  4299. return '' +
  4300. '<div class="fc-row ' + view.widgetHeaderClass + '">' +
  4301. '<table>' +
  4302. '<thead>' +
  4303. this.renderHeadTrHtml() +
  4304. '</thead>' +
  4305. '</table>' +
  4306. '</div>';
  4307. },
  4308. renderHeadIntroHtml: function() {
  4309. return this.renderIntroHtml(); // fall back to generic
  4310. },
  4311. renderHeadTrHtml: function() {
  4312. return '' +
  4313. '<tr>' +
  4314. (this.isRTL ? '' : this.renderHeadIntroHtml()) +
  4315. this.renderHeadDateCellsHtml() +
  4316. (this.isRTL ? this.renderHeadIntroHtml() : '') +
  4317. '</tr>';
  4318. },
  4319. renderHeadDateCellsHtml: function() {
  4320. var htmls = [];
  4321. var col, date;
  4322. for (col = 0; col < this.colCnt; col++) {
  4323. date = this.getCellDate(0, col);
  4324. htmls.push(this.renderHeadDateCellHtml(date));
  4325. }
  4326. return htmls.join('');
  4327. },
  4328. // TODO: when internalApiVersion, accept an object for HTML attributes
  4329. // (colspan should be no different)
  4330. renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
  4331. var view = this.view;
  4332. var classNames = [
  4333. 'fc-day-header',
  4334. view.widgetHeaderClass
  4335. ];
  4336. // if only one row of days, the classNames on the header can represent the specific days beneath
  4337. if (this.rowCnt === 1) {
  4338. classNames = classNames.concat(
  4339. // includes the day-of-week class
  4340. // noThemeHighlight=true (don't highlight the header)
  4341. this.getDayClasses(date, true)
  4342. );
  4343. }
  4344. else {
  4345. classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class
  4346. }
  4347. return '' +
  4348. '<th class="' + classNames.join(' ') + '"' +
  4349. (this.rowCnt === 1 ?
  4350. ' data-date="' + date.format('YYYY-MM-DD') + '"' :
  4351. '') +
  4352. (colspan > 1 ?
  4353. ' colspan="' + colspan + '"' :
  4354. '') +
  4355. (otherAttrs ?
  4356. ' ' + otherAttrs :
  4357. '') +
  4358. '>' +
  4359. // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
  4360. view.buildGotoAnchorHtml(
  4361. { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
  4362. htmlEscape(date.format(this.colHeadFormat)) // inner HTML
  4363. ) +
  4364. '</th>';
  4365. },
  4366. /* Background Rendering
  4367. ------------------------------------------------------------------------------------------------------------------*/
  4368. renderBgTrHtml: function(row) {
  4369. return '' +
  4370. '<tr>' +
  4371. (this.isRTL ? '' : this.renderBgIntroHtml(row)) +
  4372. this.renderBgCellsHtml(row) +
  4373. (this.isRTL ? this.renderBgIntroHtml(row) : '') +
  4374. '</tr>';
  4375. },
  4376. renderBgIntroHtml: function(row) {
  4377. return this.renderIntroHtml(); // fall back to generic
  4378. },
  4379. renderBgCellsHtml: function(row) {
  4380. var htmls = [];
  4381. var col, date;
  4382. for (col = 0; col < this.colCnt; col++) {
  4383. date = this.getCellDate(row, col);
  4384. htmls.push(this.renderBgCellHtml(date));
  4385. }
  4386. return htmls.join('');
  4387. },
  4388. renderBgCellHtml: function(date, otherAttrs) {
  4389. var view = this.view;
  4390. var classes = this.getDayClasses(date);
  4391. classes.unshift('fc-day', view.widgetContentClass);
  4392. return '<td class="' + classes.join(' ') + '"' +
  4393. ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  4394. (otherAttrs ?
  4395. ' ' + otherAttrs :
  4396. '') +
  4397. '></td>';
  4398. },
  4399. /* Generic
  4400. ------------------------------------------------------------------------------------------------------------------*/
  4401. // Generates the default HTML intro for any row. User classes should override
  4402. renderIntroHtml: function() {
  4403. },
  4404. // TODO: a generic method for dealing with <tr>, RTL, intro
  4405. // when increment internalApiVersion
  4406. // wrapTr (scheduler)
  4407. /* Utils
  4408. ------------------------------------------------------------------------------------------------------------------*/
  4409. // Applies the generic "intro" and "outro" HTML to the given cells.
  4410. // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  4411. bookendCells: function(trEl) {
  4412. var introHtml = this.renderIntroHtml();
  4413. if (introHtml) {
  4414. if (this.isRTL) {
  4415. trEl.append(introHtml);
  4416. }
  4417. else {
  4418. trEl.prepend(introHtml);
  4419. }
  4420. }
  4421. }
  4422. };
  4423. ;;
  4424. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  4425. ----------------------------------------------------------------------------------------------------------------------*/
  4426. var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
  4427. numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  4428. bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  4429. rowEls: null, // set of fake row elements
  4430. cellEls: null, // set of whole-day elements comprising the row's background
  4431. helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  4432. rowCoordCache: null,
  4433. colCoordCache: null,
  4434. // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  4435. // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  4436. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  4437. renderDates: function(isRigid) {
  4438. var view = this.view;
  4439. var rowCnt = this.rowCnt;
  4440. var colCnt = this.colCnt;
  4441. var html = '';
  4442. var row;
  4443. var col;
  4444. for (row = 0; row < rowCnt; row++) {
  4445. html += this.renderDayRowHtml(row, isRigid);
  4446. }
  4447. this.el.html(html);
  4448. this.rowEls = this.el.find('.fc-row');
  4449. this.cellEls = this.el.find('.fc-day');
  4450. this.rowCoordCache = new CoordCache({
  4451. els: this.rowEls,
  4452. isVertical: true
  4453. });
  4454. this.colCoordCache = new CoordCache({
  4455. els: this.cellEls.slice(0, this.colCnt), // only the first row
  4456. isHorizontal: true
  4457. });
  4458. // trigger dayRender with each cell's element
  4459. for (row = 0; row < rowCnt; row++) {
  4460. for (col = 0; col < colCnt; col++) {
  4461. view.publiclyTrigger(
  4462. 'dayRender',
  4463. null,
  4464. this.getCellDate(row, col),
  4465. this.getCellEl(row, col)
  4466. );
  4467. }
  4468. }
  4469. },
  4470. unrenderDates: function() {
  4471. this.removeSegPopover();
  4472. },
  4473. renderBusinessHours: function() {
  4474. var segs = this.buildBusinessHourSegs(true); // wholeDay=true
  4475. this.renderFill('businessHours', segs, 'bgevent');
  4476. },
  4477. unrenderBusinessHours: function() {
  4478. this.unrenderFill('businessHours');
  4479. },
  4480. // Generates the HTML for a single row, which is a div that wraps a table.
  4481. // `row` is the row number.
  4482. renderDayRowHtml: function(row, isRigid) {
  4483. var view = this.view;
  4484. var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  4485. if (isRigid) {
  4486. classes.push('fc-rigid');
  4487. }
  4488. return '' +
  4489. '<div class="' + classes.join(' ') + '">' +
  4490. '<div class="fc-bg">' +
  4491. '<table>' +
  4492. this.renderBgTrHtml(row) +
  4493. '</table>' +
  4494. '</div>' +
  4495. '<div class="fc-content-skeleton">' +
  4496. '<table>' +
  4497. (this.numbersVisible ?
  4498. '<thead>' +
  4499. this.renderNumberTrHtml(row) +
  4500. '</thead>' :
  4501. ''
  4502. ) +
  4503. '</table>' +
  4504. '</div>' +
  4505. '</div>';
  4506. },
  4507. /* Grid Number Rendering
  4508. ------------------------------------------------------------------------------------------------------------------*/
  4509. renderNumberTrHtml: function(row) {
  4510. return '' +
  4511. '<tr>' +
  4512. (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
  4513. this.renderNumberCellsHtml(row) +
  4514. (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
  4515. '</tr>';
  4516. },
  4517. renderNumberIntroHtml: function(row) {
  4518. return this.renderIntroHtml();
  4519. },
  4520. renderNumberCellsHtml: function(row) {
  4521. var htmls = [];
  4522. var col, date;
  4523. for (col = 0; col < this.colCnt; col++) {
  4524. date = this.getCellDate(row, col);
  4525. htmls.push(this.renderNumberCellHtml(date));
  4526. }
  4527. return htmls.join('');
  4528. },
  4529. // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  4530. // The number row will only exist if either day numbers or week numbers are turned on.
  4531. renderNumberCellHtml: function(date) {
  4532. var html = '';
  4533. var classes;
  4534. var weekCalcFirstDoW;
  4535. if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) {
  4536. // no numbers in day cell (week number must be along the side)
  4537. return '<td/>'; // will create an empty space above events :(
  4538. }
  4539. classes = this.getDayClasses(date);
  4540. classes.unshift('fc-day-top');
  4541. if (this.view.cellWeekNumbersVisible) {
  4542. // To determine the day of week number change under ISO, we cannot
  4543. // rely on moment.js methods such as firstDayOfWeek() or weekday(),
  4544. // because they rely on the locale's dow (possibly overridden by
  4545. // our firstDay option), which may not be Monday. We cannot change
  4546. // dow, because that would affect the calendar start day as well.
  4547. if (date._locale._fullCalendar_weekCalc === 'ISO') {
  4548. weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
  4549. }
  4550. else {
  4551. weekCalcFirstDoW = date._locale.firstDayOfWeek();
  4552. }
  4553. }
  4554. html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">';
  4555. if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
  4556. html += this.view.buildGotoAnchorHtml(
  4557. { date: date, type: 'week' },
  4558. { 'class': 'fc-week-number' },
  4559. date.format('w') // inner HTML
  4560. );
  4561. }
  4562. if (this.view.dayNumbersVisible) {
  4563. html += this.view.buildGotoAnchorHtml(
  4564. date,
  4565. { 'class': 'fc-day-number' },
  4566. date.date() // inner HTML
  4567. );
  4568. }
  4569. html += '</td>';
  4570. return html;
  4571. },
  4572. /* Options
  4573. ------------------------------------------------------------------------------------------------------------------*/
  4574. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  4575. computeEventTimeFormat: function() {
  4576. return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
  4577. },
  4578. // Computes a default `displayEventEnd` value if one is not expliclty defined
  4579. computeDisplayEventEnd: function() {
  4580. return this.colCnt == 1; // we'll likely have space if there's only one day
  4581. },
  4582. /* Dates
  4583. ------------------------------------------------------------------------------------------------------------------*/
  4584. rangeUpdated: function() {
  4585. this.updateDayTable();
  4586. },
  4587. // Slices up the given span (unzoned start/end with other misc data) into an array of segments
  4588. spanToSegs: function(span) {
  4589. var segs = this.sliceRangeByRow(span);
  4590. var i, seg;
  4591. for (i = 0; i < segs.length; i++) {
  4592. seg = segs[i];
  4593. if (this.isRTL) {
  4594. seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
  4595. seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
  4596. }
  4597. else {
  4598. seg.leftCol = seg.firstRowDayIndex;
  4599. seg.rightCol = seg.lastRowDayIndex;
  4600. }
  4601. }
  4602. return segs;
  4603. },
  4604. /* Hit System
  4605. ------------------------------------------------------------------------------------------------------------------*/
  4606. prepareHits: function() {
  4607. this.colCoordCache.build();
  4608. this.rowCoordCache.build();
  4609. this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
  4610. },
  4611. releaseHits: function() {
  4612. this.colCoordCache.clear();
  4613. this.rowCoordCache.clear();
  4614. },
  4615. queryHit: function(leftOffset, topOffset) {
  4616. if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
  4617. var col = this.colCoordCache.getHorizontalIndex(leftOffset);
  4618. var row = this.rowCoordCache.getVerticalIndex(topOffset);
  4619. if (row != null && col != null) {
  4620. return this.getCellHit(row, col);
  4621. }
  4622. }
  4623. },
  4624. getHitSpan: function(hit) {
  4625. return this.getCellRange(hit.row, hit.col);
  4626. },
  4627. getHitEl: function(hit) {
  4628. return this.getCellEl(hit.row, hit.col);
  4629. },
  4630. /* Cell System
  4631. ------------------------------------------------------------------------------------------------------------------*/
  4632. // FYI: the first column is the leftmost column, regardless of date
  4633. getCellHit: function(row, col) {
  4634. return {
  4635. row: row,
  4636. col: col,
  4637. component: this, // needed unfortunately :(
  4638. left: this.colCoordCache.getLeftOffset(col),
  4639. right: this.colCoordCache.getRightOffset(col),
  4640. top: this.rowCoordCache.getTopOffset(row),
  4641. bottom: this.rowCoordCache.getBottomOffset(row)
  4642. };
  4643. },
  4644. getCellEl: function(row, col) {
  4645. return this.cellEls.eq(row * this.colCnt + col);
  4646. },
  4647. /* Event Drag Visualization
  4648. ------------------------------------------------------------------------------------------------------------------*/
  4649. // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
  4650. // Renders a visual indication of an event or external element being dragged.
  4651. // `eventLocation` has zoned start and end (optional)
  4652. renderDrag: function(eventLocation, seg) {
  4653. // always render a highlight underneath
  4654. this.renderHighlight(this.eventToSpan(eventLocation));
  4655. // if a segment from the same calendar but another component is being dragged, render a helper event
  4656. if (seg && seg.component !== this) {
  4657. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  4658. }
  4659. },
  4660. // Unrenders any visual indication of a hovering event
  4661. unrenderDrag: function() {
  4662. this.unrenderHighlight();
  4663. this.unrenderHelper();
  4664. },
  4665. /* Event Resize Visualization
  4666. ------------------------------------------------------------------------------------------------------------------*/
  4667. // Renders a visual indication of an event being resized
  4668. renderEventResize: function(eventLocation, seg) {
  4669. this.renderHighlight(this.eventToSpan(eventLocation));
  4670. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  4671. },
  4672. // Unrenders a visual indication of an event being resized
  4673. unrenderEventResize: function() {
  4674. this.unrenderHighlight();
  4675. this.unrenderHelper();
  4676. },
  4677. /* Event Helper
  4678. ------------------------------------------------------------------------------------------------------------------*/
  4679. // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  4680. renderHelper: function(event, sourceSeg) {
  4681. var helperNodes = [];
  4682. var segs = this.eventToSegs(event);
  4683. var rowStructs;
  4684. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  4685. rowStructs = this.renderSegRows(segs);
  4686. // inject each new event skeleton into each associated row
  4687. this.rowEls.each(function(row, rowNode) {
  4688. var rowEl = $(rowNode); // the .fc-row
  4689. var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  4690. var skeletonTop;
  4691. // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  4692. if (sourceSeg && sourceSeg.row === row) {
  4693. skeletonTop = sourceSeg.el.position().top;
  4694. }
  4695. else {
  4696. skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  4697. }
  4698. skeletonEl.css('top', skeletonTop)
  4699. .find('table')
  4700. .append(rowStructs[row].tbodyEl);
  4701. rowEl.append(skeletonEl);
  4702. helperNodes.push(skeletonEl[0]);
  4703. });
  4704. return ( // must return the elements rendered
  4705. this.helperEls = $(helperNodes) // array -> jQuery set
  4706. );
  4707. },
  4708. // Unrenders any visual indication of a mock helper event
  4709. unrenderHelper: function() {
  4710. if (this.helperEls) {
  4711. this.helperEls.remove();
  4712. this.helperEls = null;
  4713. }
  4714. },
  4715. /* Fill System (highlight, background events, business hours)
  4716. ------------------------------------------------------------------------------------------------------------------*/
  4717. fillSegTag: 'td', // override the default tag name
  4718. // Renders a set of rectangles over the given segments of days.
  4719. // Only returns segments that successfully rendered.
  4720. renderFill: function(type, segs, className) {
  4721. var nodes = [];
  4722. var i, seg;
  4723. var skeletonEl;
  4724. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  4725. for (i = 0; i < segs.length; i++) {
  4726. seg = segs[i];
  4727. skeletonEl = this.renderFillRow(type, seg, className);
  4728. this.rowEls.eq(seg.row).append(skeletonEl);
  4729. nodes.push(skeletonEl[0]);
  4730. }
  4731. this.elsByFill[type] = $(nodes);
  4732. return segs;
  4733. },
  4734. // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  4735. renderFillRow: function(type, seg, className) {
  4736. var colCnt = this.colCnt;
  4737. var startCol = seg.leftCol;
  4738. var endCol = seg.rightCol + 1;
  4739. var skeletonEl;
  4740. var trEl;
  4741. className = className || type.toLowerCase();
  4742. skeletonEl = $(
  4743. '<div class="fc-' + className + '-skeleton">' +
  4744. '<table><tr/></table>' +
  4745. '</div>'
  4746. );
  4747. trEl = skeletonEl.find('tr');
  4748. if (startCol > 0) {
  4749. trEl.append('<td colspan="' + startCol + '"/>');
  4750. }
  4751. trEl.append(
  4752. seg.el.attr('colspan', endCol - startCol)
  4753. );
  4754. if (endCol < colCnt) {
  4755. trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  4756. }
  4757. this.bookendCells(trEl);
  4758. return skeletonEl;
  4759. }
  4760. });
  4761. ;;
  4762. /* Event-rendering methods for the DayGrid class
  4763. ----------------------------------------------------------------------------------------------------------------------*/
  4764. DayGrid.mixin({
  4765. rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  4766. // Unrenders all events currently rendered on the grid
  4767. unrenderEvents: function() {
  4768. this.removeSegPopover(); // removes the "more.." events popover
  4769. Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
  4770. },
  4771. // Retrieves all rendered segment objects currently rendered on the grid
  4772. getEventSegs: function() {
  4773. return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
  4774. .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  4775. },
  4776. // Renders the given background event segments onto the grid
  4777. renderBgSegs: function(segs) {
  4778. // don't render timed background events
  4779. var allDaySegs = $.grep(segs, function(seg) {
  4780. return seg.event.allDay;
  4781. });
  4782. return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  4783. },
  4784. // Renders the given foreground event segments onto the grid
  4785. renderFgSegs: function(segs) {
  4786. var rowStructs;
  4787. // render an `.el` on each seg
  4788. // returns a subset of the segs. segs that were actually rendered
  4789. segs = this.renderFgSegEls(segs);
  4790. rowStructs = this.rowStructs = this.renderSegRows(segs);
  4791. // append to each row's content skeleton
  4792. this.rowEls.each(function(i, rowNode) {
  4793. $(rowNode).find('.fc-content-skeleton > table').append(
  4794. rowStructs[i].tbodyEl
  4795. );
  4796. });
  4797. return segs; // return only the segs that were actually rendered
  4798. },
  4799. // Unrenders all currently rendered foreground event segments
  4800. unrenderFgSegs: function() {
  4801. var rowStructs = this.rowStructs || [];
  4802. var rowStruct;
  4803. while ((rowStruct = rowStructs.pop())) {
  4804. rowStruct.tbodyEl.remove();
  4805. }
  4806. this.rowStructs = null;
  4807. },
  4808. // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  4809. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  4810. // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  4811. renderSegRows: function(segs) {
  4812. var rowStructs = [];
  4813. var segRows;
  4814. var row;
  4815. segRows = this.groupSegRows(segs); // group into nested arrays
  4816. // iterate each row of segment groupings
  4817. for (row = 0; row < segRows.length; row++) {
  4818. rowStructs.push(
  4819. this.renderSegRow(row, segRows[row])
  4820. );
  4821. }
  4822. return rowStructs;
  4823. },
  4824. // Builds the HTML to be used for the default element for an individual segment
  4825. fgSegHtml: function(seg, disableResizing) {
  4826. var view = this.view;
  4827. var event = seg.event;
  4828. var isDraggable = view.isEventDraggable(event);
  4829. var isResizableFromStart = !disableResizing && event.allDay &&
  4830. seg.isStart && view.isEventResizableFromStart(event);
  4831. var isResizableFromEnd = !disableResizing && event.allDay &&
  4832. seg.isEnd && view.isEventResizableFromEnd(event);
  4833. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  4834. var skinCss = cssToStr(this.getSegSkinCss(seg));
  4835. var timeHtml = '';
  4836. var timeText;
  4837. var titleHtml;
  4838. classes.unshift('fc-day-grid-event', 'fc-h-event');
  4839. // Only display a timed events time if it is the starting segment
  4840. if (seg.isStart) {
  4841. timeText = this.getEventTimeText(event);
  4842. if (timeText) {
  4843. timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
  4844. }
  4845. }
  4846. titleHtml =
  4847. '<span class="fc-title">' +
  4848. (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  4849. '</span>';
  4850. return '<a class="' + classes.join(' ') + '"' +
  4851. (event.url ?
  4852. ' href="' + htmlEscape(event.url) + '"' :
  4853. ''
  4854. ) +
  4855. (skinCss ?
  4856. ' style="' + skinCss + '"' :
  4857. ''
  4858. ) +
  4859. '>' +
  4860. '<div class="fc-content">' +
  4861. (this.isRTL ?
  4862. titleHtml + ' ' + timeHtml : // put a natural space in between
  4863. timeHtml + ' ' + titleHtml //
  4864. ) +
  4865. '</div>' +
  4866. (isResizableFromStart ?
  4867. '<div class="fc-resizer fc-start-resizer" />' :
  4868. ''
  4869. ) +
  4870. (isResizableFromEnd ?
  4871. '<div class="fc-resizer fc-end-resizer" />' :
  4872. ''
  4873. ) +
  4874. '</a>';
  4875. },
  4876. // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  4877. // the segments. Returns object with a bunch of internal data about how the render was calculated.
  4878. // NOTE: modifies rowSegs
  4879. renderSegRow: function(row, rowSegs) {
  4880. var colCnt = this.colCnt;
  4881. var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  4882. var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  4883. var tbody = $('<tbody/>');
  4884. var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  4885. var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  4886. var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  4887. var i, levelSegs;
  4888. var col;
  4889. var tr;
  4890. var j, seg;
  4891. var td;
  4892. // populates empty cells from the current column (`col`) to `endCol`
  4893. function emptyCellsUntil(endCol) {
  4894. while (col < endCol) {
  4895. // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  4896. td = (loneCellMatrix[i - 1] || [])[col];
  4897. if (td) {
  4898. td.attr(
  4899. 'rowspan',
  4900. parseInt(td.attr('rowspan') || 1, 10) + 1
  4901. );
  4902. }
  4903. else {
  4904. td = $('<td/>');
  4905. tr.append(td);
  4906. }
  4907. cellMatrix[i][col] = td;
  4908. loneCellMatrix[i][col] = td;
  4909. col++;
  4910. }
  4911. }
  4912. for (i = 0; i < levelCnt; i++) { // iterate through all levels
  4913. levelSegs = segLevels[i];
  4914. col = 0;
  4915. tr = $('<tr/>');
  4916. segMatrix.push([]);
  4917. cellMatrix.push([]);
  4918. loneCellMatrix.push([]);
  4919. // levelCnt might be 1 even though there are no actual levels. protect against this.
  4920. // this single empty row is useful for styling.
  4921. if (levelSegs) {
  4922. for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  4923. seg = levelSegs[j];
  4924. emptyCellsUntil(seg.leftCol);
  4925. // create a container that occupies or more columns. append the event element.
  4926. td = $('<td class="fc-event-container"/>').append(seg.el);
  4927. if (seg.leftCol != seg.rightCol) {
  4928. td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  4929. }
  4930. else { // a single-column segment
  4931. loneCellMatrix[i][col] = td;
  4932. }
  4933. while (col <= seg.rightCol) {
  4934. cellMatrix[i][col] = td;
  4935. segMatrix[i][col] = seg;
  4936. col++;
  4937. }
  4938. tr.append(td);
  4939. }
  4940. }
  4941. emptyCellsUntil(colCnt); // finish off the row
  4942. this.bookendCells(tr);
  4943. tbody.append(tr);
  4944. }
  4945. return { // a "rowStruct"
  4946. row: row, // the row number
  4947. tbodyEl: tbody,
  4948. cellMatrix: cellMatrix,
  4949. segMatrix: segMatrix,
  4950. segLevels: segLevels,
  4951. segs: rowSegs
  4952. };
  4953. },
  4954. // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  4955. // NOTE: modifies segs
  4956. buildSegLevels: function(segs) {
  4957. var levels = [];
  4958. var i, seg;
  4959. var j;
  4960. // Give preference to elements with certain criteria, so they have
  4961. // a chance to be closer to the top.
  4962. this.sortEventSegs(segs);
  4963. for (i = 0; i < segs.length; i++) {
  4964. seg = segs[i];
  4965. // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  4966. for (j = 0; j < levels.length; j++) {
  4967. if (!isDaySegCollision(seg, levels[j])) {
  4968. break;
  4969. }
  4970. }
  4971. // `j` now holds the desired subrow index
  4972. seg.level = j;
  4973. // create new level array if needed and append segment
  4974. (levels[j] || (levels[j] = [])).push(seg);
  4975. }
  4976. // order segments left-to-right. very important if calendar is RTL
  4977. for (j = 0; j < levels.length; j++) {
  4978. levels[j].sort(compareDaySegCols);
  4979. }
  4980. return levels;
  4981. },
  4982. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  4983. groupSegRows: function(segs) {
  4984. var segRows = [];
  4985. var i;
  4986. for (i = 0; i < this.rowCnt; i++) {
  4987. segRows.push([]);
  4988. }
  4989. for (i = 0; i < segs.length; i++) {
  4990. segRows[segs[i].row].push(segs[i]);
  4991. }
  4992. return segRows;
  4993. }
  4994. });
  4995. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  4996. function isDaySegCollision(seg, otherSegs) {
  4997. var i, otherSeg;
  4998. for (i = 0; i < otherSegs.length; i++) {
  4999. otherSeg = otherSegs[i];
  5000. if (
  5001. otherSeg.leftCol <= seg.rightCol &&
  5002. otherSeg.rightCol >= seg.leftCol
  5003. ) {
  5004. return true;
  5005. }
  5006. }
  5007. return false;
  5008. }
  5009. // A cmp function for determining the leftmost event
  5010. function compareDaySegCols(a, b) {
  5011. return a.leftCol - b.leftCol;
  5012. }
  5013. ;;
  5014. /* Methods relate to limiting the number events for a given day on a DayGrid
  5015. ----------------------------------------------------------------------------------------------------------------------*/
  5016. // NOTE: all the segs being passed around in here are foreground segs
  5017. DayGrid.mixin({
  5018. segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  5019. popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  5020. removeSegPopover: function() {
  5021. if (this.segPopover) {
  5022. this.segPopover.hide(); // in handler, will call segPopover's removeElement
  5023. }
  5024. },
  5025. // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  5026. // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  5027. limitRows: function(levelLimit) {
  5028. var rowStructs = this.rowStructs || [];
  5029. var row; // row #
  5030. var rowLevelLimit;
  5031. for (row = 0; row < rowStructs.length; row++) {
  5032. this.unlimitRow(row);
  5033. if (!levelLimit) {
  5034. rowLevelLimit = false;
  5035. }
  5036. else if (typeof levelLimit === 'number') {
  5037. rowLevelLimit = levelLimit;
  5038. }
  5039. else {
  5040. rowLevelLimit = this.computeRowLevelLimit(row);
  5041. }
  5042. if (rowLevelLimit !== false) {
  5043. this.limitRow(row, rowLevelLimit);
  5044. }
  5045. }
  5046. },
  5047. // Computes the number of levels a row will accomodate without going outside its bounds.
  5048. // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  5049. // `row` is the row number.
  5050. computeRowLevelLimit: function(row) {
  5051. var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  5052. var rowHeight = rowEl.height(); // TODO: cache somehow?
  5053. var trEls = this.rowStructs[row].tbodyEl.children();
  5054. var i, trEl;
  5055. var trHeight;
  5056. function iterInnerHeights(i, childNode) {
  5057. trHeight = Math.max(trHeight, $(childNode).outerHeight());
  5058. }
  5059. // Reveal one level <tr> at a time and stop when we find one out of bounds
  5060. for (i = 0; i < trEls.length; i++) {
  5061. trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
  5062. // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
  5063. // so instead, find the tallest inner content element.
  5064. trHeight = 0;
  5065. trEl.find('> td > :first-child').each(iterInnerHeights);
  5066. if (trEl.position().top + trHeight > rowHeight) {
  5067. return i;
  5068. }
  5069. }
  5070. return false; // should not limit at all
  5071. },
  5072. // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  5073. // `row` is the row number.
  5074. // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  5075. limitRow: function(row, levelLimit) {
  5076. var _this = this;
  5077. var rowStruct = this.rowStructs[row];
  5078. var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  5079. var col = 0; // col #, left-to-right (not chronologically)
  5080. var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  5081. var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  5082. var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  5083. var i, seg;
  5084. var segsBelow; // array of segment objects below `seg` in the current `col`
  5085. var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  5086. var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  5087. var td, rowspan;
  5088. var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  5089. var j;
  5090. var moreTd, moreWrap, moreLink;
  5091. // Iterates through empty level cells and places "more" links inside if need be
  5092. function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  5093. while (col < endCol) {
  5094. segsBelow = _this.getCellSegs(row, col, levelLimit);
  5095. if (segsBelow.length) {
  5096. td = cellMatrix[levelLimit - 1][col];
  5097. moreLink = _this.renderMoreLink(row, col, segsBelow);
  5098. moreWrap = $('<div/>').append(moreLink);
  5099. td.append(moreWrap);
  5100. moreNodes.push(moreWrap[0]);
  5101. }
  5102. col++;
  5103. }
  5104. }
  5105. if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  5106. levelSegs = rowStruct.segLevels[levelLimit - 1];
  5107. cellMatrix = rowStruct.cellMatrix;
  5108. limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  5109. .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  5110. // iterate though segments in the last allowable level
  5111. for (i = 0; i < levelSegs.length; i++) {
  5112. seg = levelSegs[i];
  5113. emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  5114. // determine *all* segments below `seg` that occupy the same columns
  5115. colSegsBelow = [];
  5116. totalSegsBelow = 0;
  5117. while (col <= seg.rightCol) {
  5118. segsBelow = this.getCellSegs(row, col, levelLimit);
  5119. colSegsBelow.push(segsBelow);
  5120. totalSegsBelow += segsBelow.length;
  5121. col++;
  5122. }
  5123. if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  5124. td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  5125. rowspan = td.attr('rowspan') || 1;
  5126. segMoreNodes = [];
  5127. // make a replacement <td> for each column the segment occupies. will be one for each colspan
  5128. for (j = 0; j < colSegsBelow.length; j++) {
  5129. moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  5130. segsBelow = colSegsBelow[j];
  5131. moreLink = this.renderMoreLink(
  5132. row,
  5133. seg.leftCol + j,
  5134. [ seg ].concat(segsBelow) // count seg as hidden too
  5135. );
  5136. moreWrap = $('<div/>').append(moreLink);
  5137. moreTd.append(moreWrap);
  5138. segMoreNodes.push(moreTd[0]);
  5139. moreNodes.push(moreTd[0]);
  5140. }
  5141. td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  5142. limitedNodes.push(td[0]);
  5143. }
  5144. }
  5145. emptyCellsUntil(this.colCnt); // finish off the level
  5146. rowStruct.moreEls = $(moreNodes); // for easy undoing later
  5147. rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  5148. }
  5149. },
  5150. // Reveals all levels and removes all "more"-related elements for a grid's row.
  5151. // `row` is a row number.
  5152. unlimitRow: function(row) {
  5153. var rowStruct = this.rowStructs[row];
  5154. if (rowStruct.moreEls) {
  5155. rowStruct.moreEls.remove();
  5156. rowStruct.moreEls = null;
  5157. }
  5158. if (rowStruct.limitedEls) {
  5159. rowStruct.limitedEls.removeClass('fc-limited');
  5160. rowStruct.limitedEls = null;
  5161. }
  5162. },
  5163. // Renders an <a> element that represents hidden event element for a cell.
  5164. // Responsible for attaching click handler as well.
  5165. renderMoreLink: function(row, col, hiddenSegs) {
  5166. var _this = this;
  5167. var view = this.view;
  5168. return $('<a class="fc-more"/>')
  5169. .text(
  5170. this.getMoreLinkText(hiddenSegs.length)
  5171. )
  5172. .on('click', function(ev) {
  5173. var clickOption = view.opt('eventLimitClick');
  5174. var date = _this.getCellDate(row, col);
  5175. var moreEl = $(this);
  5176. var dayEl = _this.getCellEl(row, col);
  5177. var allSegs = _this.getCellSegs(row, col);
  5178. // rescope the segments to be within the cell's date
  5179. var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  5180. var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  5181. if (typeof clickOption === 'function') {
  5182. // the returned value can be an atomic option
  5183. clickOption = view.publiclyTrigger('eventLimitClick', null, {
  5184. date: date,
  5185. dayEl: dayEl,
  5186. moreEl: moreEl,
  5187. segs: reslicedAllSegs,
  5188. hiddenSegs: reslicedHiddenSegs
  5189. }, ev);
  5190. }
  5191. if (clickOption === 'popover') {
  5192. _this.showSegPopover(row, col, moreEl, reslicedAllSegs);
  5193. }
  5194. else if (typeof clickOption === 'string') { // a view name
  5195. view.calendar.zoomTo(date, clickOption);
  5196. }
  5197. });
  5198. },
  5199. // Reveals the popover that displays all events within a cell
  5200. showSegPopover: function(row, col, moreLink, segs) {
  5201. var _this = this;
  5202. var view = this.view;
  5203. var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  5204. var topEl; // the element we want to match the top coordinate of
  5205. var options;
  5206. if (this.rowCnt == 1) {
  5207. topEl = view.el; // will cause the popover to cover any sort of header
  5208. }
  5209. else {
  5210. topEl = this.rowEls.eq(row); // will align with top of row
  5211. }
  5212. options = {
  5213. className: 'fc-more-popover',
  5214. content: this.renderSegPopoverContent(row, col, segs),
  5215. parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
  5216. top: topEl.offset().top,
  5217. autoHide: true, // when the user clicks elsewhere, hide the popover
  5218. viewportConstrain: view.opt('popoverViewportConstrain'),
  5219. hide: function() {
  5220. // kill everything when the popover is hidden
  5221. // notify events to be removed
  5222. if (_this.popoverSegs) {
  5223. var seg;
  5224. for (var i = 0; i < _this.popoverSegs.length; ++i) {
  5225. seg = _this.popoverSegs[i];
  5226. view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
  5227. }
  5228. }
  5229. _this.segPopover.removeElement();
  5230. _this.segPopover = null;
  5231. _this.popoverSegs = null;
  5232. }
  5233. };
  5234. // Determine horizontal coordinate.
  5235. // We use the moreWrap instead of the <td> to avoid border confusion.
  5236. if (this.isRTL) {
  5237. options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  5238. }
  5239. else {
  5240. options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  5241. }
  5242. this.segPopover = new Popover(options);
  5243. this.segPopover.show();
  5244. // the popover doesn't live within the grid's container element, and thus won't get the event
  5245. // delegated-handlers for free. attach event-related handlers to the popover.
  5246. this.bindSegHandlersToEl(this.segPopover.el);
  5247. },
  5248. // Builds the inner DOM contents of the segment popover
  5249. renderSegPopoverContent: function(row, col, segs) {
  5250. var view = this.view;
  5251. var isTheme = view.opt('theme');
  5252. var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
  5253. var content = $(
  5254. '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  5255. '<span class="fc-close ' +
  5256. (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  5257. '"></span>' +
  5258. '<span class="fc-title">' +
  5259. htmlEscape(title) +
  5260. '</span>' +
  5261. '<div class="fc-clear"/>' +
  5262. '</div>' +
  5263. '<div class="fc-body ' + view.widgetContentClass + '">' +
  5264. '<div class="fc-event-container"></div>' +
  5265. '</div>'
  5266. );
  5267. var segContainer = content.find('.fc-event-container');
  5268. var i;
  5269. // render each seg's `el` and only return the visible segs
  5270. segs = this.renderFgSegEls(segs, true); // disableResizing=true
  5271. this.popoverSegs = segs;
  5272. for (i = 0; i < segs.length; i++) {
  5273. // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  5274. // grids that want to do drag-n-drop about which cell it came from
  5275. this.prepareHits();
  5276. segs[i].hit = this.getCellHit(row, col);
  5277. this.releaseHits();
  5278. segContainer.append(segs[i].el);
  5279. }
  5280. return content;
  5281. },
  5282. // Given the events within an array of segment objects, reslice them to be in a single day
  5283. resliceDaySegs: function(segs, dayDate) {
  5284. // build an array of the original events
  5285. var events = $.map(segs, function(seg) {
  5286. return seg.event;
  5287. });
  5288. var dayStart = dayDate.clone();
  5289. var dayEnd = dayStart.clone().add(1, 'days');
  5290. var dayRange = { start: dayStart, end: dayEnd };
  5291. // slice the events with a custom slicing function
  5292. segs = this.eventsToSegs(
  5293. events,
  5294. function(range) {
  5295. var seg = intersectRanges(range, dayRange); // undefind if no intersection
  5296. return seg ? [ seg ] : []; // must return an array of segments
  5297. }
  5298. );
  5299. // force an order because eventsToSegs doesn't guarantee one
  5300. this.sortEventSegs(segs);
  5301. return segs;
  5302. },
  5303. // Generates the text that should be inside a "more" link, given the number of events it represents
  5304. getMoreLinkText: function(num) {
  5305. var opt = this.view.opt('eventLimitText');
  5306. if (typeof opt === 'function') {
  5307. return opt(num);
  5308. }
  5309. else {
  5310. return '+' + num + ' ' + opt;
  5311. }
  5312. },
  5313. // Returns segments within a given cell.
  5314. // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  5315. getCellSegs: function(row, col, startLevel) {
  5316. var segMatrix = this.rowStructs[row].segMatrix;
  5317. var level = startLevel || 0;
  5318. var segs = [];
  5319. var seg;
  5320. while (level < segMatrix.length) {
  5321. seg = segMatrix[level][col];
  5322. if (seg) {
  5323. segs.push(seg);
  5324. }
  5325. level++;
  5326. }
  5327. return segs;
  5328. }
  5329. });
  5330. ;;
  5331. /* A component that renders one or more columns of vertical time slots
  5332. ----------------------------------------------------------------------------------------------------------------------*/
  5333. // We mixin DayTable, even though there is only a single row of days
  5334. var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
  5335. slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  5336. snapDuration: null, // granularity of time for dragging and selecting
  5337. snapsPerSlot: null,
  5338. minTime: null, // Duration object that denotes the first visible time of any given day
  5339. maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  5340. labelFormat: null, // formatting string for times running along vertical axis
  5341. labelInterval: null, // duration of how often a label should be displayed for a slot
  5342. colEls: null, // cells elements in the day-row background
  5343. slatContainerEl: null, // div that wraps all the slat rows
  5344. slatEls: null, // elements running horizontally across all columns
  5345. nowIndicatorEls: null,
  5346. colCoordCache: null,
  5347. slatCoordCache: null,
  5348. constructor: function() {
  5349. Grid.apply(this, arguments); // call the super-constructor
  5350. this.processOptions();
  5351. },
  5352. // Renders the time grid into `this.el`, which should already be assigned.
  5353. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  5354. renderDates: function() {
  5355. this.el.html(this.renderHtml());
  5356. this.colEls = this.el.find('.fc-day');
  5357. this.slatContainerEl = this.el.find('.fc-slats');
  5358. this.slatEls = this.slatContainerEl.find('tr');
  5359. this.colCoordCache = new CoordCache({
  5360. els: this.colEls,
  5361. isHorizontal: true
  5362. });
  5363. this.slatCoordCache = new CoordCache({
  5364. els: this.slatEls,
  5365. isVertical: true
  5366. });
  5367. this.renderContentSkeleton();
  5368. },
  5369. // Renders the basic HTML skeleton for the grid
  5370. renderHtml: function() {
  5371. return '' +
  5372. '<div class="fc-bg">' +
  5373. '<table>' +
  5374. this.renderBgTrHtml(0) + // row=0
  5375. '</table>' +
  5376. '</div>' +
  5377. '<div class="fc-slats">' +
  5378. '<table>' +
  5379. this.renderSlatRowHtml() +
  5380. '</table>' +
  5381. '</div>';
  5382. },
  5383. // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  5384. renderSlatRowHtml: function() {
  5385. var view = this.view;
  5386. var isRTL = this.isRTL;
  5387. var html = '';
  5388. var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  5389. var slotDate; // will be on the view's first day, but we only care about its time
  5390. var isLabeled;
  5391. var axisHtml;
  5392. // Calculate the time for each slot
  5393. while (slotTime < this.maxTime) {
  5394. slotDate = this.start.clone().time(slotTime);
  5395. isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
  5396. axisHtml =
  5397. '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  5398. (isLabeled ?
  5399. '<span>' + // for matchCellWidths
  5400. htmlEscape(slotDate.format(this.labelFormat)) +
  5401. '</span>' :
  5402. ''
  5403. ) +
  5404. '</td>';
  5405. html +=
  5406. '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
  5407. (isLabeled ? '' : ' class="fc-minor"') +
  5408. '>' +
  5409. (!isRTL ? axisHtml : '') +
  5410. '<td class="' + view.widgetContentClass + '"/>' +
  5411. (isRTL ? axisHtml : '') +
  5412. "</tr>";
  5413. slotTime.add(this.slotDuration);
  5414. }
  5415. return html;
  5416. },
  5417. /* Options
  5418. ------------------------------------------------------------------------------------------------------------------*/
  5419. // Parses various options into properties of this object
  5420. processOptions: function() {
  5421. var view = this.view;
  5422. var slotDuration = view.opt('slotDuration');
  5423. var snapDuration = view.opt('snapDuration');
  5424. var input;
  5425. slotDuration = moment.duration(slotDuration);
  5426. snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  5427. this.slotDuration = slotDuration;
  5428. this.snapDuration = snapDuration;
  5429. this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
  5430. this.minResizeDuration = snapDuration; // hack
  5431. this.minTime = moment.duration(view.opt('minTime'));
  5432. this.maxTime = moment.duration(view.opt('maxTime'));
  5433. // might be an array value (for TimelineView).
  5434. // if so, getting the most granular entry (the last one probably).
  5435. input = view.opt('slotLabelFormat');
  5436. if ($.isArray(input)) {
  5437. input = input[input.length - 1];
  5438. }
  5439. this.labelFormat =
  5440. input ||
  5441. view.opt('smallTimeFormat'); // the computed default
  5442. input = view.opt('slotLabelInterval');
  5443. this.labelInterval = input ?
  5444. moment.duration(input) :
  5445. this.computeLabelInterval(slotDuration);
  5446. },
  5447. // Computes an automatic value for slotLabelInterval
  5448. computeLabelInterval: function(slotDuration) {
  5449. var i;
  5450. var labelInterval;
  5451. var slotsPerLabel;
  5452. // find the smallest stock label interval that results in more than one slots-per-label
  5453. for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
  5454. labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
  5455. slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration);
  5456. if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
  5457. return labelInterval;
  5458. }
  5459. }
  5460. return moment.duration(slotDuration); // fall back. clone
  5461. },
  5462. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  5463. computeEventTimeFormat: function() {
  5464. return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
  5465. },
  5466. // Computes a default `displayEventEnd` value if one is not expliclty defined
  5467. computeDisplayEventEnd: function() {
  5468. return true;
  5469. },
  5470. /* Hit System
  5471. ------------------------------------------------------------------------------------------------------------------*/
  5472. prepareHits: function() {
  5473. this.colCoordCache.build();
  5474. this.slatCoordCache.build();
  5475. },
  5476. releaseHits: function() {
  5477. this.colCoordCache.clear();
  5478. // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
  5479. },
  5480. queryHit: function(leftOffset, topOffset) {
  5481. var snapsPerSlot = this.snapsPerSlot;
  5482. var colCoordCache = this.colCoordCache;
  5483. var slatCoordCache = this.slatCoordCache;
  5484. if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
  5485. var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
  5486. var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
  5487. if (colIndex != null && slatIndex != null) {
  5488. var slatTop = slatCoordCache.getTopOffset(slatIndex);
  5489. var slatHeight = slatCoordCache.getHeight(slatIndex);
  5490. var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
  5491. var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
  5492. var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
  5493. var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
  5494. var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
  5495. return {
  5496. col: colIndex,
  5497. snap: snapIndex,
  5498. component: this, // needed unfortunately :(
  5499. left: colCoordCache.getLeftOffset(colIndex),
  5500. right: colCoordCache.getRightOffset(colIndex),
  5501. top: snapTop,
  5502. bottom: snapBottom
  5503. };
  5504. }
  5505. }
  5506. },
  5507. getHitSpan: function(hit) {
  5508. var start = this.getCellDate(0, hit.col); // row=0
  5509. var time = this.computeSnapTime(hit.snap); // pass in the snap-index
  5510. var end;
  5511. start.time(time);
  5512. end = start.clone().add(this.snapDuration);
  5513. return { start: start, end: end };
  5514. },
  5515. getHitEl: function(hit) {
  5516. return this.colEls.eq(hit.col);
  5517. },
  5518. /* Dates
  5519. ------------------------------------------------------------------------------------------------------------------*/
  5520. rangeUpdated: function() {
  5521. this.updateDayTable();
  5522. },
  5523. // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
  5524. computeSnapTime: function(snapIndex) {
  5525. return moment.duration(this.minTime + this.snapDuration * snapIndex);
  5526. },
  5527. // Slices up the given span (unzoned start/end with other misc data) into an array of segments
  5528. spanToSegs: function(span) {
  5529. var segs = this.sliceRangeByTimes(span);
  5530. var i;
  5531. for (i = 0; i < segs.length; i++) {
  5532. if (this.isRTL) {
  5533. segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
  5534. }
  5535. else {
  5536. segs[i].col = segs[i].dayIndex;
  5537. }
  5538. }
  5539. return segs;
  5540. },
  5541. sliceRangeByTimes: function(range) {
  5542. var segs = [];
  5543. var seg;
  5544. var dayIndex;
  5545. var dayDate;
  5546. var dayRange;
  5547. for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
  5548. dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this?
  5549. dayRange = {
  5550. start: dayDate.clone().time(this.minTime),
  5551. end: dayDate.clone().time(this.maxTime)
  5552. };
  5553. seg = intersectRanges(range, dayRange); // both will be ambig timezone
  5554. if (seg) {
  5555. seg.dayIndex = dayIndex;
  5556. segs.push(seg);
  5557. }
  5558. }
  5559. return segs;
  5560. },
  5561. /* Coordinates
  5562. ------------------------------------------------------------------------------------------------------------------*/
  5563. updateSize: function(isResize) { // NOT a standard Grid method
  5564. this.slatCoordCache.build();
  5565. if (isResize) {
  5566. this.updateSegVerticals(
  5567. [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || [])
  5568. );
  5569. }
  5570. },
  5571. getTotalSlatHeight: function() {
  5572. return this.slatContainerEl.outerHeight();
  5573. },
  5574. // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  5575. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  5576. computeDateTop: function(date, startOfDayDate) {
  5577. return this.computeTimeTop(
  5578. moment.duration(
  5579. date - startOfDayDate.clone().stripTime()
  5580. )
  5581. );
  5582. },
  5583. // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  5584. computeTimeTop: function(time) {
  5585. var len = this.slatEls.length;
  5586. var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  5587. var slatIndex;
  5588. var slatRemainder;
  5589. // compute a floating-point number for how many slats should be progressed through.
  5590. // from 0 to number of slats (inclusive)
  5591. // constrained because minTime/maxTime might be customized.
  5592. slatCoverage = Math.max(0, slatCoverage);
  5593. slatCoverage = Math.min(len, slatCoverage);
  5594. // an integer index of the furthest whole slat
  5595. // from 0 to number slats (*exclusive*, so len-1)
  5596. slatIndex = Math.floor(slatCoverage);
  5597. slatIndex = Math.min(slatIndex, len - 1);
  5598. // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
  5599. // could be 1.0 if slatCoverage is covering *all* the slots
  5600. slatRemainder = slatCoverage - slatIndex;
  5601. return this.slatCoordCache.getTopPosition(slatIndex) +
  5602. this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
  5603. },
  5604. /* Event Drag Visualization
  5605. ------------------------------------------------------------------------------------------------------------------*/
  5606. // Renders a visual indication of an event being dragged over the specified date(s).
  5607. // A returned value of `true` signals that a mock "helper" event has been rendered.
  5608. renderDrag: function(eventLocation, seg) {
  5609. if (seg) { // if there is event information for this drag, render a helper event
  5610. // returns mock event elements
  5611. // signal that a helper has been rendered
  5612. return this.renderEventLocationHelper(eventLocation, seg);
  5613. }
  5614. else {
  5615. // otherwise, just render a highlight
  5616. this.renderHighlight(this.eventToSpan(eventLocation));
  5617. }
  5618. },
  5619. // Unrenders any visual indication of an event being dragged
  5620. unrenderDrag: function() {
  5621. this.unrenderHelper();
  5622. this.unrenderHighlight();
  5623. },
  5624. /* Event Resize Visualization
  5625. ------------------------------------------------------------------------------------------------------------------*/
  5626. // Renders a visual indication of an event being resized
  5627. renderEventResize: function(eventLocation, seg) {
  5628. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  5629. },
  5630. // Unrenders any visual indication of an event being resized
  5631. unrenderEventResize: function() {
  5632. this.unrenderHelper();
  5633. },
  5634. /* Event Helper
  5635. ------------------------------------------------------------------------------------------------------------------*/
  5636. // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  5637. renderHelper: function(event, sourceSeg) {
  5638. return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
  5639. },
  5640. // Unrenders any mock helper event
  5641. unrenderHelper: function() {
  5642. this.unrenderHelperSegs();
  5643. },
  5644. /* Business Hours
  5645. ------------------------------------------------------------------------------------------------------------------*/
  5646. renderBusinessHours: function() {
  5647. this.renderBusinessSegs(
  5648. this.buildBusinessHourSegs()
  5649. );
  5650. },
  5651. unrenderBusinessHours: function() {
  5652. this.unrenderBusinessSegs();
  5653. },
  5654. /* Now Indicator
  5655. ------------------------------------------------------------------------------------------------------------------*/
  5656. getNowIndicatorUnit: function() {
  5657. return 'minute'; // will refresh on the minute
  5658. },
  5659. renderNowIndicator: function(date) {
  5660. // seg system might be overkill, but it handles scenario where line needs to be rendered
  5661. // more than once because of columns with the same date (resources columns for example)
  5662. var segs = this.spanToSegs({ start: date, end: date });
  5663. var top = this.computeDateTop(date, date);
  5664. var nodes = [];
  5665. var i;
  5666. // render lines within the columns
  5667. for (i = 0; i < segs.length; i++) {
  5668. nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
  5669. .css('top', top)
  5670. .appendTo(this.colContainerEls.eq(segs[i].col))[0]);
  5671. }
  5672. // render an arrow over the axis
  5673. if (segs.length > 0) { // is the current time in view?
  5674. nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
  5675. .css('top', top)
  5676. .appendTo(this.el.find('.fc-content-skeleton'))[0]);
  5677. }
  5678. this.nowIndicatorEls = $(nodes);
  5679. },
  5680. unrenderNowIndicator: function() {
  5681. if (this.nowIndicatorEls) {
  5682. this.nowIndicatorEls.remove();
  5683. this.nowIndicatorEls = null;
  5684. }
  5685. },
  5686. /* Selection
  5687. ------------------------------------------------------------------------------------------------------------------*/
  5688. // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  5689. renderSelection: function(span) {
  5690. if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  5691. // normally acceps an eventLocation, span has a start/end, which is good enough
  5692. this.renderEventLocationHelper(span);
  5693. }
  5694. else {
  5695. this.renderHighlight(span);
  5696. }
  5697. },
  5698. // Unrenders any visual indication of a selection
  5699. unrenderSelection: function() {
  5700. this.unrenderHelper();
  5701. this.unrenderHighlight();
  5702. },
  5703. /* Highlight
  5704. ------------------------------------------------------------------------------------------------------------------*/
  5705. renderHighlight: function(span) {
  5706. this.renderHighlightSegs(this.spanToSegs(span));
  5707. },
  5708. unrenderHighlight: function() {
  5709. this.unrenderHighlightSegs();
  5710. }
  5711. });
  5712. ;;
  5713. /* Methods for rendering SEGMENTS, pieces of content that live on the view
  5714. ( this file is no longer just for events )
  5715. ----------------------------------------------------------------------------------------------------------------------*/
  5716. TimeGrid.mixin({
  5717. colContainerEls: null, // containers for each column
  5718. // inner-containers for each column where different types of segs live
  5719. fgContainerEls: null,
  5720. bgContainerEls: null,
  5721. helperContainerEls: null,
  5722. highlightContainerEls: null,
  5723. businessContainerEls: null,
  5724. // arrays of different types of displayed segments
  5725. fgSegs: null,
  5726. bgSegs: null,
  5727. helperSegs: null,
  5728. highlightSegs: null,
  5729. businessSegs: null,
  5730. // Renders the DOM that the view's content will live in
  5731. renderContentSkeleton: function() {
  5732. var cellHtml = '';
  5733. var i;
  5734. var skeletonEl;
  5735. for (i = 0; i < this.colCnt; i++) {
  5736. cellHtml +=
  5737. '<td>' +
  5738. '<div class="fc-content-col">' +
  5739. '<div class="fc-event-container fc-helper-container"></div>' +
  5740. '<div class="fc-event-container"></div>' +
  5741. '<div class="fc-highlight-container"></div>' +
  5742. '<div class="fc-bgevent-container"></div>' +
  5743. '<div class="fc-business-container"></div>' +
  5744. '</div>' +
  5745. '</td>';
  5746. }
  5747. skeletonEl = $(
  5748. '<div class="fc-content-skeleton">' +
  5749. '<table>' +
  5750. '<tr>' + cellHtml + '</tr>' +
  5751. '</table>' +
  5752. '</div>'
  5753. );
  5754. this.colContainerEls = skeletonEl.find('.fc-content-col');
  5755. this.helperContainerEls = skeletonEl.find('.fc-helper-container');
  5756. this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
  5757. this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
  5758. this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
  5759. this.businessContainerEls = skeletonEl.find('.fc-business-container');
  5760. this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
  5761. this.el.append(skeletonEl);
  5762. },
  5763. /* Foreground Events
  5764. ------------------------------------------------------------------------------------------------------------------*/
  5765. renderFgSegs: function(segs) {
  5766. segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls);
  5767. this.fgSegs = segs;
  5768. return segs; // needed for Grid::renderEvents
  5769. },
  5770. unrenderFgSegs: function() {
  5771. this.unrenderNamedSegs('fgSegs');
  5772. },
  5773. /* Foreground Helper Events
  5774. ------------------------------------------------------------------------------------------------------------------*/
  5775. renderHelperSegs: function(segs, sourceSeg) {
  5776. var helperEls = [];
  5777. var i, seg;
  5778. var sourceEl;
  5779. segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls);
  5780. // Try to make the segment that is in the same row as sourceSeg look the same
  5781. for (i = 0; i < segs.length; i++) {
  5782. seg = segs[i];
  5783. if (sourceSeg && sourceSeg.col === seg.col) {
  5784. sourceEl = sourceSeg.el;
  5785. seg.el.css({
  5786. left: sourceEl.css('left'),
  5787. right: sourceEl.css('right'),
  5788. 'margin-left': sourceEl.css('margin-left'),
  5789. 'margin-right': sourceEl.css('margin-right')
  5790. });
  5791. }
  5792. helperEls.push(seg.el[0]);
  5793. }
  5794. this.helperSegs = segs;
  5795. return $(helperEls); // must return rendered helpers
  5796. },
  5797. unrenderHelperSegs: function() {
  5798. this.unrenderNamedSegs('helperSegs');
  5799. },
  5800. /* Background Events
  5801. ------------------------------------------------------------------------------------------------------------------*/
  5802. renderBgSegs: function(segs) {
  5803. segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system
  5804. this.updateSegVerticals(segs);
  5805. this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
  5806. this.bgSegs = segs;
  5807. return segs; // needed for Grid::renderEvents
  5808. },
  5809. unrenderBgSegs: function() {
  5810. this.unrenderNamedSegs('bgSegs');
  5811. },
  5812. /* Highlight
  5813. ------------------------------------------------------------------------------------------------------------------*/
  5814. renderHighlightSegs: function(segs) {
  5815. segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system
  5816. this.updateSegVerticals(segs);
  5817. this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls);
  5818. this.highlightSegs = segs;
  5819. },
  5820. unrenderHighlightSegs: function() {
  5821. this.unrenderNamedSegs('highlightSegs');
  5822. },
  5823. /* Business Hours
  5824. ------------------------------------------------------------------------------------------------------------------*/
  5825. renderBusinessSegs: function(segs) {
  5826. segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system
  5827. this.updateSegVerticals(segs);
  5828. this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls);
  5829. this.businessSegs = segs;
  5830. },
  5831. unrenderBusinessSegs: function() {
  5832. this.unrenderNamedSegs('businessSegs');
  5833. },
  5834. /* Seg Rendering Utils
  5835. ------------------------------------------------------------------------------------------------------------------*/
  5836. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  5837. groupSegsByCol: function(segs) {
  5838. var segsByCol = [];
  5839. var i;
  5840. for (i = 0; i < this.colCnt; i++) {
  5841. segsByCol.push([]);
  5842. }
  5843. for (i = 0; i < segs.length; i++) {
  5844. segsByCol[segs[i].col].push(segs[i]);
  5845. }
  5846. return segsByCol;
  5847. },
  5848. // Given segments grouped by column, insert the segments' elements into a parallel array of container
  5849. // elements, each living within a column.
  5850. attachSegsByCol: function(segsByCol, containerEls) {
  5851. var col;
  5852. var segs;
  5853. var i;
  5854. for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
  5855. segs = segsByCol[col];
  5856. for (i = 0; i < segs.length; i++) {
  5857. containerEls.eq(col).append(segs[i].el);
  5858. }
  5859. }
  5860. },
  5861. // Given the name of a property of `this` object, assumed to be an array of segments,
  5862. // loops through each segment and removes from DOM. Will null-out the property afterwards.
  5863. unrenderNamedSegs: function(propName) {
  5864. var segs = this[propName];
  5865. var i;
  5866. if (segs) {
  5867. for (i = 0; i < segs.length; i++) {
  5868. segs[i].el.remove();
  5869. }
  5870. this[propName] = null;
  5871. }
  5872. },
  5873. /* Foreground Event Rendering Utils
  5874. ------------------------------------------------------------------------------------------------------------------*/
  5875. // Given an array of foreground segments, render a DOM element for each, computes position,
  5876. // and attaches to the column inner-container elements.
  5877. renderFgSegsIntoContainers: function(segs, containerEls) {
  5878. var segsByCol;
  5879. var col;
  5880. segs = this.renderFgSegEls(segs); // will call fgSegHtml
  5881. segsByCol = this.groupSegsByCol(segs);
  5882. for (col = 0; col < this.colCnt; col++) {
  5883. this.updateFgSegCoords(segsByCol[col]);
  5884. }
  5885. this.attachSegsByCol(segsByCol, containerEls);
  5886. return segs;
  5887. },
  5888. // Renders the HTML for a single event segment's default rendering
  5889. fgSegHtml: function(seg, disableResizing) {
  5890. var view = this.view;
  5891. var event = seg.event;
  5892. var isDraggable = view.isEventDraggable(event);
  5893. var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
  5894. var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
  5895. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  5896. var skinCss = cssToStr(this.getSegSkinCss(seg));
  5897. var timeText;
  5898. var fullTimeText; // more verbose time text. for the print stylesheet
  5899. var startTimeText; // just the start time text
  5900. classes.unshift('fc-time-grid-event', 'fc-v-event');
  5901. if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  5902. // Don't display time text on segments that run entirely through a day.
  5903. // That would appear as midnight-midnight and would look dumb.
  5904. // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  5905. if (seg.isStart || seg.isEnd) {
  5906. timeText = this.getEventTimeText(seg);
  5907. fullTimeText = this.getEventTimeText(seg, 'LT');
  5908. startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
  5909. }
  5910. } else {
  5911. // Display the normal time text for the *event's* times
  5912. timeText = this.getEventTimeText(event);
  5913. fullTimeText = this.getEventTimeText(event, 'LT');
  5914. startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
  5915. }
  5916. return '<a class="' + classes.join(' ') + '"' +
  5917. (event.url ?
  5918. ' href="' + htmlEscape(event.url) + '"' :
  5919. ''
  5920. ) +
  5921. (skinCss ?
  5922. ' style="' + skinCss + '"' :
  5923. ''
  5924. ) +
  5925. '>' +
  5926. '<div class="fc-content">' +
  5927. (timeText ?
  5928. '<div class="fc-time"' +
  5929. ' data-start="' + htmlEscape(startTimeText) + '"' +
  5930. ' data-full="' + htmlEscape(fullTimeText) + '"' +
  5931. '>' +
  5932. '<span>' + htmlEscape(timeText) + '</span>' +
  5933. '</div>' :
  5934. ''
  5935. ) +
  5936. (event.title ?
  5937. '<div class="fc-title">' +
  5938. htmlEscape(event.title) +
  5939. '</div>' :
  5940. ''
  5941. ) +
  5942. '</div>' +
  5943. '<div class="fc-bg"/>' +
  5944. /* TODO: write CSS for this
  5945. (isResizableFromStart ?
  5946. '<div class="fc-resizer fc-start-resizer" />' :
  5947. ''
  5948. ) +
  5949. */
  5950. (isResizableFromEnd ?
  5951. '<div class="fc-resizer fc-end-resizer" />' :
  5952. ''
  5953. ) +
  5954. '</a>';
  5955. },
  5956. /* Seg Position Utils
  5957. ------------------------------------------------------------------------------------------------------------------*/
  5958. // Refreshes the CSS top/bottom coordinates for each segment element.
  5959. // Works when called after initial render, after a window resize/zoom for example.
  5960. updateSegVerticals: function(segs) {
  5961. this.computeSegVerticals(segs);
  5962. this.assignSegVerticals(segs);
  5963. },
  5964. // For each segment in an array, computes and assigns its top and bottom properties
  5965. computeSegVerticals: function(segs) {
  5966. var i, seg;
  5967. for (i = 0; i < segs.length; i++) {
  5968. seg = segs[i];
  5969. seg.top = this.computeDateTop(seg.start, seg.start);
  5970. seg.bottom = this.computeDateTop(seg.end, seg.start);
  5971. }
  5972. },
  5973. // Given segments that already have their top/bottom properties computed, applies those values to
  5974. // the segments' elements.
  5975. assignSegVerticals: function(segs) {
  5976. var i, seg;
  5977. for (i = 0; i < segs.length; i++) {
  5978. seg = segs[i];
  5979. seg.el.css(this.generateSegVerticalCss(seg));
  5980. }
  5981. },
  5982. // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  5983. generateSegVerticalCss: function(seg) {
  5984. return {
  5985. top: seg.top,
  5986. bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  5987. };
  5988. },
  5989. /* Foreground Event Positioning Utils
  5990. ------------------------------------------------------------------------------------------------------------------*/
  5991. // Given segments that are assumed to all live in the *same column*,
  5992. // compute their verical/horizontal coordinates and assign to their elements.
  5993. updateFgSegCoords: function(segs) {
  5994. this.computeSegVerticals(segs); // horizontals relies on this
  5995. this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
  5996. this.assignSegVerticals(segs);
  5997. this.assignFgSegHorizontals(segs);
  5998. },
  5999. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  6000. // NOTE: Also reorders the given array by date!
  6001. computeFgSegHorizontals: function(segs) {
  6002. var levels;
  6003. var level0;
  6004. var i;
  6005. this.sortEventSegs(segs); // order by certain criteria
  6006. levels = buildSlotSegLevels(segs);
  6007. computeForwardSlotSegs(levels);
  6008. if ((level0 = levels[0])) {
  6009. for (i = 0; i < level0.length; i++) {
  6010. computeSlotSegPressures(level0[i]);
  6011. }
  6012. for (i = 0; i < level0.length; i++) {
  6013. this.computeFgSegForwardBack(level0[i], 0, 0);
  6014. }
  6015. }
  6016. },
  6017. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  6018. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  6019. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  6020. //
  6021. // The segment might be part of a "series", which means consecutive segments with the same pressure
  6022. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  6023. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  6024. // coordinate of the first segment in the series.
  6025. computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
  6026. var forwardSegs = seg.forwardSegs;
  6027. var i;
  6028. if (seg.forwardCoord === undefined) { // not already computed
  6029. if (!forwardSegs.length) {
  6030. // if there are no forward segments, this segment should butt up against the edge
  6031. seg.forwardCoord = 1;
  6032. }
  6033. else {
  6034. // sort highest pressure first
  6035. this.sortForwardSegs(forwardSegs);
  6036. // this segment's forwardCoord will be calculated from the backwardCoord of the
  6037. // highest-pressure forward segment.
  6038. this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  6039. seg.forwardCoord = forwardSegs[0].backwardCoord;
  6040. }
  6041. // calculate the backwardCoord from the forwardCoord. consider the series
  6042. seg.backwardCoord = seg.forwardCoord -
  6043. (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  6044. (seriesBackwardPressure + 1); // # of segments in the series
  6045. // use this segment's coordinates to computed the coordinates of the less-pressurized
  6046. // forward segments
  6047. for (i=0; i<forwardSegs.length; i++) {
  6048. this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
  6049. }
  6050. }
  6051. },
  6052. sortForwardSegs: function(forwardSegs) {
  6053. forwardSegs.sort(proxy(this, 'compareForwardSegs'));
  6054. },
  6055. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  6056. compareForwardSegs: function(seg1, seg2) {
  6057. // put higher-pressure first
  6058. return seg2.forwardPressure - seg1.forwardPressure ||
  6059. // put segments that are closer to initial edge first (and favor ones with no coords yet)
  6060. (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  6061. // do normal sorting...
  6062. this.compareEventSegs(seg1, seg2);
  6063. },
  6064. // Given foreground event segments that have already had their position coordinates computed,
  6065. // assigns position-related CSS values to their elements.
  6066. assignFgSegHorizontals: function(segs) {
  6067. var i, seg;
  6068. for (i = 0; i < segs.length; i++) {
  6069. seg = segs[i];
  6070. seg.el.css(this.generateFgSegHorizontalCss(seg));
  6071. // if the height is short, add a className for alternate styling
  6072. if (seg.bottom - seg.top < 30) {
  6073. seg.el.addClass('fc-short');
  6074. }
  6075. }
  6076. },
  6077. // Generates an object with CSS properties/values that should be applied to an event segment element.
  6078. // Contains important positioning-related properties that should be applied to any event element, customized or not.
  6079. generateFgSegHorizontalCss: function(seg) {
  6080. var shouldOverlap = this.view.opt('slotEventOverlap');
  6081. var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  6082. var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  6083. var props = this.generateSegVerticalCss(seg); // get top/bottom first
  6084. var left; // amount of space from left edge, a fraction of the total width
  6085. var right; // amount of space from right edge, a fraction of the total width
  6086. if (shouldOverlap) {
  6087. // double the width, but don't go beyond the maximum forward coordinate (1.0)
  6088. forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  6089. }
  6090. if (this.isRTL) {
  6091. left = 1 - forwardCoord;
  6092. right = backwardCoord;
  6093. }
  6094. else {
  6095. left = backwardCoord;
  6096. right = 1 - forwardCoord;
  6097. }
  6098. props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  6099. props.left = left * 100 + '%';
  6100. props.right = right * 100 + '%';
  6101. if (shouldOverlap && seg.forwardPressure) {
  6102. // add padding to the edge so that forward stacked events don't cover the resizer's icon
  6103. props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  6104. }
  6105. return props;
  6106. }
  6107. });
  6108. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  6109. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  6110. function buildSlotSegLevels(segs) {
  6111. var levels = [];
  6112. var i, seg;
  6113. var j;
  6114. for (i=0; i<segs.length; i++) {
  6115. seg = segs[i];
  6116. // go through all the levels and stop on the first level where there are no collisions
  6117. for (j=0; j<levels.length; j++) {
  6118. if (!computeSlotSegCollisions(seg, levels[j]).length) {
  6119. break;
  6120. }
  6121. }
  6122. seg.level = j;
  6123. (levels[j] || (levels[j] = [])).push(seg);
  6124. }
  6125. return levels;
  6126. }
  6127. // For every segment, figure out the other segments that are in subsequent
  6128. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  6129. function computeForwardSlotSegs(levels) {
  6130. var i, level;
  6131. var j, seg;
  6132. var k;
  6133. for (i=0; i<levels.length; i++) {
  6134. level = levels[i];
  6135. for (j=0; j<level.length; j++) {
  6136. seg = level[j];
  6137. seg.forwardSegs = [];
  6138. for (k=i+1; k<levels.length; k++) {
  6139. computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  6140. }
  6141. }
  6142. }
  6143. }
  6144. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  6145. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  6146. function computeSlotSegPressures(seg) {
  6147. var forwardSegs = seg.forwardSegs;
  6148. var forwardPressure = 0;
  6149. var i, forwardSeg;
  6150. if (seg.forwardPressure === undefined) { // not already computed
  6151. for (i=0; i<forwardSegs.length; i++) {
  6152. forwardSeg = forwardSegs[i];
  6153. // figure out the child's maximum forward path
  6154. computeSlotSegPressures(forwardSeg);
  6155. // either use the existing maximum, or use the child's forward pressure
  6156. // plus one (for the forwardSeg itself)
  6157. forwardPressure = Math.max(
  6158. forwardPressure,
  6159. 1 + forwardSeg.forwardPressure
  6160. );
  6161. }
  6162. seg.forwardPressure = forwardPressure;
  6163. }
  6164. }
  6165. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  6166. // Append into an optionally-supplied `results` array and return.
  6167. function computeSlotSegCollisions(seg, otherSegs, results) {
  6168. results = results || [];
  6169. for (var i=0; i<otherSegs.length; i++) {
  6170. if (isSlotSegCollision(seg, otherSegs[i])) {
  6171. results.push(otherSegs[i]);
  6172. }
  6173. }
  6174. return results;
  6175. }
  6176. // Do these segments occupy the same vertical space?
  6177. function isSlotSegCollision(seg1, seg2) {
  6178. return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  6179. }
  6180. ;;
  6181. /* An abstract class from which other views inherit from
  6182. ----------------------------------------------------------------------------------------------------------------------*/
  6183. var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
  6184. type: null, // subclass' view name (string)
  6185. name: null, // deprecated. use `type` instead
  6186. title: null, // the text that will be displayed in the header's title
  6187. calendar: null, // owner Calendar object
  6188. options: null, // hash containing all options. already merged with view-specific-options
  6189. el: null, // the view's containing element. set by Calendar
  6190. isDateSet: false,
  6191. isDateRendered: false,
  6192. dateRenderQueue: null,
  6193. isEventsBound: false,
  6194. isEventsSet: false,
  6195. isEventsRendered: false,
  6196. eventRenderQueue: null,
  6197. // range the view is actually displaying (moments)
  6198. start: null,
  6199. end: null, // exclusive
  6200. // range the view is formally responsible for (moments)
  6201. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  6202. intervalStart: null,
  6203. intervalEnd: null, // exclusive
  6204. intervalDuration: null,
  6205. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  6206. isRTL: false,
  6207. isSelected: false, // boolean whether a range of time is user-selected or not
  6208. selectedEvent: null,
  6209. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  6210. // classNames styled by jqui themes
  6211. widgetHeaderClass: null,
  6212. widgetContentClass: null,
  6213. highlightStateClass: null,
  6214. // for date utils, computed from options
  6215. nextDayThreshold: null,
  6216. isHiddenDayHash: null,
  6217. // now indicator
  6218. isNowIndicatorRendered: null,
  6219. initialNowDate: null, // result first getNow call
  6220. initialNowQueriedMs: null, // ms time the getNow was called
  6221. nowIndicatorTimeoutID: null, // for refresh timing of now indicator
  6222. nowIndicatorIntervalID: null, // "
  6223. constructor: function(calendar, type, options, intervalDuration) {
  6224. this.calendar = calendar;
  6225. this.type = this.name = type; // .name is deprecated
  6226. this.options = options;
  6227. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  6228. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  6229. this.initThemingProps();
  6230. this.initHiddenDays();
  6231. this.isRTL = this.opt('isRTL');
  6232. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  6233. this.dateRenderQueue = new TaskQueue();
  6234. this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
  6235. this.initialize();
  6236. },
  6237. // A good place for subclasses to initialize member variables
  6238. initialize: function() {
  6239. // subclasses can implement
  6240. },
  6241. // Retrieves an option with the given name
  6242. opt: function(name) {
  6243. return this.options[name];
  6244. },
  6245. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  6246. publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  6247. var calendar = this.calendar;
  6248. return calendar.publiclyTrigger.apply(
  6249. calendar,
  6250. [name, thisObj || this].concat(
  6251. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  6252. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  6253. )
  6254. );
  6255. },
  6256. // Returns a proxy of the given promise that will be rejected if the given event fires
  6257. // before the promise resolves.
  6258. rejectOn: function(eventName, promise) {
  6259. var _this = this;
  6260. return new Promise(function(resolve, reject) {
  6261. _this.one(eventName, reject);
  6262. function cleanup() {
  6263. _this.off(eventName, reject);
  6264. }
  6265. promise.then(function(res) { // success
  6266. cleanup();
  6267. resolve(res);
  6268. }, function() { // failure
  6269. cleanup();
  6270. reject();
  6271. });
  6272. });
  6273. },
  6274. /* Date Computation
  6275. ------------------------------------------------------------------------------------------------------------------*/
  6276. // Updates all internal dates for displaying the given unzoned range.
  6277. setRange: function(range) {
  6278. $.extend(this, range); // assigns every property to this object's member variables
  6279. this.updateTitle();
  6280. },
  6281. // Given a single current unzoned date, produce information about what range to display.
  6282. // Subclasses can override. Must return all properties.
  6283. computeRange: function(date) {
  6284. var intervalUnit = computeIntervalUnit(this.intervalDuration);
  6285. var intervalStart = date.clone().startOf(intervalUnit);
  6286. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  6287. var start, end;
  6288. // normalize the range's time-ambiguity
  6289. if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  6290. intervalStart.stripTime();
  6291. intervalEnd.stripTime();
  6292. }
  6293. else { // needs to have a time?
  6294. if (!intervalStart.hasTime()) {
  6295. intervalStart = this.calendar.time(0); // give 00:00 time
  6296. }
  6297. if (!intervalEnd.hasTime()) {
  6298. intervalEnd = this.calendar.time(0); // give 00:00 time
  6299. }
  6300. }
  6301. start = intervalStart.clone();
  6302. start = this.skipHiddenDays(start);
  6303. end = intervalEnd.clone();
  6304. end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  6305. return {
  6306. intervalUnit: intervalUnit,
  6307. intervalStart: intervalStart,
  6308. intervalEnd: intervalEnd,
  6309. start: start,
  6310. end: end
  6311. };
  6312. },
  6313. // Computes the new date when the user hits the prev button, given the current date
  6314. computePrevDate: function(date) {
  6315. return this.massageCurrentDate(
  6316. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  6317. );
  6318. },
  6319. // Computes the new date when the user hits the next button, given the current date
  6320. computeNextDate: function(date) {
  6321. return this.massageCurrentDate(
  6322. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  6323. );
  6324. },
  6325. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  6326. // visible. `direction` is optional and indicates which direction the current date was being
  6327. // incremented or decremented (1 or -1).
  6328. massageCurrentDate: function(date, direction) {
  6329. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  6330. if (this.isHiddenDay(date)) {
  6331. date = this.skipHiddenDays(date, direction);
  6332. date.startOf('day');
  6333. }
  6334. }
  6335. return date;
  6336. },
  6337. /* Title and Date Formatting
  6338. ------------------------------------------------------------------------------------------------------------------*/
  6339. // Sets the view's title property to the most updated computed value
  6340. updateTitle: function() {
  6341. this.title = this.computeTitle();
  6342. this.calendar.setToolbarsTitle(this.title);
  6343. },
  6344. // Computes what the title at the top of the calendar should be for this view
  6345. computeTitle: function() {
  6346. return this.formatRange(
  6347. {
  6348. // in case intervalStart/End has a time, make sure timezone is correct
  6349. start: this.calendar.applyTimezone(this.intervalStart),
  6350. end: this.calendar.applyTimezone(this.intervalEnd)
  6351. },
  6352. this.opt('titleFormat') || this.computeTitleFormat(),
  6353. this.opt('titleRangeSeparator')
  6354. );
  6355. },
  6356. // Generates the format string that should be used to generate the title for the current date range.
  6357. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  6358. computeTitleFormat: function() {
  6359. if (this.intervalUnit == 'year') {
  6360. return 'YYYY';
  6361. }
  6362. else if (this.intervalUnit == 'month') {
  6363. return this.opt('monthYearFormat'); // like "September 2014"
  6364. }
  6365. else if (this.intervalDuration.as('days') > 1) {
  6366. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  6367. }
  6368. else {
  6369. return 'LL'; // one day. longer, like "September 9 2014"
  6370. }
  6371. },
  6372. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  6373. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  6374. // The timezones of the dates within `range` will be respected.
  6375. formatRange: function(range, formatStr, separator) {
  6376. var end = range.end;
  6377. if (!end.hasTime()) { // all-day?
  6378. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  6379. }
  6380. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  6381. },
  6382. getAllDayHtml: function() {
  6383. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
  6384. },
  6385. /* Navigation
  6386. ------------------------------------------------------------------------------------------------------------------*/
  6387. // Generates HTML for an anchor to another view into the calendar.
  6388. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  6389. // `gotoOptions` can either be a moment input, or an object with the form:
  6390. // { date, type, forceOff }
  6391. // `type` is a view-type like "day" or "week". default value is "day".
  6392. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  6393. buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
  6394. var date, type, forceOff;
  6395. var finalOptions;
  6396. if ($.isPlainObject(gotoOptions)) {
  6397. date = gotoOptions.date;
  6398. type = gotoOptions.type;
  6399. forceOff = gotoOptions.forceOff;
  6400. }
  6401. else {
  6402. date = gotoOptions; // a single moment input
  6403. }
  6404. date = FC.moment(date); // if a string, parse it
  6405. finalOptions = { // for serialization into the link
  6406. date: date.format('YYYY-MM-DD'),
  6407. type: type || 'day'
  6408. };
  6409. if (typeof attrs === 'string') {
  6410. innerHtml = attrs;
  6411. attrs = null;
  6412. }
  6413. attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
  6414. innerHtml = innerHtml || '';
  6415. if (!forceOff && this.opt('navLinks')) {
  6416. return '<a' + attrs +
  6417. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  6418. innerHtml +
  6419. '</a>';
  6420. }
  6421. else {
  6422. return '<span' + attrs + '>' +
  6423. innerHtml +
  6424. '</span>';
  6425. }
  6426. },
  6427. // Rendering Non-date-related Content
  6428. // -----------------------------------------------------------------------------------------------------------------
  6429. // Sets the container element that the view should render inside of, does global DOM-related initializations,
  6430. // and renders all the non-date-related content inside.
  6431. setElement: function(el) {
  6432. this.el = el;
  6433. this.bindGlobalHandlers();
  6434. this.renderSkeleton();
  6435. },
  6436. // Removes the view's container element from the DOM, clearing any content beforehand.
  6437. // Undoes any other DOM-related attachments.
  6438. removeElement: function() {
  6439. this.unsetDate();
  6440. this.unrenderSkeleton();
  6441. this.unbindGlobalHandlers();
  6442. this.el.remove();
  6443. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  6444. // We don't null-out the View's other jQuery element references upon destroy,
  6445. // so we shouldn't kill this.el either.
  6446. },
  6447. // Renders the basic structure of the view before any content is rendered
  6448. renderSkeleton: function() {
  6449. // subclasses should implement
  6450. },
  6451. // Unrenders the basic structure of the view
  6452. unrenderSkeleton: function() {
  6453. // subclasses should implement
  6454. },
  6455. // Date Setting/Unsetting
  6456. // -----------------------------------------------------------------------------------------------------------------
  6457. setDate: function(date) {
  6458. var isReset = this.isDateSet;
  6459. this.isDateSet = true;
  6460. this.handleDate(date, isReset);
  6461. this.trigger(isReset ? 'dateReset' : 'dateSet', date);
  6462. },
  6463. unsetDate: function() {
  6464. if (this.isDateSet) {
  6465. this.isDateSet = false;
  6466. this.handleDateUnset();
  6467. this.trigger('dateUnset');
  6468. }
  6469. },
  6470. // Date Handling
  6471. // -----------------------------------------------------------------------------------------------------------------
  6472. handleDate: function(date, isReset) {
  6473. var _this = this;
  6474. this.unbindEvents(); // will do nothing if not already bound
  6475. this.requestDateRender(date).then(function() {
  6476. // wish we could start earlier, but setRange/computeRange needs to execute first
  6477. _this.bindEvents(); // will request events
  6478. });
  6479. },
  6480. handleDateUnset: function() {
  6481. this.unbindEvents();
  6482. this.requestDateUnrender();
  6483. },
  6484. // Date Render Queuing
  6485. // -----------------------------------------------------------------------------------------------------------------
  6486. // if date not specified, uses current
  6487. requestDateRender: function(date) {
  6488. var _this = this;
  6489. return this.dateRenderQueue.add(function() {
  6490. return _this.executeDateRender(date);
  6491. });
  6492. },
  6493. requestDateUnrender: function() {
  6494. var _this = this;
  6495. return this.dateRenderQueue.add(function() {
  6496. return _this.executeDateUnrender();
  6497. });
  6498. },
  6499. // Date High-level Rendering
  6500. // -----------------------------------------------------------------------------------------------------------------
  6501. // if date not specified, uses current
  6502. executeDateRender: function(date) {
  6503. var _this = this;
  6504. // if rendering a new date, reset scroll to initial state (scrollTime)
  6505. if (date) {
  6506. this.captureInitialScroll();
  6507. }
  6508. else {
  6509. this.captureScroll(); // a rerender of the current date
  6510. }
  6511. this.freezeHeight();
  6512. return this.executeDateUnrender().then(function() {
  6513. if (date) {
  6514. _this.setRange(_this.computeRange(date));
  6515. }
  6516. if (_this.render) {
  6517. _this.render(); // TODO: deprecate
  6518. }
  6519. _this.renderDates();
  6520. _this.updateSize();
  6521. _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  6522. _this.startNowIndicator();
  6523. _this.thawHeight();
  6524. _this.releaseScroll();
  6525. _this.isDateRendered = true;
  6526. _this.onDateRender();
  6527. _this.trigger('dateRender');
  6528. });
  6529. },
  6530. executeDateUnrender: function() {
  6531. var _this = this;
  6532. if (_this.isDateRendered) {
  6533. return this.requestEventsUnrender().then(function() {
  6534. _this.unselect();
  6535. _this.stopNowIndicator();
  6536. _this.triggerUnrender();
  6537. _this.unrenderBusinessHours();
  6538. _this.unrenderDates();
  6539. if (_this.destroy) {
  6540. _this.destroy(); // TODO: deprecate
  6541. }
  6542. _this.isDateRendered = false;
  6543. _this.trigger('dateUnrender');
  6544. });
  6545. }
  6546. else {
  6547. return Promise.resolve();
  6548. }
  6549. },
  6550. // Date Rendering Triggers
  6551. // -----------------------------------------------------------------------------------------------------------------
  6552. onDateRender: function() {
  6553. this.triggerRender();
  6554. },
  6555. // Date Low-level Rendering
  6556. // -----------------------------------------------------------------------------------------------------------------
  6557. // date-cell content only
  6558. renderDates: function() {
  6559. // subclasses should implement
  6560. },
  6561. // date-cell content only
  6562. unrenderDates: function() {
  6563. // subclasses should override
  6564. },
  6565. // Misc view rendering utils
  6566. // -------------------------
  6567. // Signals that the view's content has been rendered
  6568. triggerRender: function() {
  6569. this.publiclyTrigger('viewRender', this, this, this.el);
  6570. },
  6571. // Signals that the view's content is about to be unrendered
  6572. triggerUnrender: function() {
  6573. this.publiclyTrigger('viewDestroy', this, this, this.el);
  6574. },
  6575. // Binds DOM handlers to elements that reside outside the view container, such as the document
  6576. bindGlobalHandlers: function() {
  6577. this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
  6578. this.listenTo($(document), 'touchstart', this.processUnselect);
  6579. },
  6580. // Unbinds DOM handlers from elements that reside outside the view container
  6581. unbindGlobalHandlers: function() {
  6582. this.stopListeningTo($(document));
  6583. },
  6584. // Initializes internal variables related to theming
  6585. initThemingProps: function() {
  6586. var tm = this.opt('theme') ? 'ui' : 'fc';
  6587. this.widgetHeaderClass = tm + '-widget-header';
  6588. this.widgetContentClass = tm + '-widget-content';
  6589. this.highlightStateClass = tm + '-state-highlight';
  6590. },
  6591. /* Business Hours
  6592. ------------------------------------------------------------------------------------------------------------------*/
  6593. // Renders business-hours onto the view. Assumes updateSize has already been called.
  6594. renderBusinessHours: function() {
  6595. // subclasses should implement
  6596. },
  6597. // Unrenders previously-rendered business-hours
  6598. unrenderBusinessHours: function() {
  6599. // subclasses should implement
  6600. },
  6601. /* Now Indicator
  6602. ------------------------------------------------------------------------------------------------------------------*/
  6603. // Immediately render the current time indicator and begins re-rendering it at an interval,
  6604. // which is defined by this.getNowIndicatorUnit().
  6605. // TODO: somehow do this for the current whole day's background too
  6606. startNowIndicator: function() {
  6607. var _this = this;
  6608. var unit;
  6609. var update;
  6610. var delay; // ms wait value
  6611. if (this.opt('nowIndicator')) {
  6612. unit = this.getNowIndicatorUnit();
  6613. if (unit) {
  6614. update = proxy(this, 'updateNowIndicator'); // bind to `this`
  6615. this.initialNowDate = this.calendar.getNow();
  6616. this.initialNowQueriedMs = +new Date();
  6617. this.renderNowIndicator(this.initialNowDate);
  6618. this.isNowIndicatorRendered = true;
  6619. // wait until the beginning of the next interval
  6620. delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
  6621. this.nowIndicatorTimeoutID = setTimeout(function() {
  6622. _this.nowIndicatorTimeoutID = null;
  6623. update();
  6624. delay = +moment.duration(1, unit);
  6625. delay = Math.max(100, delay); // prevent too frequent
  6626. _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
  6627. }, delay);
  6628. }
  6629. }
  6630. },
  6631. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  6632. // since the initial getNow call.
  6633. updateNowIndicator: function() {
  6634. if (this.isNowIndicatorRendered) {
  6635. this.unrenderNowIndicator();
  6636. this.renderNowIndicator(
  6637. this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
  6638. );
  6639. }
  6640. },
  6641. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  6642. // Won't cause side effects if indicator isn't rendered.
  6643. stopNowIndicator: function() {
  6644. if (this.isNowIndicatorRendered) {
  6645. if (this.nowIndicatorTimeoutID) {
  6646. clearTimeout(this.nowIndicatorTimeoutID);
  6647. this.nowIndicatorTimeoutID = null;
  6648. }
  6649. if (this.nowIndicatorIntervalID) {
  6650. clearTimeout(this.nowIndicatorIntervalID);
  6651. this.nowIndicatorIntervalID = null;
  6652. }
  6653. this.unrenderNowIndicator();
  6654. this.isNowIndicatorRendered = false;
  6655. }
  6656. },
  6657. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  6658. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  6659. getNowIndicatorUnit: function() {
  6660. // subclasses should implement
  6661. },
  6662. // Renders a current time indicator at the given datetime
  6663. renderNowIndicator: function(date) {
  6664. // subclasses should implement
  6665. },
  6666. // Undoes the rendering actions from renderNowIndicator
  6667. unrenderNowIndicator: function() {
  6668. // subclasses should implement
  6669. },
  6670. /* Dimensions
  6671. ------------------------------------------------------------------------------------------------------------------*/
  6672. // Refreshes anything dependant upon sizing of the container element of the grid
  6673. updateSize: function(isResize) {
  6674. if (isResize) {
  6675. this.captureScroll();
  6676. }
  6677. this.updateHeight(isResize);
  6678. this.updateWidth(isResize);
  6679. this.updateNowIndicator();
  6680. if (isResize) {
  6681. this.releaseScroll();
  6682. }
  6683. },
  6684. // Refreshes the horizontal dimensions of the calendar
  6685. updateWidth: function(isResize) {
  6686. // subclasses should implement
  6687. },
  6688. // Refreshes the vertical dimensions of the calendar
  6689. updateHeight: function(isResize) {
  6690. var calendar = this.calendar; // we poll the calendar for height information
  6691. this.setHeight(
  6692. calendar.getSuggestedViewHeight(),
  6693. calendar.isHeightAuto()
  6694. );
  6695. },
  6696. // Updates the vertical dimensions of the calendar to the specified height.
  6697. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  6698. setHeight: function(height, isAuto) {
  6699. // subclasses should implement
  6700. },
  6701. /* Scroller
  6702. ------------------------------------------------------------------------------------------------------------------*/
  6703. capturedScroll: null,
  6704. capturedScrollDepth: 0,
  6705. captureScroll: function() {
  6706. if (!(this.capturedScrollDepth++)) {
  6707. this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
  6708. return true; // root?
  6709. }
  6710. return false;
  6711. },
  6712. captureInitialScroll: function(forcedScroll) {
  6713. if (this.captureScroll()) { // root?
  6714. this.capturedScroll.isInitial = true;
  6715. if (forcedScroll) {
  6716. $.extend(this.capturedScroll, forcedScroll);
  6717. }
  6718. else {
  6719. this.capturedScroll.isComputed = true;
  6720. }
  6721. }
  6722. },
  6723. releaseScroll: function() {
  6724. var scroll = this.capturedScroll;
  6725. var isRoot = this.discardScroll();
  6726. if (scroll.isComputed) {
  6727. if (isRoot) {
  6728. // only compute initial scroll if it will actually be used (is the root capture)
  6729. $.extend(scroll, this.computeInitialScroll());
  6730. }
  6731. else {
  6732. scroll = null; // scroll couldn't be computed. don't apply it to the DOM
  6733. }
  6734. }
  6735. if (scroll) {
  6736. // we act immediately on a releaseScroll operation, as opposed to captureScroll.
  6737. // if capture/release wraps a render operation that screws up the scroll,
  6738. // we still want to restore it a good state after, regardless of depth.
  6739. if (scroll.isInitial) {
  6740. this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
  6741. }
  6742. else {
  6743. this.setScroll(scroll);
  6744. }
  6745. }
  6746. },
  6747. discardScroll: function() {
  6748. if (!(--this.capturedScrollDepth)) {
  6749. this.capturedScroll = null;
  6750. return true; // root?
  6751. }
  6752. return false;
  6753. },
  6754. computeInitialScroll: function() {
  6755. return {};
  6756. },
  6757. queryScroll: function() {
  6758. return {};
  6759. },
  6760. hardSetScroll: function(scroll) {
  6761. var _this = this;
  6762. var exec = function() { _this.setScroll(scroll); };
  6763. exec();
  6764. setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
  6765. },
  6766. setScroll: function(scroll) {
  6767. },
  6768. /* Height Freezing
  6769. ------------------------------------------------------------------------------------------------------------------*/
  6770. freezeHeight: function() {
  6771. this.calendar.freezeContentHeight();
  6772. },
  6773. thawHeight: function() {
  6774. this.calendar.thawContentHeight();
  6775. },
  6776. // Event Binding/Unbinding
  6777. // -----------------------------------------------------------------------------------------------------------------
  6778. bindEvents: function() {
  6779. var _this = this;
  6780. if (!this.isEventsBound) {
  6781. this.isEventsBound = true;
  6782. this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
  6783. _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
  6784. _this.setEvents(events);
  6785. });
  6786. }
  6787. },
  6788. unbindEvents: function() {
  6789. if (this.isEventsBound) {
  6790. this.isEventsBound = false;
  6791. this.stopListeningTo(this.calendar, 'eventsReset');
  6792. this.unsetEvents();
  6793. this.trigger('eventsUnbind');
  6794. }
  6795. },
  6796. // Event Setting/Unsetting
  6797. // -----------------------------------------------------------------------------------------------------------------
  6798. setEvents: function(events) {
  6799. var isReset = this.isEventSet;
  6800. this.isEventsSet = true;
  6801. this.handleEvents(events, isReset);
  6802. this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
  6803. },
  6804. unsetEvents: function() {
  6805. if (this.isEventsSet) {
  6806. this.isEventsSet = false;
  6807. this.handleEventsUnset();
  6808. this.trigger('eventsUnset');
  6809. }
  6810. },
  6811. whenEventsSet: function() {
  6812. var _this = this;
  6813. if (this.isEventsSet) {
  6814. return Promise.resolve(this.getCurrentEvents());
  6815. }
  6816. else {
  6817. return new Promise(function(resolve) {
  6818. _this.one('eventsSet', resolve);
  6819. });
  6820. }
  6821. },
  6822. // Event Handling
  6823. // -----------------------------------------------------------------------------------------------------------------
  6824. handleEvents: function(events, isReset) {
  6825. this.requestEventsRender(events);
  6826. },
  6827. handleEventsUnset: function() {
  6828. this.requestEventsUnrender();
  6829. },
  6830. // Event Render Queuing
  6831. // -----------------------------------------------------------------------------------------------------------------
  6832. // assumes any previous event renders have been cleared already
  6833. requestEventsRender: function(events) {
  6834. var _this = this;
  6835. return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
  6836. return _this.executeEventsRender(events);
  6837. });
  6838. },
  6839. requestEventsUnrender: function() {
  6840. var _this = this;
  6841. if (this.isEventsRendered) {
  6842. return this.eventRenderQueue.addQuickly(function() {
  6843. return _this.executeEventsUnrender();
  6844. });
  6845. }
  6846. else {
  6847. return Promise.resolve();
  6848. }
  6849. },
  6850. requestCurrentEventsRender: function() {
  6851. if (this.isEventsSet) {
  6852. this.requestEventsRender(this.getCurrentEvents());
  6853. }
  6854. else {
  6855. return Promise.reject();
  6856. }
  6857. },
  6858. // Event High-level Rendering
  6859. // -----------------------------------------------------------------------------------------------------------------
  6860. executeEventsRender: function(events) {
  6861. var _this = this;
  6862. this.captureScroll();
  6863. this.freezeHeight();
  6864. return this.executeEventsUnrender().then(function() {
  6865. _this.renderEvents(events);
  6866. _this.thawHeight();
  6867. _this.releaseScroll();
  6868. _this.isEventsRendered = true;
  6869. _this.onEventsRender();
  6870. _this.trigger('eventsRender');
  6871. });
  6872. },
  6873. executeEventsUnrender: function() {
  6874. if (this.isEventsRendered) {
  6875. this.onBeforeEventsUnrender();
  6876. this.captureScroll();
  6877. this.freezeHeight();
  6878. if (this.destroyEvents) {
  6879. this.destroyEvents(); // TODO: deprecate
  6880. }
  6881. this.unrenderEvents();
  6882. this.thawHeight();
  6883. this.releaseScroll();
  6884. this.isEventsRendered = false;
  6885. this.trigger('eventsUnrender');
  6886. }
  6887. return Promise.resolve(); // always synchronous
  6888. },
  6889. // Event Rendering Triggers
  6890. // -----------------------------------------------------------------------------------------------------------------
  6891. // Signals that all events have been rendered
  6892. onEventsRender: function() {
  6893. this.renderedEventSegEach(function(seg) {
  6894. this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
  6895. });
  6896. this.publiclyTrigger('eventAfterAllRender');
  6897. },
  6898. // Signals that all event elements are about to be removed
  6899. onBeforeEventsUnrender: function() {
  6900. this.renderedEventSegEach(function(seg) {
  6901. this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
  6902. });
  6903. },
  6904. // Event Low-level Rendering
  6905. // -----------------------------------------------------------------------------------------------------------------
  6906. // Renders the events onto the view.
  6907. renderEvents: function(events) {
  6908. // subclasses should implement
  6909. },
  6910. // Removes event elements from the view.
  6911. unrenderEvents: function() {
  6912. // subclasses should implement
  6913. },
  6914. // Event Data Access
  6915. // -----------------------------------------------------------------------------------------------------------------
  6916. requestEvents: function() {
  6917. return this.calendar.requestEvents(this.start, this.end);
  6918. },
  6919. getCurrentEvents: function() {
  6920. return this.calendar.getPrunedEventCache();
  6921. },
  6922. // Event Rendering Utils
  6923. // -----------------------------------------------------------------------------------------------------------------
  6924. // Given an event and the default element used for rendering, returns the element that should actually be used.
  6925. // Basically runs events and elements through the eventRender hook.
  6926. resolveEventEl: function(event, el) {
  6927. var custom = this.publiclyTrigger('eventRender', event, event, el);
  6928. if (custom === false) { // means don't render at all
  6929. el = null;
  6930. }
  6931. else if (custom && custom !== true) {
  6932. el = $(custom);
  6933. }
  6934. return el;
  6935. },
  6936. // Hides all rendered event segments linked to the given event
  6937. showEvent: function(event) {
  6938. this.renderedEventSegEach(function(seg) {
  6939. seg.el.css('visibility', '');
  6940. }, event);
  6941. },
  6942. // Shows all rendered event segments linked to the given event
  6943. hideEvent: function(event) {
  6944. this.renderedEventSegEach(function(seg) {
  6945. seg.el.css('visibility', 'hidden');
  6946. }, event);
  6947. },
  6948. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  6949. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  6950. // The `this` value of the callback function will be the view.
  6951. renderedEventSegEach: function(func, event) {
  6952. var segs = this.getEventSegs();
  6953. var i;
  6954. for (i = 0; i < segs.length; i++) {
  6955. if (!event || segs[i].event._id === event._id) {
  6956. if (segs[i].el) {
  6957. func.call(this, segs[i]);
  6958. }
  6959. }
  6960. }
  6961. },
  6962. // Retrieves all the rendered segment objects for the view
  6963. getEventSegs: function() {
  6964. // subclasses must implement
  6965. return [];
  6966. },
  6967. /* Event Drag-n-Drop
  6968. ------------------------------------------------------------------------------------------------------------------*/
  6969. // Computes if the given event is allowed to be dragged by the user
  6970. isEventDraggable: function(event) {
  6971. return this.isEventStartEditable(event);
  6972. },
  6973. isEventStartEditable: function(event) {
  6974. return firstDefined(
  6975. event.startEditable,
  6976. (event.source || {}).startEditable,
  6977. this.opt('eventStartEditable'),
  6978. this.isEventGenerallyEditable(event)
  6979. );
  6980. },
  6981. isEventGenerallyEditable: function(event) {
  6982. return firstDefined(
  6983. event.editable,
  6984. (event.source || {}).editable,
  6985. this.opt('editable')
  6986. );
  6987. },
  6988. // Must be called when an event in the view is dropped onto new location.
  6989. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  6990. reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  6991. var calendar = this.calendar;
  6992. var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  6993. var undoFunc = function() {
  6994. mutateResult.undo();
  6995. calendar.reportEventChange();
  6996. };
  6997. this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  6998. calendar.reportEventChange(); // will rerender events
  6999. },
  7000. // Triggers event-drop handlers that have subscribed via the API
  7001. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  7002. this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  7003. },
  7004. /* External Element Drag-n-Drop
  7005. ------------------------------------------------------------------------------------------------------------------*/
  7006. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  7007. // `meta` is the parsed data that has been embedded into the dragging event.
  7008. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  7009. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  7010. var eventProps = meta.eventProps;
  7011. var eventInput;
  7012. var event;
  7013. // Try to build an event object and render it. TODO: decouple the two
  7014. if (eventProps) {
  7015. eventInput = $.extend({}, eventProps, dropLocation);
  7016. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  7017. }
  7018. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  7019. },
  7020. // Triggers external-drop handlers that have subscribed via the API
  7021. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  7022. // trigger 'drop' regardless of whether element represents an event
  7023. this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
  7024. if (event) {
  7025. this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
  7026. }
  7027. },
  7028. /* Drag-n-Drop Rendering (for both events and external elements)
  7029. ------------------------------------------------------------------------------------------------------------------*/
  7030. // Renders a visual indication of a event or external-element drag over the given drop zone.
  7031. // If an external-element, seg will be `null`.
  7032. // Must return elements used for any mock events.
  7033. renderDrag: function(dropLocation, seg) {
  7034. // subclasses must implement
  7035. },
  7036. // Unrenders a visual indication of an event or external-element being dragged.
  7037. unrenderDrag: function() {
  7038. // subclasses must implement
  7039. },
  7040. /* Event Resizing
  7041. ------------------------------------------------------------------------------------------------------------------*/
  7042. // Computes if the given event is allowed to be resized from its starting edge
  7043. isEventResizableFromStart: function(event) {
  7044. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  7045. },
  7046. // Computes if the given event is allowed to be resized from its ending edge
  7047. isEventResizableFromEnd: function(event) {
  7048. return this.isEventResizable(event);
  7049. },
  7050. // Computes if the given event is allowed to be resized by the user at all
  7051. isEventResizable: function(event) {
  7052. var source = event.source || {};
  7053. return firstDefined(
  7054. event.durationEditable,
  7055. source.durationEditable,
  7056. this.opt('eventDurationEditable'),
  7057. event.editable,
  7058. source.editable,
  7059. this.opt('editable')
  7060. );
  7061. },
  7062. // Must be called when an event in the view has been resized to a new length
  7063. reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  7064. var calendar = this.calendar;
  7065. var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  7066. var undoFunc = function() {
  7067. mutateResult.undo();
  7068. calendar.reportEventChange();
  7069. };
  7070. this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  7071. calendar.reportEventChange(); // will rerender events
  7072. },
  7073. // Triggers event-resize handlers that have subscribed via the API
  7074. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  7075. this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  7076. },
  7077. /* Selection (time range)
  7078. ------------------------------------------------------------------------------------------------------------------*/
  7079. // Selects a date span on the view. `start` and `end` are both Moments.
  7080. // `ev` is the native mouse event that begin the interaction.
  7081. select: function(span, ev) {
  7082. this.unselect(ev);
  7083. this.renderSelection(span);
  7084. this.reportSelection(span, ev);
  7085. },
  7086. // Renders a visual indication of the selection
  7087. renderSelection: function(span) {
  7088. // subclasses should implement
  7089. },
  7090. // Called when a new selection is made. Updates internal state and triggers handlers.
  7091. reportSelection: function(span, ev) {
  7092. this.isSelected = true;
  7093. this.triggerSelect(span, ev);
  7094. },
  7095. // Triggers handlers to 'select'
  7096. triggerSelect: function(span, ev) {
  7097. this.publiclyTrigger(
  7098. 'select',
  7099. null,
  7100. this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
  7101. this.calendar.applyTimezone(span.end), // "
  7102. ev
  7103. );
  7104. },
  7105. // Undoes a selection. updates in the internal state and triggers handlers.
  7106. // `ev` is the native mouse event that began the interaction.
  7107. unselect: function(ev) {
  7108. if (this.isSelected) {
  7109. this.isSelected = false;
  7110. if (this.destroySelection) {
  7111. this.destroySelection(); // TODO: deprecate
  7112. }
  7113. this.unrenderSelection();
  7114. this.publiclyTrigger('unselect', null, ev);
  7115. }
  7116. },
  7117. // Unrenders a visual indication of selection
  7118. unrenderSelection: function() {
  7119. // subclasses should implement
  7120. },
  7121. /* Event Selection
  7122. ------------------------------------------------------------------------------------------------------------------*/
  7123. selectEvent: function(event) {
  7124. if (!this.selectedEvent || this.selectedEvent !== event) {
  7125. this.unselectEvent();
  7126. this.renderedEventSegEach(function(seg) {
  7127. seg.el.addClass('fc-selected');
  7128. }, event);
  7129. this.selectedEvent = event;
  7130. }
  7131. },
  7132. unselectEvent: function() {
  7133. if (this.selectedEvent) {
  7134. this.renderedEventSegEach(function(seg) {
  7135. seg.el.removeClass('fc-selected');
  7136. }, this.selectedEvent);
  7137. this.selectedEvent = null;
  7138. }
  7139. },
  7140. isEventSelected: function(event) {
  7141. // event references might change on refetchEvents(), while selectedEvent doesn't,
  7142. // so compare IDs
  7143. return this.selectedEvent && this.selectedEvent._id === event._id;
  7144. },
  7145. /* Mouse / Touch Unselecting (time range & event unselection)
  7146. ------------------------------------------------------------------------------------------------------------------*/
  7147. // TODO: move consistently to down/start or up/end?
  7148. // TODO: don't kill previous selection if touch scrolling
  7149. handleDocumentMousedown: function(ev) {
  7150. if (isPrimaryMouseButton(ev)) {
  7151. this.processUnselect(ev);
  7152. }
  7153. },
  7154. processUnselect: function(ev) {
  7155. this.processRangeUnselect(ev);
  7156. this.processEventUnselect(ev);
  7157. },
  7158. processRangeUnselect: function(ev) {
  7159. var ignore;
  7160. // is there a time-range selection?
  7161. if (this.isSelected && this.opt('unselectAuto')) {
  7162. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  7163. ignore = this.opt('unselectCancel');
  7164. if (!ignore || !$(ev.target).closest(ignore).length) {
  7165. this.unselect(ev);
  7166. }
  7167. }
  7168. },
  7169. processEventUnselect: function(ev) {
  7170. if (this.selectedEvent) {
  7171. if (!$(ev.target).closest('.fc-selected').length) {
  7172. this.unselectEvent();
  7173. }
  7174. }
  7175. },
  7176. /* Day Click
  7177. ------------------------------------------------------------------------------------------------------------------*/
  7178. // Triggers handlers to 'dayClick'
  7179. // Span has start/end of the clicked area. Only the start is useful.
  7180. triggerDayClick: function(span, dayEl, ev) {
  7181. this.publiclyTrigger(
  7182. 'dayClick',
  7183. dayEl,
  7184. this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
  7185. ev
  7186. );
  7187. },
  7188. /* Date Utils
  7189. ------------------------------------------------------------------------------------------------------------------*/
  7190. // Initializes internal variables related to calculating hidden days-of-week
  7191. initHiddenDays: function() {
  7192. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  7193. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  7194. var dayCnt = 0;
  7195. var i;
  7196. if (this.opt('weekends') === false) {
  7197. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  7198. }
  7199. for (i = 0; i < 7; i++) {
  7200. if (
  7201. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  7202. ) {
  7203. dayCnt++;
  7204. }
  7205. }
  7206. if (!dayCnt) {
  7207. throw 'invalid hiddenDays'; // all days were hidden? bad.
  7208. }
  7209. this.isHiddenDayHash = isHiddenDayHash;
  7210. },
  7211. // Is the current day hidden?
  7212. // `day` is a day-of-week index (0-6), or a Moment
  7213. isHiddenDay: function(day) {
  7214. if (moment.isMoment(day)) {
  7215. day = day.day();
  7216. }
  7217. return this.isHiddenDayHash[day];
  7218. },
  7219. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  7220. // If the initial value of `date` is not a hidden day, don't do anything.
  7221. // Pass `isExclusive` as `true` if you are dealing with an end date.
  7222. // `inc` defaults to `1` (increment one day forward each time)
  7223. skipHiddenDays: function(date, inc, isExclusive) {
  7224. var out = date.clone();
  7225. inc = inc || 1;
  7226. while (
  7227. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  7228. ) {
  7229. out.add(inc, 'days');
  7230. }
  7231. return out;
  7232. },
  7233. // Returns the date range of the full days the given range visually appears to occupy.
  7234. // Returns a new range object.
  7235. computeDayRange: function(range) {
  7236. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  7237. var end = range.end;
  7238. var endDay = null;
  7239. var endTimeMS;
  7240. if (end) {
  7241. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  7242. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  7243. // If the end time is actually inclusively part of the next day and is equal to or
  7244. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  7245. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  7246. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  7247. endDay.add(1, 'days');
  7248. }
  7249. }
  7250. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  7251. // assign the default duration of one day.
  7252. if (!end || endDay <= startDay) {
  7253. endDay = startDay.clone().add(1, 'days');
  7254. }
  7255. return { start: startDay, end: endDay };
  7256. },
  7257. // Does the given event visually appear to occupy more than one day?
  7258. isMultiDayEvent: function(event) {
  7259. var range = this.computeDayRange(event); // event is range-ish
  7260. return range.end.diff(range.start, 'days') > 1;
  7261. }
  7262. });
  7263. ;;
  7264. /*
  7265. Embodies a div that has potential scrollbars
  7266. */
  7267. var Scroller = FC.Scroller = Class.extend({
  7268. el: null, // the guaranteed outer element
  7269. scrollEl: null, // the element with the scrollbars
  7270. overflowX: null,
  7271. overflowY: null,
  7272. constructor: function(options) {
  7273. options = options || {};
  7274. this.overflowX = options.overflowX || options.overflow || 'auto';
  7275. this.overflowY = options.overflowY || options.overflow || 'auto';
  7276. },
  7277. render: function() {
  7278. this.el = this.renderEl();
  7279. this.applyOverflow();
  7280. },
  7281. renderEl: function() {
  7282. return (this.scrollEl = $('<div class="fc-scroller"></div>'));
  7283. },
  7284. // sets to natural height, unlocks overflow
  7285. clear: function() {
  7286. this.setHeight('auto');
  7287. this.applyOverflow();
  7288. },
  7289. destroy: function() {
  7290. this.el.remove();
  7291. },
  7292. // Overflow
  7293. // -----------------------------------------------------------------------------------------------------------------
  7294. applyOverflow: function() {
  7295. this.scrollEl.css({
  7296. 'overflow-x': this.overflowX,
  7297. 'overflow-y': this.overflowY
  7298. });
  7299. },
  7300. // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
  7301. // Useful for preserving scrollbar widths regardless of future resizes.
  7302. // Can pass in scrollbarWidths for optimization.
  7303. lockOverflow: function(scrollbarWidths) {
  7304. var overflowX = this.overflowX;
  7305. var overflowY = this.overflowY;
  7306. scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
  7307. if (overflowX === 'auto') {
  7308. overflowX = (
  7309. scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
  7310. // OR scrolling pane with massless scrollbars?
  7311. this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
  7312. // subtract 1 because of IE off-by-one issue
  7313. ) ? 'scroll' : 'hidden';
  7314. }
  7315. if (overflowY === 'auto') {
  7316. overflowY = (
  7317. scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
  7318. // OR scrolling pane with massless scrollbars?
  7319. this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
  7320. // subtract 1 because of IE off-by-one issue
  7321. ) ? 'scroll' : 'hidden';
  7322. }
  7323. this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
  7324. },
  7325. // Getters / Setters
  7326. // -----------------------------------------------------------------------------------------------------------------
  7327. setHeight: function(height) {
  7328. this.scrollEl.height(height);
  7329. },
  7330. getScrollTop: function() {
  7331. return this.scrollEl.scrollTop();
  7332. },
  7333. setScrollTop: function(top) {
  7334. this.scrollEl.scrollTop(top);
  7335. },
  7336. getClientWidth: function() {
  7337. return this.scrollEl[0].clientWidth;
  7338. },
  7339. getClientHeight: function() {
  7340. return this.scrollEl[0].clientHeight;
  7341. },
  7342. getScrollbarWidths: function() {
  7343. return getScrollbarWidths(this.scrollEl);
  7344. }
  7345. });
  7346. ;;
  7347. function Iterator(items) {
  7348. this.items = items || [];
  7349. }
  7350. /* Calls a method on every item passing the arguments through */
  7351. Iterator.prototype.proxyCall = function(methodName) {
  7352. var args = Array.prototype.slice.call(arguments, 1);
  7353. var results = [];
  7354. this.items.forEach(function(item) {
  7355. results.push(item[methodName].apply(item, args));
  7356. });
  7357. return results;
  7358. };
  7359. ;;
  7360. /* Toolbar with buttons and title
  7361. ----------------------------------------------------------------------------------------------------------------------*/
  7362. function Toolbar(calendar, toolbarOptions) {
  7363. var t = this;
  7364. // exports
  7365. t.setToolbarOptions = setToolbarOptions;
  7366. t.render = render;
  7367. t.removeElement = removeElement;
  7368. t.updateTitle = updateTitle;
  7369. t.activateButton = activateButton;
  7370. t.deactivateButton = deactivateButton;
  7371. t.disableButton = disableButton;
  7372. t.enableButton = enableButton;
  7373. t.getViewsWithButtons = getViewsWithButtons;
  7374. t.el = null; // mirrors local `el`
  7375. // locals
  7376. var el;
  7377. var viewsWithButtons = [];
  7378. var tm;
  7379. // method to update toolbar-specific options, not calendar-wide options
  7380. function setToolbarOptions(newToolbarOptions) {
  7381. toolbarOptions = newToolbarOptions;
  7382. }
  7383. // can be called repeatedly and will rerender
  7384. function render() {
  7385. var sections = toolbarOptions.layout;
  7386. tm = calendar.options.theme ? 'ui' : 'fc';
  7387. if (sections) {
  7388. if (!el) {
  7389. el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
  7390. }
  7391. else {
  7392. el.empty();
  7393. }
  7394. el.append(renderSection('left'))
  7395. .append(renderSection('right'))
  7396. .append(renderSection('center'))
  7397. .append('<div class="fc-clear"/>');
  7398. }
  7399. else {
  7400. removeElement();
  7401. }
  7402. }
  7403. function removeElement() {
  7404. if (el) {
  7405. el.remove();
  7406. el = t.el = null;
  7407. }
  7408. }
  7409. function renderSection(position) {
  7410. var sectionEl = $('<div class="fc-' + position + '"/>');
  7411. var buttonStr = toolbarOptions.layout[position];
  7412. if (buttonStr) {
  7413. $.each(buttonStr.split(' '), function(i) {
  7414. var groupChildren = $();
  7415. var isOnlyButtons = true;
  7416. var groupEl;
  7417. $.each(this.split(','), function(j, buttonName) {
  7418. var customButtonProps;
  7419. var viewSpec;
  7420. var buttonClick;
  7421. var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
  7422. var defaultText;
  7423. var themeIcon;
  7424. var normalIcon;
  7425. var innerHtml;
  7426. var classes;
  7427. var button; // the element
  7428. if (buttonName == 'title') {
  7429. groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  7430. isOnlyButtons = false;
  7431. }
  7432. else {
  7433. if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
  7434. buttonClick = function(ev) {
  7435. if (customButtonProps.click) {
  7436. customButtonProps.click.call(button[0], ev);
  7437. }
  7438. };
  7439. overrideText = ''; // icons will override text
  7440. defaultText = customButtonProps.text;
  7441. }
  7442. else if ((viewSpec = calendar.getViewSpec(buttonName))) {
  7443. buttonClick = function() {
  7444. calendar.changeView(buttonName);
  7445. };
  7446. viewsWithButtons.push(buttonName);
  7447. overrideText = viewSpec.buttonTextOverride;
  7448. defaultText = viewSpec.buttonTextDefault;
  7449. }
  7450. else if (calendar[buttonName]) { // a calendar method
  7451. buttonClick = function() {
  7452. calendar[buttonName]();
  7453. };
  7454. overrideText = (calendar.overrides.buttonText || {})[buttonName];
  7455. defaultText = calendar.options.buttonText[buttonName]; // everything else is considered default
  7456. }
  7457. if (buttonClick) {
  7458. themeIcon =
  7459. customButtonProps ?
  7460. customButtonProps.themeIcon :
  7461. calendar.options.themeButtonIcons[buttonName];
  7462. normalIcon =
  7463. customButtonProps ?
  7464. customButtonProps.icon :
  7465. calendar.options.buttonIcons[buttonName];
  7466. if (overrideText) {
  7467. innerHtml = htmlEscape(overrideText);
  7468. }
  7469. else if (themeIcon && calendar.options.theme) {
  7470. innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  7471. }
  7472. else if (normalIcon && !calendar.options.theme) {
  7473. innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  7474. }
  7475. else {
  7476. innerHtml = htmlEscape(defaultText);
  7477. }
  7478. classes = [
  7479. 'fc-' + buttonName + '-button',
  7480. tm + '-button',
  7481. tm + '-state-default'
  7482. ];
  7483. button = $( // type="button" so that it doesn't submit a form
  7484. '<button type="button" class="' + classes.join(' ') + '">' +
  7485. innerHtml +
  7486. '</button>'
  7487. )
  7488. .click(function(ev) {
  7489. // don't process clicks for disabled buttons
  7490. if (!button.hasClass(tm + '-state-disabled')) {
  7491. buttonClick(ev);
  7492. // after the click action, if the button becomes the "active" tab, or disabled,
  7493. // it should never have a hover class, so remove it now.
  7494. if (
  7495. button.hasClass(tm + '-state-active') ||
  7496. button.hasClass(tm + '-state-disabled')
  7497. ) {
  7498. button.removeClass(tm + '-state-hover');
  7499. }
  7500. }
  7501. })
  7502. .mousedown(function() {
  7503. // the *down* effect (mouse pressed in).
  7504. // only on buttons that are not the "active" tab, or disabled
  7505. button
  7506. .not('.' + tm + '-state-active')
  7507. .not('.' + tm + '-state-disabled')
  7508. .addClass(tm + '-state-down');
  7509. })
  7510. .mouseup(function() {
  7511. // undo the *down* effect
  7512. button.removeClass(tm + '-state-down');
  7513. })
  7514. .hover(
  7515. function() {
  7516. // the *hover* effect.
  7517. // only on buttons that are not the "active" tab, or disabled
  7518. button
  7519. .not('.' + tm + '-state-active')
  7520. .not('.' + tm + '-state-disabled')
  7521. .addClass(tm + '-state-hover');
  7522. },
  7523. function() {
  7524. // undo the *hover* effect
  7525. button
  7526. .removeClass(tm + '-state-hover')
  7527. .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  7528. }
  7529. );
  7530. groupChildren = groupChildren.add(button);
  7531. }
  7532. }
  7533. });
  7534. if (isOnlyButtons) {
  7535. groupChildren
  7536. .first().addClass(tm + '-corner-left').end()
  7537. .last().addClass(tm + '-corner-right').end();
  7538. }
  7539. if (groupChildren.length > 1) {
  7540. groupEl = $('<div/>');
  7541. if (isOnlyButtons) {
  7542. groupEl.addClass('fc-button-group');
  7543. }
  7544. groupEl.append(groupChildren);
  7545. sectionEl.append(groupEl);
  7546. }
  7547. else {
  7548. sectionEl.append(groupChildren); // 1 or 0 children
  7549. }
  7550. });
  7551. }
  7552. return sectionEl;
  7553. }
  7554. function updateTitle(text) {
  7555. if (el) {
  7556. el.find('h2').text(text);
  7557. }
  7558. }
  7559. function activateButton(buttonName) {
  7560. if (el) {
  7561. el.find('.fc-' + buttonName + '-button')
  7562. .addClass(tm + '-state-active');
  7563. }
  7564. }
  7565. function deactivateButton(buttonName) {
  7566. if (el) {
  7567. el.find('.fc-' + buttonName + '-button')
  7568. .removeClass(tm + '-state-active');
  7569. }
  7570. }
  7571. function disableButton(buttonName) {
  7572. if (el) {
  7573. el.find('.fc-' + buttonName + '-button')
  7574. .prop('disabled', true)
  7575. .addClass(tm + '-state-disabled');
  7576. }
  7577. }
  7578. function enableButton(buttonName) {
  7579. if (el) {
  7580. el.find('.fc-' + buttonName + '-button')
  7581. .prop('disabled', false)
  7582. .removeClass(tm + '-state-disabled');
  7583. }
  7584. }
  7585. function getViewsWithButtons() {
  7586. return viewsWithButtons;
  7587. }
  7588. }
  7589. ;;
  7590. var Calendar = FC.Calendar = Class.extend({
  7591. dirDefaults: null, // option defaults related to LTR or RTL
  7592. localeDefaults: null, // option defaults related to current locale
  7593. overrides: null, // option overrides given to the fullCalendar constructor
  7594. dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
  7595. options: null, // all defaults combined with overrides
  7596. viewSpecCache: null, // cache of view definitions
  7597. view: null, // current View object
  7598. header: null,
  7599. footer: null,
  7600. loadingLevel: 0, // number of simultaneous loading tasks
  7601. // a lot of this class' OOP logic is scoped within this constructor function,
  7602. // but in the future, write individual methods on the prototype.
  7603. constructor: Calendar_constructor,
  7604. // Subclasses can override this for initialization logic after the constructor has been called
  7605. initialize: function() {
  7606. },
  7607. // Computes the flattened options hash for the calendar and assigns to `this.options`.
  7608. // Assumes this.overrides and this.dynamicOverrides have already been initialized.
  7609. populateOptionsHash: function() {
  7610. var locale, localeDefaults;
  7611. var isRTL, dirDefaults;
  7612. locale = firstDefined( // explicit locale option given?
  7613. this.dynamicOverrides.locale,
  7614. this.overrides.locale
  7615. );
  7616. localeDefaults = localeOptionHash[locale];
  7617. if (!localeDefaults) { // explicit locale option not given or invalid?
  7618. locale = Calendar.defaults.locale;
  7619. localeDefaults = localeOptionHash[locale] || {};
  7620. }
  7621. isRTL = firstDefined( // based on options computed so far, is direction RTL?
  7622. this.dynamicOverrides.isRTL,
  7623. this.overrides.isRTL,
  7624. localeDefaults.isRTL,
  7625. Calendar.defaults.isRTL
  7626. );
  7627. dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  7628. this.dirDefaults = dirDefaults;
  7629. this.localeDefaults = localeDefaults;
  7630. this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  7631. Calendar.defaults, // global defaults
  7632. dirDefaults,
  7633. localeDefaults,
  7634. this.overrides,
  7635. this.dynamicOverrides
  7636. ]);
  7637. populateInstanceComputableOptions(this.options); // fill in gaps with computed options
  7638. },
  7639. // Gets information about how to create a view. Will use a cache.
  7640. getViewSpec: function(viewType) {
  7641. var cache = this.viewSpecCache;
  7642. return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  7643. },
  7644. // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  7645. // Preference is given to views that have corresponding buttons.
  7646. getUnitViewSpec: function(unit) {
  7647. var viewTypes;
  7648. var i;
  7649. var spec;
  7650. if ($.inArray(unit, intervalUnits) != -1) {
  7651. // put views that have buttons first. there will be duplicates, but oh well
  7652. viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
  7653. $.each(FC.views, function(viewType) { // all views
  7654. viewTypes.push(viewType);
  7655. });
  7656. for (i = 0; i < viewTypes.length; i++) {
  7657. spec = this.getViewSpec(viewTypes[i]);
  7658. if (spec) {
  7659. if (spec.singleUnit == unit) {
  7660. return spec;
  7661. }
  7662. }
  7663. }
  7664. }
  7665. },
  7666. // Builds an object with information on how to create a given view
  7667. buildViewSpec: function(requestedViewType) {
  7668. var viewOverrides = this.overrides.views || {};
  7669. var specChain = []; // for the view. lowest to highest priority
  7670. var defaultsChain = []; // for the view. lowest to highest priority
  7671. var overridesChain = []; // for the view. lowest to highest priority
  7672. var viewType = requestedViewType;
  7673. var spec; // for the view
  7674. var overrides; // for the view
  7675. var duration;
  7676. var unit;
  7677. // iterate from the specific view definition to a more general one until we hit an actual View class
  7678. while (viewType) {
  7679. spec = fcViews[viewType];
  7680. overrides = viewOverrides[viewType];
  7681. viewType = null; // clear. might repopulate for another iteration
  7682. if (typeof spec === 'function') { // TODO: deprecate
  7683. spec = { 'class': spec };
  7684. }
  7685. if (spec) {
  7686. specChain.unshift(spec);
  7687. defaultsChain.unshift(spec.defaults || {});
  7688. duration = duration || spec.duration;
  7689. viewType = viewType || spec.type;
  7690. }
  7691. if (overrides) {
  7692. overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  7693. duration = duration || overrides.duration;
  7694. viewType = viewType || overrides.type;
  7695. }
  7696. }
  7697. spec = mergeProps(specChain);
  7698. spec.type = requestedViewType;
  7699. if (!spec['class']) {
  7700. return false;
  7701. }
  7702. if (duration) {
  7703. duration = moment.duration(duration);
  7704. if (duration.valueOf()) { // valid?
  7705. spec.duration = duration;
  7706. unit = computeIntervalUnit(duration);
  7707. // view is a single-unit duration, like "week" or "day"
  7708. // incorporate options for this. lowest priority
  7709. if (duration.as(unit) === 1) {
  7710. spec.singleUnit = unit;
  7711. overridesChain.unshift(viewOverrides[unit] || {});
  7712. }
  7713. }
  7714. }
  7715. spec.defaults = mergeOptions(defaultsChain);
  7716. spec.overrides = mergeOptions(overridesChain);
  7717. this.buildViewSpecOptions(spec);
  7718. this.buildViewSpecButtonText(spec, requestedViewType);
  7719. return spec;
  7720. },
  7721. // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  7722. buildViewSpecOptions: function(spec) {
  7723. spec.options = mergeOptions([ // lowest to highest priority
  7724. Calendar.defaults, // global defaults
  7725. spec.defaults, // view's defaults (from ViewSubclass.defaults)
  7726. this.dirDefaults,
  7727. this.localeDefaults, // locale and dir take precedence over view's defaults!
  7728. this.overrides, // calendar's overrides (options given to constructor)
  7729. spec.overrides, // view's overrides (view-specific options)
  7730. this.dynamicOverrides // dynamically set via setter. highest precedence
  7731. ]);
  7732. populateInstanceComputableOptions(spec.options);
  7733. },
  7734. // Computes and assigns a view spec's buttonText-related options
  7735. buildViewSpecButtonText: function(spec, requestedViewType) {
  7736. // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  7737. // requested view, falling back to a generic unit entry like "week" or "day"
  7738. function queryButtonText(options) {
  7739. var buttonText = options.buttonText || {};
  7740. return buttonText[requestedViewType] ||
  7741. // view can decide to look up a certain key
  7742. (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
  7743. // a key like "month"
  7744. (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  7745. }
  7746. // highest to lowest priority
  7747. spec.buttonTextOverride =
  7748. queryButtonText(this.dynamicOverrides) ||
  7749. queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  7750. spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  7751. // highest to lowest priority. mirrors buildViewSpecOptions
  7752. spec.buttonTextDefault =
  7753. queryButtonText(this.localeDefaults) ||
  7754. queryButtonText(this.dirDefaults) ||
  7755. spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  7756. queryButtonText(Calendar.defaults) ||
  7757. (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  7758. requestedViewType; // fall back to given view name
  7759. },
  7760. // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  7761. instantiateView: function(viewType) {
  7762. var spec = this.getViewSpec(viewType);
  7763. return new spec['class'](this, viewType, spec.options, spec.duration);
  7764. },
  7765. // Returns a boolean about whether the view is okay to instantiate at some point
  7766. isValidViewType: function(viewType) {
  7767. return Boolean(this.getViewSpec(viewType));
  7768. },
  7769. // Should be called when any type of async data fetching begins
  7770. pushLoading: function() {
  7771. if (!(this.loadingLevel++)) {
  7772. this.publiclyTrigger('loading', null, true, this.view);
  7773. }
  7774. },
  7775. // Should be called when any type of async data fetching completes
  7776. popLoading: function() {
  7777. if (!(--this.loadingLevel)) {
  7778. this.publiclyTrigger('loading', null, false, this.view);
  7779. }
  7780. },
  7781. // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
  7782. buildSelectSpan: function(zonedStartInput, zonedEndInput) {
  7783. var start = this.moment(zonedStartInput).stripZone();
  7784. var end;
  7785. if (zonedEndInput) {
  7786. end = this.moment(zonedEndInput).stripZone();
  7787. }
  7788. else if (start.hasTime()) {
  7789. end = start.clone().add(this.defaultTimedEventDuration);
  7790. }
  7791. else {
  7792. end = start.clone().add(this.defaultAllDayEventDuration);
  7793. }
  7794. return { start: start, end: end };
  7795. }
  7796. });
  7797. Calendar.mixin(EmitterMixin);
  7798. function Calendar_constructor(element, overrides) {
  7799. var t = this;
  7800. // Exports
  7801. // -----------------------------------------------------------------------------------
  7802. t.render = render;
  7803. t.destroy = destroy;
  7804. t.rerenderEvents = rerenderEvents;
  7805. t.changeView = renderView; // `renderView` will switch to another view
  7806. t.select = select;
  7807. t.unselect = unselect;
  7808. t.prev = prev;
  7809. t.next = next;
  7810. t.prevYear = prevYear;
  7811. t.nextYear = nextYear;
  7812. t.today = today;
  7813. t.gotoDate = gotoDate;
  7814. t.incrementDate = incrementDate;
  7815. t.zoomTo = zoomTo;
  7816. t.getDate = getDate;
  7817. t.getCalendar = getCalendar;
  7818. t.getView = getView;
  7819. t.option = option; // getter/setter method
  7820. t.publiclyTrigger = publiclyTrigger;
  7821. // Options
  7822. // -----------------------------------------------------------------------------------
  7823. t.dynamicOverrides = {};
  7824. t.viewSpecCache = {};
  7825. t.optionHandlers = {}; // for Calendar.options.js
  7826. t.overrides = $.extend({}, overrides); // make a copy
  7827. t.populateOptionsHash(); // sets this.options
  7828. // Locale-data Internals
  7829. // -----------------------------------------------------------------------------------
  7830. // Apply overrides to the current locale's data
  7831. var localeData;
  7832. // Called immediately, and when any of the options change.
  7833. // Happens before any internal objects rebuild or rerender, because this is very core.
  7834. t.bindOptions([
  7835. 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
  7836. ], function(locale, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) {
  7837. // normalize
  7838. if (weekNumberCalculation === 'iso') {
  7839. weekNumberCalculation = 'ISO'; // normalize
  7840. }
  7841. localeData = createObject( // make a cheap copy
  7842. getMomentLocaleData(locale) // will fall back to en
  7843. );
  7844. if (monthNames) {
  7845. localeData._months = monthNames;
  7846. }
  7847. if (monthNamesShort) {
  7848. localeData._monthsShort = monthNamesShort;
  7849. }
  7850. if (dayNames) {
  7851. localeData._weekdays = dayNames;
  7852. }
  7853. if (dayNamesShort) {
  7854. localeData._weekdaysShort = dayNamesShort;
  7855. }
  7856. if (firstDay == null && weekNumberCalculation === 'ISO') {
  7857. firstDay = 1;
  7858. }
  7859. if (firstDay != null) {
  7860. var _week = createObject(localeData._week); // _week: { dow: # }
  7861. _week.dow = firstDay;
  7862. localeData._week = _week;
  7863. }
  7864. if ( // whitelist certain kinds of input
  7865. weekNumberCalculation === 'ISO' ||
  7866. weekNumberCalculation === 'local' ||
  7867. typeof weekNumberCalculation === 'function'
  7868. ) {
  7869. localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
  7870. }
  7871. // If the internal current date object already exists, move to new locale.
  7872. // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
  7873. if (date) {
  7874. localizeMoment(date); // sets to localeData
  7875. }
  7876. });
  7877. // Calendar-specific Date Utilities
  7878. // -----------------------------------------------------------------------------------
  7879. t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration);
  7880. t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration);
  7881. // Builds a moment using the settings of the current calendar: timezone and locale.
  7882. // Accepts anything the vanilla moment() constructor accepts.
  7883. t.moment = function() {
  7884. var mom;
  7885. if (t.options.timezone === 'local') {
  7886. mom = FC.moment.apply(null, arguments);
  7887. // Force the moment to be local, because FC.moment doesn't guarantee it.
  7888. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  7889. mom.local();
  7890. }
  7891. }
  7892. else if (t.options.timezone === 'UTC') {
  7893. mom = FC.moment.utc.apply(null, arguments); // process as UTC
  7894. }
  7895. else {
  7896. mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
  7897. }
  7898. localizeMoment(mom);
  7899. return mom;
  7900. };
  7901. // Updates the given moment's locale settings to the current calendar locale settings.
  7902. function localizeMoment(mom) {
  7903. mom._locale = localeData;
  7904. }
  7905. t.localizeMoment = localizeMoment;
  7906. // Returns a boolean about whether or not the calendar knows how to calculate
  7907. // the timezone offset of arbitrary dates in the current timezone.
  7908. t.getIsAmbigTimezone = function() {
  7909. return t.options.timezone !== 'local' && t.options.timezone !== 'UTC';
  7910. };
  7911. // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
  7912. t.applyTimezone = function(date) {
  7913. if (!date.hasTime()) {
  7914. return date.clone();
  7915. }
  7916. var zonedDate = t.moment(date.toArray());
  7917. var timeAdjust = date.time() - zonedDate.time();
  7918. var adjustedZonedDate;
  7919. // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
  7920. if (timeAdjust) { // is the time result different than expected?
  7921. adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
  7922. if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now?
  7923. zonedDate = adjustedZonedDate;
  7924. }
  7925. }
  7926. return zonedDate;
  7927. };
  7928. // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
  7929. // Will return an moment with an ambiguous timezone.
  7930. t.getNow = function() {
  7931. var now = t.options.now;
  7932. if (typeof now === 'function') {
  7933. now = now();
  7934. }
  7935. return t.moment(now).stripZone();
  7936. };
  7937. // Get an event's normalized end date. If not present, calculate it from the defaults.
  7938. t.getEventEnd = function(event) {
  7939. if (event.end) {
  7940. return event.end.clone();
  7941. }
  7942. else {
  7943. return t.getDefaultEventEnd(event.allDay, event.start);
  7944. }
  7945. };
  7946. // Given an event's allDay status and start date, return what its fallback end date should be.
  7947. // TODO: rename to computeDefaultEventEnd
  7948. t.getDefaultEventEnd = function(allDay, zonedStart) {
  7949. var end = zonedStart.clone();
  7950. if (allDay) {
  7951. end.stripTime().add(t.defaultAllDayEventDuration);
  7952. }
  7953. else {
  7954. end.add(t.defaultTimedEventDuration);
  7955. }
  7956. if (t.getIsAmbigTimezone()) {
  7957. end.stripZone(); // we don't know what the tzo should be
  7958. }
  7959. return end;
  7960. };
  7961. // Produces a human-readable string for the given duration.
  7962. // Side-effect: changes the locale of the given duration.
  7963. t.humanizeDuration = function(duration) {
  7964. return duration.locale(t.options.locale).humanize();
  7965. };
  7966. // Imports
  7967. // -----------------------------------------------------------------------------------
  7968. EventManager.call(t);
  7969. // Locals
  7970. // -----------------------------------------------------------------------------------
  7971. var _element = element[0];
  7972. var toolbarsManager;
  7973. var header;
  7974. var footer;
  7975. var content;
  7976. var tm; // for making theme classes
  7977. var currentView; // NOTE: keep this in sync with this.view
  7978. var viewsByType = {}; // holds all instantiated view instances, current or not
  7979. var suggestedViewHeight;
  7980. var windowResizeProxy; // wraps the windowResize function
  7981. var ignoreWindowResize = 0;
  7982. var date; // unzoned
  7983. // Main Rendering
  7984. // -----------------------------------------------------------------------------------
  7985. // compute the initial ambig-timezone date
  7986. if (t.options.defaultDate != null) {
  7987. date = t.moment(t.options.defaultDate).stripZone();
  7988. }
  7989. else {
  7990. date = t.getNow(); // getNow already returns unzoned
  7991. }
  7992. function render() {
  7993. if (!content) {
  7994. initialRender();
  7995. }
  7996. else if (elementVisible()) {
  7997. // mainly for the public API
  7998. calcSize();
  7999. renderView();
  8000. }
  8001. }
  8002. function initialRender() {
  8003. element.addClass('fc');
  8004. // event delegation for nav links
  8005. element.on('click.fc', 'a[data-goto]', function(ev) {
  8006. var anchorEl = $(this);
  8007. var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
  8008. var date = t.moment(gotoOptions.date);
  8009. var viewType = gotoOptions.type;
  8010. // property like "navLinkDayClick". might be a string or a function
  8011. var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
  8012. if (typeof customAction === 'function') {
  8013. customAction(date, ev);
  8014. }
  8015. else {
  8016. if (typeof customAction === 'string') {
  8017. viewType = customAction;
  8018. }
  8019. zoomTo(date, viewType);
  8020. }
  8021. });
  8022. // called immediately, and upon option change
  8023. t.bindOption('theme', function(theme) {
  8024. tm = theme ? 'ui' : 'fc'; // affects a larger scope
  8025. element.toggleClass('ui-widget', theme);
  8026. element.toggleClass('fc-unthemed', !theme);
  8027. });
  8028. // called immediately, and upon option change.
  8029. // HACK: locale often affects isRTL, so we explicitly listen to that too.
  8030. t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
  8031. element.toggleClass('fc-ltr', !isRTL);
  8032. element.toggleClass('fc-rtl', isRTL);
  8033. });
  8034. content = $("<div class='fc-view-container'/>").prependTo(element);
  8035. var toolbars = buildToolbars();
  8036. toolbarsManager = new Iterator(toolbars);
  8037. header = t.header = toolbars[0];
  8038. footer = t.footer = toolbars[1];
  8039. renderHeader();
  8040. renderFooter();
  8041. renderView(t.options.defaultView);
  8042. if (t.options.handleWindowResize) {
  8043. windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
  8044. $(window).resize(windowResizeProxy);
  8045. }
  8046. }
  8047. function destroy() {
  8048. if (currentView) {
  8049. currentView.removeElement();
  8050. // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  8051. // It is still the "current" view, just not rendered.
  8052. }
  8053. toolbarsManager.proxyCall('removeElement');
  8054. content.remove();
  8055. element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  8056. element.off('.fc'); // unbind nav link handlers
  8057. if (windowResizeProxy) {
  8058. $(window).unbind('resize', windowResizeProxy);
  8059. }
  8060. }
  8061. function elementVisible() {
  8062. return element.is(':visible');
  8063. }
  8064. // View Rendering
  8065. // -----------------------------------------------------------------------------------
  8066. // Renders a view because of a date change, view-type change, or for the first time.
  8067. // If not given a viewType, keep the current view but render different dates.
  8068. // Accepts an optional scroll state to restore to.
  8069. function renderView(viewType, forcedScroll) {
  8070. ignoreWindowResize++;
  8071. var needsClearView = currentView && viewType && currentView.type !== viewType;
  8072. // if viewType is changing, remove the old view's rendering
  8073. if (needsClearView) {
  8074. freezeContentHeight(); // prevent a scroll jump when view element is removed
  8075. clearView();
  8076. }
  8077. // if viewType changed, or the view was never created, create a fresh view
  8078. if (!currentView && viewType) {
  8079. currentView = t.view =
  8080. viewsByType[viewType] ||
  8081. (viewsByType[viewType] = t.instantiateView(viewType));
  8082. currentView.setElement(
  8083. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  8084. );
  8085. toolbarsManager.proxyCall('activateButton', viewType);
  8086. }
  8087. if (currentView) {
  8088. // in case the view should render a period of time that is completely hidden
  8089. date = currentView.massageCurrentDate(date);
  8090. // render or rerender the view
  8091. if (
  8092. !currentView.isDateSet ||
  8093. !( // NOT within interval range signals an implicit date window change
  8094. date >= currentView.intervalStart &&
  8095. date < currentView.intervalEnd
  8096. )
  8097. ) {
  8098. if (elementVisible()) {
  8099. if (forcedScroll) {
  8100. currentView.captureInitialScroll(forcedScroll);
  8101. }
  8102. currentView.setDate(date, forcedScroll);
  8103. if (forcedScroll) {
  8104. currentView.releaseScroll();
  8105. }
  8106. // need to do this after View::render, so dates are calculated
  8107. // NOTE: view updates title text proactively
  8108. updateToolbarsTodayButton();
  8109. }
  8110. }
  8111. }
  8112. if (needsClearView) {
  8113. thawContentHeight();
  8114. }
  8115. ignoreWindowResize--;
  8116. }
  8117. // Unrenders the current view and reflects this change in the Header.
  8118. // Unregsiters the `currentView`, but does not remove from viewByType hash.
  8119. function clearView() {
  8120. toolbarsManager.proxyCall('deactivateButton', currentView.type);
  8121. currentView.removeElement();
  8122. currentView = t.view = null;
  8123. }
  8124. // Destroys the view, including the view object. Then, re-instantiates it and renders it.
  8125. // Maintains the same scroll state.
  8126. // TODO: maintain any other user-manipulated state.
  8127. function reinitView() {
  8128. ignoreWindowResize++;
  8129. freezeContentHeight();
  8130. var viewType = currentView.type;
  8131. var scrollState = currentView.queryScroll();
  8132. clearView();
  8133. calcSize();
  8134. renderView(viewType, scrollState);
  8135. thawContentHeight();
  8136. ignoreWindowResize--;
  8137. }
  8138. // Resizing
  8139. // -----------------------------------------------------------------------------------
  8140. t.getSuggestedViewHeight = function() {
  8141. if (suggestedViewHeight === undefined) {
  8142. calcSize();
  8143. }
  8144. return suggestedViewHeight;
  8145. };
  8146. t.isHeightAuto = function() {
  8147. return t.options.contentHeight === 'auto' || t.options.height === 'auto';
  8148. };
  8149. function updateSize(shouldRecalc) {
  8150. if (elementVisible()) {
  8151. if (shouldRecalc) {
  8152. _calcSize();
  8153. }
  8154. ignoreWindowResize++;
  8155. currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  8156. ignoreWindowResize--;
  8157. return true; // signal success
  8158. }
  8159. }
  8160. function calcSize() {
  8161. if (elementVisible()) {
  8162. _calcSize();
  8163. }
  8164. }
  8165. function _calcSize() { // assumes elementVisible
  8166. var contentHeightInput = t.options.contentHeight;
  8167. var heightInput = t.options.height;
  8168. if (typeof contentHeightInput === 'number') { // exists and not 'auto'
  8169. suggestedViewHeight = contentHeightInput;
  8170. }
  8171. else if (typeof contentHeightInput === 'function') { // exists and is a function
  8172. suggestedViewHeight = contentHeightInput();
  8173. }
  8174. else if (typeof heightInput === 'number') { // exists and not 'auto'
  8175. suggestedViewHeight = heightInput - queryToolbarsHeight();
  8176. }
  8177. else if (typeof heightInput === 'function') { // exists and is a function
  8178. suggestedViewHeight = heightInput() - queryToolbarsHeight();
  8179. }
  8180. else if (heightInput === 'parent') { // set to height of parent element
  8181. suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
  8182. }
  8183. else {
  8184. suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
  8185. }
  8186. }
  8187. function queryToolbarsHeight() {
  8188. return toolbarsManager.items.reduce(function(accumulator, toolbar) {
  8189. var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
  8190. return accumulator + toolbarHeight;
  8191. }, 0);
  8192. }
  8193. function windowResize(ev) {
  8194. if (
  8195. !ignoreWindowResize &&
  8196. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  8197. currentView.start // view has already been rendered
  8198. ) {
  8199. if (updateSize(true)) {
  8200. currentView.publiclyTrigger('windowResize', _element);
  8201. }
  8202. }
  8203. }
  8204. /* Event Rendering
  8205. -----------------------------------------------------------------------------*/
  8206. function rerenderEvents() { // API method. destroys old events if previously rendered.
  8207. if (elementVisible()) {
  8208. t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
  8209. }
  8210. }
  8211. /* Toolbars
  8212. -----------------------------------------------------------------------------*/
  8213. function buildToolbars() {
  8214. return [
  8215. new Toolbar(t, computeHeaderOptions()),
  8216. new Toolbar(t, computeFooterOptions())
  8217. ];
  8218. }
  8219. function computeHeaderOptions() {
  8220. return {
  8221. extraClasses: 'fc-header-toolbar',
  8222. layout: t.options.header
  8223. };
  8224. }
  8225. function computeFooterOptions() {
  8226. return {
  8227. extraClasses: 'fc-footer-toolbar',
  8228. layout: t.options.footer
  8229. };
  8230. }
  8231. // can be called repeatedly and Header will rerender
  8232. function renderHeader() {
  8233. header.setToolbarOptions(computeHeaderOptions());
  8234. header.render();
  8235. if (header.el) {
  8236. element.prepend(header.el);
  8237. }
  8238. }
  8239. // can be called repeatedly and Footer will rerender
  8240. function renderFooter() {
  8241. footer.setToolbarOptions(computeFooterOptions());
  8242. footer.render();
  8243. if (footer.el) {
  8244. element.append(footer.el);
  8245. }
  8246. }
  8247. t.setToolbarsTitle = function(title) {
  8248. toolbarsManager.proxyCall('updateTitle', title);
  8249. };
  8250. function updateToolbarsTodayButton() {
  8251. var now = t.getNow();
  8252. if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
  8253. toolbarsManager.proxyCall('disableButton', 'today');
  8254. }
  8255. else {
  8256. toolbarsManager.proxyCall('enableButton', 'today');
  8257. }
  8258. }
  8259. /* Selection
  8260. -----------------------------------------------------------------------------*/
  8261. // this public method receives start/end dates in any format, with any timezone
  8262. function select(zonedStartInput, zonedEndInput) {
  8263. currentView.select(
  8264. t.buildSelectSpan.apply(t, arguments)
  8265. );
  8266. }
  8267. function unselect() { // safe to be called before renderView
  8268. if (currentView) {
  8269. currentView.unselect();
  8270. }
  8271. }
  8272. /* Date
  8273. -----------------------------------------------------------------------------*/
  8274. function prev() {
  8275. date = currentView.computePrevDate(date);
  8276. renderView();
  8277. }
  8278. function next() {
  8279. date = currentView.computeNextDate(date);
  8280. renderView();
  8281. }
  8282. function prevYear() {
  8283. date.add(-1, 'years');
  8284. renderView();
  8285. }
  8286. function nextYear() {
  8287. date.add(1, 'years');
  8288. renderView();
  8289. }
  8290. function today() {
  8291. date = t.getNow();
  8292. renderView();
  8293. }
  8294. function gotoDate(zonedDateInput) {
  8295. date = t.moment(zonedDateInput).stripZone();
  8296. renderView();
  8297. }
  8298. function incrementDate(delta) {
  8299. date.add(moment.duration(delta));
  8300. renderView();
  8301. }
  8302. // Forces navigation to a view for the given date.
  8303. // `viewType` can be a specific view name or a generic one like "week" or "day".
  8304. function zoomTo(newDate, viewType) {
  8305. var spec;
  8306. viewType = viewType || 'day'; // day is default zoom
  8307. spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  8308. date = newDate.clone();
  8309. renderView(spec ? spec.type : null);
  8310. }
  8311. // for external API
  8312. function getDate() {
  8313. return t.applyTimezone(date); // infuse the calendar's timezone
  8314. }
  8315. /* Height "Freezing"
  8316. -----------------------------------------------------------------------------*/
  8317. t.freezeContentHeight = freezeContentHeight;
  8318. t.thawContentHeight = thawContentHeight;
  8319. var freezeContentHeightDepth = 0;
  8320. function freezeContentHeight() {
  8321. if (!(freezeContentHeightDepth++)) {
  8322. content.css({
  8323. width: '100%',
  8324. height: content.height(),
  8325. overflow: 'hidden'
  8326. });
  8327. }
  8328. }
  8329. function thawContentHeight() {
  8330. if (!(--freezeContentHeightDepth)) {
  8331. content.css({
  8332. width: '',
  8333. height: '',
  8334. overflow: ''
  8335. });
  8336. }
  8337. }
  8338. /* Misc
  8339. -----------------------------------------------------------------------------*/
  8340. function getCalendar() {
  8341. return t;
  8342. }
  8343. function getView() {
  8344. return currentView;
  8345. }
  8346. function option(name, value) {
  8347. var newOptionHash;
  8348. if (typeof name === 'string') {
  8349. if (value === undefined) { // getter
  8350. return t.options[name];
  8351. }
  8352. else { // setter for individual option
  8353. newOptionHash = {};
  8354. newOptionHash[name] = value;
  8355. setOptions(newOptionHash);
  8356. }
  8357. }
  8358. else if (typeof name === 'object') { // compound setter with object input
  8359. setOptions(name);
  8360. }
  8361. }
  8362. function setOptions(newOptionHash) {
  8363. var optionCnt = 0;
  8364. var optionName;
  8365. for (optionName in newOptionHash) {
  8366. t.dynamicOverrides[optionName] = newOptionHash[optionName];
  8367. }
  8368. t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
  8369. t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
  8370. // trigger handlers after this.options has been updated
  8371. for (optionName in newOptionHash) {
  8372. t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
  8373. optionCnt++;
  8374. }
  8375. // special-case handling of single option change.
  8376. // if only one option change, `optionName` will be its name.
  8377. if (optionCnt === 1) {
  8378. if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
  8379. updateSize(true); // true = allow recalculation of height
  8380. return;
  8381. }
  8382. else if (optionName === 'defaultDate') {
  8383. return; // can't change date this way. use gotoDate instead
  8384. }
  8385. else if (optionName === 'businessHours') {
  8386. if (currentView) {
  8387. currentView.unrenderBusinessHours();
  8388. currentView.renderBusinessHours();
  8389. }
  8390. return;
  8391. }
  8392. else if (optionName === 'timezone') {
  8393. t.rezoneArrayEventSources();
  8394. t.refetchEvents();
  8395. return;
  8396. }
  8397. }
  8398. // catch-all. rerender the header and footer and rebuild/rerender the current view
  8399. renderHeader();
  8400. renderFooter();
  8401. viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
  8402. reinitView();
  8403. }
  8404. function publiclyTrigger(name, thisObj) {
  8405. var args = Array.prototype.slice.call(arguments, 2);
  8406. thisObj = thisObj || _element;
  8407. this.triggerWith(name, thisObj, args); // Emitter's method
  8408. if (t.options[name]) {
  8409. return t.options[name].apply(thisObj, args);
  8410. }
  8411. }
  8412. t.initialize();
  8413. }
  8414. ;;
  8415. /*
  8416. Options binding/triggering system.
  8417. */
  8418. Calendar.mixin({
  8419. // A map of option names to arrays of handler objects. Initialized to {} in Calendar.
  8420. // Format for a handler object:
  8421. // {
  8422. // func // callback function to be called upon change
  8423. // names // option names whose values should be given to func
  8424. // }
  8425. optionHandlers: null,
  8426. // Calls handlerFunc immediately, and when the given option has changed.
  8427. // handlerFunc will be given the option value.
  8428. bindOption: function(optionName, handlerFunc) {
  8429. this.bindOptions([ optionName ], handlerFunc);
  8430. },
  8431. // Calls handlerFunc immediately, and when any of the given options change.
  8432. // handlerFunc will be given each option value as ordered function arguments.
  8433. bindOptions: function(optionNames, handlerFunc) {
  8434. var handlerObj = { func: handlerFunc, names: optionNames };
  8435. var i;
  8436. for (i = 0; i < optionNames.length; i++) {
  8437. this.registerOptionHandlerObj(optionNames[i], handlerObj);
  8438. }
  8439. this.triggerOptionHandlerObj(handlerObj);
  8440. },
  8441. // Puts the given handler object into the internal hash
  8442. registerOptionHandlerObj: function(optionName, handlerObj) {
  8443. (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = []))
  8444. .push(handlerObj);
  8445. },
  8446. // Reports that the given option has changed, and calls all appropriate handlers.
  8447. triggerOptionHandlers: function(optionName) {
  8448. var handlerObjs = this.optionHandlers[optionName] || [];
  8449. var i;
  8450. for (i = 0; i < handlerObjs.length; i++) {
  8451. this.triggerOptionHandlerObj(handlerObjs[i]);
  8452. }
  8453. },
  8454. // Calls the callback for a specific handler object, passing in the appropriate arguments.
  8455. triggerOptionHandlerObj: function(handlerObj) {
  8456. var optionNames = handlerObj.names;
  8457. var optionValues = [];
  8458. var i;
  8459. for (i = 0; i < optionNames.length; i++) {
  8460. optionValues.push(this.options[optionNames[i]]);
  8461. }
  8462. handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context
  8463. }
  8464. });
  8465. ;;
  8466. Calendar.defaults = {
  8467. titleRangeSeparator: ' \u2013 ', // en dash
  8468. monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
  8469. defaultTimedEventDuration: '02:00:00',
  8470. defaultAllDayEventDuration: { days: 1 },
  8471. forceEventDuration: false,
  8472. nextDayThreshold: '09:00:00', // 9am
  8473. // display
  8474. defaultView: 'month',
  8475. aspectRatio: 1.35,
  8476. header: {
  8477. left: 'title',
  8478. center: '',
  8479. right: 'today prev,next'
  8480. },
  8481. weekends: true,
  8482. weekNumbers: false,
  8483. weekNumberTitle: 'W',
  8484. weekNumberCalculation: 'local',
  8485. //editable: false,
  8486. //nowIndicator: false,
  8487. scrollTime: '06:00:00',
  8488. // event ajax
  8489. lazyFetching: true,
  8490. startParam: 'start',
  8491. endParam: 'end',
  8492. timezoneParam: 'timezone',
  8493. timezone: false,
  8494. //allDayDefault: undefined,
  8495. // locale
  8496. isRTL: false,
  8497. buttonText: {
  8498. prev: "prev",
  8499. next: "next",
  8500. prevYear: "prev year",
  8501. nextYear: "next year",
  8502. year: 'year', // TODO: locale files need to specify this
  8503. today: 'today',
  8504. month: 'month',
  8505. week: 'week',
  8506. day: 'day'
  8507. },
  8508. buttonIcons: {
  8509. prev: 'left-single-arrow',
  8510. next: 'right-single-arrow',
  8511. prevYear: 'left-double-arrow',
  8512. nextYear: 'right-double-arrow'
  8513. },
  8514. allDayText: 'all-day',
  8515. // jquery-ui theming
  8516. theme: false,
  8517. themeButtonIcons: {
  8518. prev: 'circle-triangle-w',
  8519. next: 'circle-triangle-e',
  8520. prevYear: 'seek-prev',
  8521. nextYear: 'seek-next'
  8522. },
  8523. //eventResizableFromStart: false,
  8524. dragOpacity: .75,
  8525. dragRevertDuration: 500,
  8526. dragScroll: true,
  8527. //selectable: false,
  8528. unselectAuto: true,
  8529. dropAccept: '*',
  8530. eventOrder: 'title',
  8531. //eventRenderWait: null,
  8532. eventLimit: false,
  8533. eventLimitText: 'more',
  8534. eventLimitClick: 'popover',
  8535. dayPopoverFormat: 'LL',
  8536. handleWindowResize: true,
  8537. windowResizeDelay: 100, // milliseconds before an updateSize happens
  8538. longPressDelay: 1000
  8539. };
  8540. Calendar.englishDefaults = { // used by locale.js
  8541. dayPopoverFormat: 'dddd, MMMM D'
  8542. };
  8543. Calendar.rtlDefaults = { // right-to-left defaults
  8544. header: { // TODO: smarter solution (first/center/last ?)
  8545. left: 'next,prev today',
  8546. center: '',
  8547. right: 'title'
  8548. },
  8549. buttonIcons: {
  8550. prev: 'right-single-arrow',
  8551. next: 'left-single-arrow',
  8552. prevYear: 'right-double-arrow',
  8553. nextYear: 'left-double-arrow'
  8554. },
  8555. themeButtonIcons: {
  8556. prev: 'circle-triangle-e',
  8557. next: 'circle-triangle-w',
  8558. nextYear: 'seek-prev',
  8559. prevYear: 'seek-next'
  8560. }
  8561. };
  8562. ;;
  8563. var localeOptionHash = FC.locales = {}; // initialize and expose
  8564. // TODO: document the structure and ordering of a FullCalendar locale file
  8565. // Initialize jQuery UI datepicker translations while using some of the translations
  8566. // Will set this as the default locales for datepicker.
  8567. FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
  8568. // get the FullCalendar internal option hash for this locale. create if necessary
  8569. var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
  8570. // transfer some simple options from datepicker to fc
  8571. fcOptions.isRTL = dpOptions.isRTL;
  8572. fcOptions.weekNumberTitle = dpOptions.weekHeader;
  8573. // compute some more complex options from datepicker
  8574. $.each(dpComputableOptions, function(name, func) {
  8575. fcOptions[name] = func(dpOptions);
  8576. });
  8577. // is jQuery UI Datepicker is on the page?
  8578. if ($.datepicker) {
  8579. // Register the locale data.
  8580. // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
  8581. // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
  8582. // Make an alias so the locale can be referenced either way.
  8583. $.datepicker.regional[dpLocaleCode] =
  8584. $.datepicker.regional[localeCode] = // alias
  8585. dpOptions;
  8586. // Alias 'en' to the default locale data. Do this every time.
  8587. $.datepicker.regional.en = $.datepicker.regional[''];
  8588. // Set as Datepicker's global defaults.
  8589. $.datepicker.setDefaults(dpOptions);
  8590. }
  8591. };
  8592. // Sets FullCalendar-specific translations. Will set the locales as the global default.
  8593. FC.locale = function(localeCode, newFcOptions) {
  8594. var fcOptions;
  8595. var momOptions;
  8596. // get the FullCalendar internal option hash for this locale. create if necessary
  8597. fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
  8598. // provided new options for this locales? merge them in
  8599. if (newFcOptions) {
  8600. fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]);
  8601. }
  8602. // compute locale options that weren't defined.
  8603. // always do this. newFcOptions can be undefined when initializing from i18n file,
  8604. // so no way to tell if this is an initialization or a default-setting.
  8605. momOptions = getMomentLocaleData(localeCode); // will fall back to en
  8606. $.each(momComputableOptions, function(name, func) {
  8607. if (fcOptions[name] == null) {
  8608. fcOptions[name] = func(momOptions, fcOptions);
  8609. }
  8610. });
  8611. // set it as the default locale for FullCalendar
  8612. Calendar.defaults.locale = localeCode;
  8613. };
  8614. // NOTE: can't guarantee any of these computations will run because not every locale has datepicker
  8615. // configs, so make sure there are English fallbacks for these in the defaults file.
  8616. var dpComputableOptions = {
  8617. buttonText: function(dpOptions) {
  8618. return {
  8619. // the translations sometimes wrongly contain HTML entities
  8620. prev: stripHtmlEntities(dpOptions.prevText),
  8621. next: stripHtmlEntities(dpOptions.nextText),
  8622. today: stripHtmlEntities(dpOptions.currentText)
  8623. };
  8624. },
  8625. // Produces format strings like "MMMM YYYY" -> "September 2014"
  8626. monthYearFormat: function(dpOptions) {
  8627. return dpOptions.showMonthAfterYear ?
  8628. 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
  8629. 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
  8630. }
  8631. };
  8632. var momComputableOptions = {
  8633. // Produces format strings like "ddd M/D" -> "Fri 9/15"
  8634. dayOfMonthFormat: function(momOptions, fcOptions) {
  8635. var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
  8636. // strip the year off the edge, as well as other misc non-whitespace chars
  8637. format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
  8638. if (fcOptions.isRTL) {
  8639. format += ' ddd'; // for RTL, add day-of-week to end
  8640. }
  8641. else {
  8642. format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  8643. }
  8644. return format;
  8645. },
  8646. // Produces format strings like "h:mma" -> "6:00pm"
  8647. mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
  8648. return momOptions.longDateFormat('LT')
  8649. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8650. },
  8651. // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
  8652. smallTimeFormat: function(momOptions) {
  8653. return momOptions.longDateFormat('LT')
  8654. .replace(':mm', '(:mm)')
  8655. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
  8656. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8657. },
  8658. // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
  8659. extraSmallTimeFormat: function(momOptions) {
  8660. return momOptions.longDateFormat('LT')
  8661. .replace(':mm', '(:mm)')
  8662. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
  8663. .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  8664. },
  8665. // Produces format strings like "ha" / "H" -> "6pm" / "18"
  8666. hourFormat: function(momOptions) {
  8667. return momOptions.longDateFormat('LT')
  8668. .replace(':mm', '')
  8669. .replace(/(\Wmm)$/, '') // like above, but for foreign locales
  8670. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8671. },
  8672. // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
  8673. noMeridiemTimeFormat: function(momOptions) {
  8674. return momOptions.longDateFormat('LT')
  8675. .replace(/\s*a$/i, ''); // remove trailing AM/PM
  8676. }
  8677. };
  8678. // options that should be computed off live calendar options (considers override options)
  8679. // TODO: best place for this? related to locale?
  8680. // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
  8681. var instanceComputableOptions = {
  8682. // Produces format strings for results like "Mo 16"
  8683. smallDayDateFormat: function(options) {
  8684. return options.isRTL ?
  8685. 'D dd' :
  8686. 'dd D';
  8687. },
  8688. // Produces format strings for results like "Wk 5"
  8689. weekFormat: function(options) {
  8690. return options.isRTL ?
  8691. 'w[ ' + options.weekNumberTitle + ']' :
  8692. '[' + options.weekNumberTitle + ' ]w';
  8693. },
  8694. // Produces format strings for results like "Wk5"
  8695. smallWeekFormat: function(options) {
  8696. return options.isRTL ?
  8697. 'w[' + options.weekNumberTitle + ']' :
  8698. '[' + options.weekNumberTitle + ']w';
  8699. }
  8700. };
  8701. function populateInstanceComputableOptions(options) {
  8702. $.each(instanceComputableOptions, function(name, func) {
  8703. if (options[name] == null) {
  8704. options[name] = func(options);
  8705. }
  8706. });
  8707. }
  8708. // Returns moment's internal locale data. If doesn't exist, returns English.
  8709. function getMomentLocaleData(localeCode) {
  8710. return moment.localeData(localeCode) || moment.localeData('en');
  8711. }
  8712. // Initialize English by forcing computation of moment-derived options.
  8713. // Also, sets it as the default.
  8714. FC.locale('en', Calendar.englishDefaults);
  8715. ;;
  8716. FC.sourceNormalizers = [];
  8717. FC.sourceFetchers = [];
  8718. var ajaxDefaults = {
  8719. dataType: 'json',
  8720. cache: false
  8721. };
  8722. var eventGUID = 1;
  8723. function EventManager() { // assumed to be a calendar
  8724. var t = this;
  8725. // exports
  8726. t.requestEvents = requestEvents;
  8727. t.reportEventChange = reportEventChange;
  8728. t.isFetchNeeded = isFetchNeeded;
  8729. t.fetchEvents = fetchEvents;
  8730. t.fetchEventSources = fetchEventSources;
  8731. t.refetchEvents = refetchEvents;
  8732. t.refetchEventSources = refetchEventSources;
  8733. t.getEventSources = getEventSources;
  8734. t.getEventSourceById = getEventSourceById;
  8735. t.addEventSource = addEventSource;
  8736. t.removeEventSource = removeEventSource;
  8737. t.removeEventSources = removeEventSources;
  8738. t.updateEvent = updateEvent;
  8739. t.updateEvents = updateEvents;
  8740. t.renderEvent = renderEvent;
  8741. t.renderEvents = renderEvents;
  8742. t.removeEvents = removeEvents;
  8743. t.clientEvents = clientEvents;
  8744. t.mutateEvent = mutateEvent;
  8745. t.normalizeEventDates = normalizeEventDates;
  8746. t.normalizeEventTimes = normalizeEventTimes;
  8747. // locals
  8748. var stickySource = { events: [] };
  8749. var sources = [ stickySource ];
  8750. var rangeStart, rangeEnd;
  8751. var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
  8752. var cache = []; // holds events that have already been expanded
  8753. var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd
  8754. $.each(
  8755. (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []),
  8756. function(i, sourceInput) {
  8757. var source = buildEventSource(sourceInput);
  8758. if (source) {
  8759. sources.push(source);
  8760. }
  8761. }
  8762. );
  8763. function requestEvents(start, end) {
  8764. if (!t.options.lazyFetching || isFetchNeeded(start, end)) {
  8765. return fetchEvents(start, end);
  8766. }
  8767. else {
  8768. return Promise.resolve(prunedCache);
  8769. }
  8770. }
  8771. function reportEventChange() {
  8772. prunedCache = filterEventsWithinRange(cache);
  8773. t.trigger('eventsReset', prunedCache);
  8774. }
  8775. function filterEventsWithinRange(events) {
  8776. var filteredEvents = [];
  8777. var i, event;
  8778. for (i = 0; i < events.length; i++) {
  8779. event = events[i];
  8780. if (
  8781. event.start.clone().stripZone() < rangeEnd &&
  8782. t.getEventEnd(event).stripZone() > rangeStart
  8783. ) {
  8784. filteredEvents.push(event);
  8785. }
  8786. }
  8787. return filteredEvents;
  8788. }
  8789. t.getEventCache = function() {
  8790. return cache;
  8791. };
  8792. t.getPrunedEventCache = function() {
  8793. return prunedCache;
  8794. };
  8795. /* Fetching
  8796. -----------------------------------------------------------------------------*/
  8797. // start and end are assumed to be unzoned
  8798. function isFetchNeeded(start, end) {
  8799. return !rangeStart || // nothing has been fetched yet?
  8800. start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
  8801. }
  8802. function fetchEvents(start, end) {
  8803. rangeStart = start;
  8804. rangeEnd = end;
  8805. return refetchEvents();
  8806. }
  8807. // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`.
  8808. function refetchEvents() {
  8809. return fetchEventSources(sources, 'reset');
  8810. }
  8811. // poorly named. fetches a subset of event sources.
  8812. function refetchEventSources(matchInputs) {
  8813. return fetchEventSources(getEventSourcesByMatchArray(matchInputs));
  8814. }
  8815. // expects an array of event source objects (the originals, not copies)
  8816. // `specialFetchType` is an optimization parameter that affects purging of the event cache.
  8817. function fetchEventSources(specificSources, specialFetchType) {
  8818. var i, source;
  8819. if (specialFetchType === 'reset') {
  8820. cache = [];
  8821. }
  8822. else if (specialFetchType !== 'add') {
  8823. cache = excludeEventsBySources(cache, specificSources);
  8824. }
  8825. for (i = 0; i < specificSources.length; i++) {
  8826. source = specificSources[i];
  8827. // already-pending sources have already been accounted for in pendingSourceCnt
  8828. if (source._status !== 'pending') {
  8829. pendingSourceCnt++;
  8830. }
  8831. source._fetchId = (source._fetchId || 0) + 1;
  8832. source._status = 'pending';
  8833. }
  8834. for (i = 0; i < specificSources.length; i++) {
  8835. source = specificSources[i];
  8836. tryFetchEventSource(source, source._fetchId);
  8837. }
  8838. if (pendingSourceCnt) {
  8839. return new Promise(function(resolve) {
  8840. t.one('eventsReceived', resolve); // will send prunedCache
  8841. });
  8842. }
  8843. else { // executed all synchronously, or no sources at all
  8844. return Promise.resolve(prunedCache);
  8845. }
  8846. }
  8847. // fetches an event source and processes its result ONLY if it is still the current fetch.
  8848. // caller is responsible for incrementing pendingSourceCnt first.
  8849. function tryFetchEventSource(source, fetchId) {
  8850. _fetchEventSource(source, function(eventInputs) {
  8851. var isArraySource = $.isArray(source.events);
  8852. var i, eventInput;
  8853. var abstractEvent;
  8854. if (
  8855. // is this the source's most recent fetch?
  8856. // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
  8857. fetchId === source._fetchId &&
  8858. // event source no longer valid?
  8859. source._status !== 'rejected'
  8860. ) {
  8861. source._status = 'resolved';
  8862. if (eventInputs) {
  8863. for (i = 0; i < eventInputs.length; i++) {
  8864. eventInput = eventInputs[i];
  8865. if (isArraySource) { // array sources have already been convert to Event Objects
  8866. abstractEvent = eventInput;
  8867. }
  8868. else {
  8869. abstractEvent = buildEventFromInput(eventInput, source);
  8870. }
  8871. if (abstractEvent) { // not false (an invalid event)
  8872. cache.push.apply( // append
  8873. cache,
  8874. expandEvent(abstractEvent) // add individual expanded events to the cache
  8875. );
  8876. }
  8877. }
  8878. }
  8879. decrementPendingSourceCnt();
  8880. }
  8881. });
  8882. }
  8883. function rejectEventSource(source) {
  8884. var wasPending = source._status === 'pending';
  8885. source._status = 'rejected';
  8886. if (wasPending) {
  8887. decrementPendingSourceCnt();
  8888. }
  8889. }
  8890. function decrementPendingSourceCnt() {
  8891. pendingSourceCnt--;
  8892. if (!pendingSourceCnt) {
  8893. reportEventChange(cache); // updates prunedCache
  8894. t.trigger('eventsReceived', prunedCache);
  8895. }
  8896. }
  8897. function _fetchEventSource(source, callback) {
  8898. var i;
  8899. var fetchers = FC.sourceFetchers;
  8900. var res;
  8901. for (i=0; i<fetchers.length; i++) {
  8902. res = fetchers[i].call(
  8903. t, // this, the Calendar object
  8904. source,
  8905. rangeStart.clone(),
  8906. rangeEnd.clone(),
  8907. t.options.timezone,
  8908. callback
  8909. );
  8910. if (res === true) {
  8911. // the fetcher is in charge. made its own async request
  8912. return;
  8913. }
  8914. else if (typeof res == 'object') {
  8915. // the fetcher returned a new source. process it
  8916. _fetchEventSource(res, callback);
  8917. return;
  8918. }
  8919. }
  8920. var events = source.events;
  8921. if (events) {
  8922. if ($.isFunction(events)) {
  8923. t.pushLoading();
  8924. events.call(
  8925. t, // this, the Calendar object
  8926. rangeStart.clone(),
  8927. rangeEnd.clone(),
  8928. t.options.timezone,
  8929. function(events) {
  8930. callback(events);
  8931. t.popLoading();
  8932. }
  8933. );
  8934. }
  8935. else if ($.isArray(events)) {
  8936. callback(events);
  8937. }
  8938. else {
  8939. callback();
  8940. }
  8941. }else{
  8942. var url = source.url;
  8943. if (url) {
  8944. var success = source.success;
  8945. var error = source.error;
  8946. var complete = source.complete;
  8947. // retrieve any outbound GET/POST $.ajax data from the options
  8948. var customData;
  8949. if ($.isFunction(source.data)) {
  8950. // supplied as a function that returns a key/value object
  8951. customData = source.data();
  8952. }
  8953. else {
  8954. // supplied as a straight key/value object
  8955. customData = source.data;
  8956. }
  8957. // use a copy of the custom data so we can modify the parameters
  8958. // and not affect the passed-in object.
  8959. var data = $.extend({}, customData || {});
  8960. var startParam = firstDefined(source.startParam, t.options.startParam);
  8961. var endParam = firstDefined(source.endParam, t.options.endParam);
  8962. var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
  8963. if (startParam) {
  8964. data[startParam] = rangeStart.format();
  8965. }
  8966. if (endParam) {
  8967. data[endParam] = rangeEnd.format();
  8968. }
  8969. if (t.options.timezone && t.options.timezone != 'local') {
  8970. data[timezoneParam] = t.options.timezone;
  8971. }
  8972. t.pushLoading();
  8973. $.ajax($.extend({}, ajaxDefaults, source, {
  8974. data: data,
  8975. success: function(events) {
  8976. events = events || [];
  8977. var res = applyAll(success, this, arguments);
  8978. if ($.isArray(res)) {
  8979. events = res;
  8980. }
  8981. callback(events);
  8982. },
  8983. error: function() {
  8984. applyAll(error, this, arguments);
  8985. callback();
  8986. },
  8987. complete: function() {
  8988. applyAll(complete, this, arguments);
  8989. t.popLoading();
  8990. }
  8991. }));
  8992. }else{
  8993. callback();
  8994. }
  8995. }
  8996. }
  8997. /* Sources
  8998. -----------------------------------------------------------------------------*/
  8999. function addEventSource(sourceInput) {
  9000. var source = buildEventSource(sourceInput);
  9001. if (source) {
  9002. sources.push(source);
  9003. fetchEventSources([ source ], 'add'); // will eventually call reportEventChange
  9004. }
  9005. }
  9006. function buildEventSource(sourceInput) { // will return undefined if invalid source
  9007. var normalizers = FC.sourceNormalizers;
  9008. var source;
  9009. var i;
  9010. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  9011. source = { events: sourceInput };
  9012. }
  9013. else if (typeof sourceInput === 'string') {
  9014. source = { url: sourceInput };
  9015. }
  9016. else if (typeof sourceInput === 'object') {
  9017. source = $.extend({}, sourceInput); // shallow copy
  9018. }
  9019. if (source) {
  9020. // TODO: repeat code, same code for event classNames
  9021. if (source.className) {
  9022. if (typeof source.className === 'string') {
  9023. source.className = source.className.split(/\s+/);
  9024. }
  9025. // otherwise, assumed to be an array
  9026. }
  9027. else {
  9028. source.className = [];
  9029. }
  9030. // for array sources, we convert to standard Event Objects up front
  9031. if ($.isArray(source.events)) {
  9032. source.origArray = source.events; // for removeEventSource
  9033. source.events = $.map(source.events, function(eventInput) {
  9034. return buildEventFromInput(eventInput, source);
  9035. });
  9036. }
  9037. for (i=0; i<normalizers.length; i++) {
  9038. normalizers[i].call(t, source);
  9039. }
  9040. return source;
  9041. }
  9042. }
  9043. function removeEventSource(matchInput) {
  9044. removeSpecificEventSources(
  9045. getEventSourcesByMatch(matchInput)
  9046. );
  9047. }
  9048. // if called with no arguments, removes all.
  9049. function removeEventSources(matchInputs) {
  9050. if (matchInputs == null) {
  9051. removeSpecificEventSources(sources, true); // isAll=true
  9052. }
  9053. else {
  9054. removeSpecificEventSources(
  9055. getEventSourcesByMatchArray(matchInputs)
  9056. );
  9057. }
  9058. }
  9059. function removeSpecificEventSources(targetSources, isAll) {
  9060. var i;
  9061. // cancel pending requests
  9062. for (i = 0; i < targetSources.length; i++) {
  9063. rejectEventSource(targetSources[i]);
  9064. }
  9065. if (isAll) { // an optimization
  9066. sources = [];
  9067. cache = [];
  9068. }
  9069. else {
  9070. // remove from persisted source list
  9071. sources = $.grep(sources, function(source) {
  9072. for (i = 0; i < targetSources.length; i++) {
  9073. if (source === targetSources[i]) {
  9074. return false; // exclude
  9075. }
  9076. }
  9077. return true; // include
  9078. });
  9079. cache = excludeEventsBySources(cache, targetSources);
  9080. }
  9081. reportEventChange();
  9082. }
  9083. function getEventSources() {
  9084. return sources.slice(1); // returns a shallow copy of sources with stickySource removed
  9085. }
  9086. function getEventSourceById(id) {
  9087. return $.grep(sources, function(source) {
  9088. return source.id && source.id === id;
  9089. })[0];
  9090. }
  9091. // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
  9092. function getEventSourcesByMatchArray(matchInputs) {
  9093. // coerce into an array
  9094. if (!matchInputs) {
  9095. matchInputs = [];
  9096. }
  9097. else if (!$.isArray(matchInputs)) {
  9098. matchInputs = [ matchInputs ];
  9099. }
  9100. var matchingSources = [];
  9101. var i;
  9102. // resolve raw inputs to real event source objects
  9103. for (i = 0; i < matchInputs.length; i++) {
  9104. matchingSources.push.apply( // append
  9105. matchingSources,
  9106. getEventSourcesByMatch(matchInputs[i])
  9107. );
  9108. }
  9109. return matchingSources;
  9110. }
  9111. // matchInput can either by a real event source object, an ID, or the function/URL for the source.
  9112. // returns an array of matching source objects.
  9113. function getEventSourcesByMatch(matchInput) {
  9114. var i, source;
  9115. // given an proper event source object
  9116. for (i = 0; i < sources.length; i++) {
  9117. source = sources[i];
  9118. if (source === matchInput) {
  9119. return [ source ];
  9120. }
  9121. }
  9122. // an ID match
  9123. source = getEventSourceById(matchInput);
  9124. if (source) {
  9125. return [ source ];
  9126. }
  9127. return $.grep(sources, function(source) {
  9128. return isSourcesEquivalent(matchInput, source);
  9129. });
  9130. }
  9131. function isSourcesEquivalent(source1, source2) {
  9132. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  9133. }
  9134. function getSourcePrimitive(source) {
  9135. return (
  9136. (typeof source === 'object') ? // a normalized event source?
  9137. (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  9138. null
  9139. ) ||
  9140. source; // the given argument *is* the primitive
  9141. }
  9142. // util
  9143. // returns a filtered array without events that are part of any of the given sources
  9144. function excludeEventsBySources(specificEvents, specificSources) {
  9145. return $.grep(specificEvents, function(event) {
  9146. for (var i = 0; i < specificSources.length; i++) {
  9147. if (event.source === specificSources[i]) {
  9148. return false; // exclude
  9149. }
  9150. }
  9151. return true; // keep
  9152. });
  9153. }
  9154. /* Manipulation
  9155. -----------------------------------------------------------------------------*/
  9156. // Only ever called from the externally-facing API
  9157. function updateEvent(event) {
  9158. updateEvents([ event ]);
  9159. }
  9160. // Only ever called from the externally-facing API
  9161. function updateEvents(events) {
  9162. var i, event;
  9163. for (i = 0; i < events.length; i++) {
  9164. event = events[i];
  9165. // massage start/end values, even if date string values
  9166. event.start = t.moment(event.start);
  9167. if (event.end) {
  9168. event.end = t.moment(event.end);
  9169. }
  9170. else {
  9171. event.end = null;
  9172. }
  9173. mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  9174. }
  9175. reportEventChange(); // reports event modifications (so we can redraw)
  9176. }
  9177. // Returns a hash of misc event properties that should be copied over to related events.
  9178. function getMiscEventProps(event) {
  9179. var props = {};
  9180. $.each(event, function(name, val) {
  9181. if (isMiscEventPropName(name)) {
  9182. if (val !== undefined && isAtomic(val)) { // a defined non-object
  9183. props[name] = val;
  9184. }
  9185. }
  9186. });
  9187. return props;
  9188. }
  9189. // non-date-related, non-id-related, non-secret
  9190. function isMiscEventPropName(name) {
  9191. return !/^_|^(id|allDay|start|end)$/.test(name);
  9192. }
  9193. // returns the expanded events that were created
  9194. function renderEvent(eventInput, stick) {
  9195. return renderEvents([ eventInput ], stick);
  9196. }
  9197. // returns the expanded events that were created
  9198. function renderEvents(eventInputs, stick) {
  9199. var renderedEvents = [];
  9200. var renderableEvents;
  9201. var abstractEvent;
  9202. var i, j, event;
  9203. for (i = 0; i < eventInputs.length; i++) {
  9204. abstractEvent = buildEventFromInput(eventInputs[i]);
  9205. if (abstractEvent) { // not false (a valid input)
  9206. renderableEvents = expandEvent(abstractEvent);
  9207. for (j = 0; j < renderableEvents.length; j++) {
  9208. event = renderableEvents[j];
  9209. if (!event.source) {
  9210. if (stick) {
  9211. stickySource.events.push(event);
  9212. event.source = stickySource;
  9213. }
  9214. cache.push(event);
  9215. }
  9216. }
  9217. renderedEvents = renderedEvents.concat(renderableEvents);
  9218. }
  9219. }
  9220. if (renderedEvents.length) { // any new events rendered?
  9221. reportEventChange();
  9222. }
  9223. return renderedEvents;
  9224. }
  9225. function removeEvents(filter) {
  9226. var eventID;
  9227. var i;
  9228. if (filter == null) { // null or undefined. remove all events
  9229. filter = function() { return true; }; // will always match
  9230. }
  9231. else if (!$.isFunction(filter)) { // an event ID
  9232. eventID = filter + '';
  9233. filter = function(event) {
  9234. return event._id == eventID;
  9235. };
  9236. }
  9237. // Purge event(s) from our local cache
  9238. cache = $.grep(cache, filter, true); // inverse=true
  9239. // Remove events from array sources.
  9240. // This works because they have been converted to official Event Objects up front.
  9241. // (and as a result, event._id has been calculated).
  9242. for (i=0; i<sources.length; i++) {
  9243. if ($.isArray(sources[i].events)) {
  9244. sources[i].events = $.grep(sources[i].events, filter, true);
  9245. }
  9246. }
  9247. reportEventChange();
  9248. }
  9249. function clientEvents(filter) {
  9250. if ($.isFunction(filter)) {
  9251. return $.grep(cache, filter);
  9252. }
  9253. else if (filter != null) { // not null, not undefined. an event ID
  9254. filter += '';
  9255. return $.grep(cache, function(e) {
  9256. return e._id == filter;
  9257. });
  9258. }
  9259. return cache; // else, return all
  9260. }
  9261. // Makes sure all array event sources have their internal event objects
  9262. // converted over to the Calendar's current timezone.
  9263. t.rezoneArrayEventSources = function() {
  9264. var i;
  9265. var events;
  9266. var j;
  9267. for (i = 0; i < sources.length; i++) {
  9268. events = sources[i].events;
  9269. if ($.isArray(events)) {
  9270. for (j = 0; j < events.length; j++) {
  9271. rezoneEventDates(events[j]);
  9272. }
  9273. }
  9274. }
  9275. };
  9276. function rezoneEventDates(event) {
  9277. event.start = t.moment(event.start);
  9278. if (event.end) {
  9279. event.end = t.moment(event.end);
  9280. }
  9281. backupEventDates(event);
  9282. }
  9283. /* Event Normalization
  9284. -----------------------------------------------------------------------------*/
  9285. // Given a raw object with key/value properties, returns an "abstract" Event object.
  9286. // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  9287. // Will return `false` when input is invalid.
  9288. // `source` is optional
  9289. function buildEventFromInput(input, source) {
  9290. var out = {};
  9291. var start, end;
  9292. var allDay;
  9293. if (t.options.eventDataTransform) {
  9294. input = t.options.eventDataTransform(input);
  9295. }
  9296. if (source && source.eventDataTransform) {
  9297. input = source.eventDataTransform(input);
  9298. }
  9299. // Copy all properties over to the resulting object.
  9300. // The special-case properties will be copied over afterwards.
  9301. $.extend(out, input);
  9302. if (source) {
  9303. out.source = source;
  9304. }
  9305. out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  9306. if (input.className) {
  9307. if (typeof input.className == 'string') {
  9308. out.className = input.className.split(/\s+/);
  9309. }
  9310. else { // assumed to be an array
  9311. out.className = input.className;
  9312. }
  9313. }
  9314. else {
  9315. out.className = [];
  9316. }
  9317. start = input.start || input.date; // "date" is an alias for "start"
  9318. end = input.end;
  9319. // parse as a time (Duration) if applicable
  9320. if (isTimeString(start)) {
  9321. start = moment.duration(start);
  9322. }
  9323. if (isTimeString(end)) {
  9324. end = moment.duration(end);
  9325. }
  9326. if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  9327. // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  9328. out.start = start ? moment.duration(start) : null; // will be a Duration or null
  9329. out.end = end ? moment.duration(end) : null; // will be a Duration or null
  9330. out._recurring = true; // our internal marker
  9331. }
  9332. else {
  9333. if (start) {
  9334. start = t.moment(start);
  9335. if (!start.isValid()) {
  9336. return false;
  9337. }
  9338. }
  9339. if (end) {
  9340. end = t.moment(end);
  9341. if (!end.isValid()) {
  9342. end = null; // let defaults take over
  9343. }
  9344. }
  9345. allDay = input.allDay;
  9346. if (allDay === undefined) { // still undefined? fallback to default
  9347. allDay = firstDefined(
  9348. source ? source.allDayDefault : undefined,
  9349. t.options.allDayDefault
  9350. );
  9351. // still undefined? normalizeEventDates will calculate it
  9352. }
  9353. assignDatesToEvent(start, end, allDay, out);
  9354. }
  9355. t.normalizeEvent(out); // hook for external use. a prototype method
  9356. return out;
  9357. }
  9358. t.buildEventFromInput = buildEventFromInput;
  9359. // Normalizes and assigns the given dates to the given partially-formed event object.
  9360. // NOTE: mutates the given start/end moments. does not make a copy.
  9361. function assignDatesToEvent(start, end, allDay, event) {
  9362. event.start = start;
  9363. event.end = end;
  9364. event.allDay = allDay;
  9365. normalizeEventDates(event);
  9366. backupEventDates(event);
  9367. }
  9368. // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  9369. // NOTE: Will modify the given object.
  9370. function normalizeEventDates(eventProps) {
  9371. normalizeEventTimes(eventProps);
  9372. if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
  9373. eventProps.end = null;
  9374. }
  9375. if (!eventProps.end) {
  9376. if (t.options.forceEventDuration) {
  9377. eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
  9378. }
  9379. else {
  9380. eventProps.end = null;
  9381. }
  9382. }
  9383. }
  9384. // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  9385. function normalizeEventTimes(eventProps) {
  9386. if (eventProps.allDay == null) {
  9387. eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
  9388. }
  9389. if (eventProps.allDay) {
  9390. eventProps.start.stripTime();
  9391. if (eventProps.end) {
  9392. // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  9393. eventProps.end.stripTime();
  9394. }
  9395. }
  9396. else {
  9397. if (!eventProps.start.hasTime()) {
  9398. eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
  9399. }
  9400. if (eventProps.end && !eventProps.end.hasTime()) {
  9401. eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
  9402. }
  9403. }
  9404. }
  9405. // If the given event is a recurring event, break it down into an array of individual instances.
  9406. // If not a recurring event, return an array with the single original event.
  9407. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  9408. // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  9409. function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  9410. var events = [];
  9411. var dowHash;
  9412. var dow;
  9413. var i;
  9414. var date;
  9415. var startTime, endTime;
  9416. var start, end;
  9417. var event;
  9418. _rangeStart = _rangeStart || rangeStart;
  9419. _rangeEnd = _rangeEnd || rangeEnd;
  9420. if (abstractEvent) {
  9421. if (abstractEvent._recurring) {
  9422. // make a boolean hash as to whether the event occurs on each day-of-week
  9423. if ((dow = abstractEvent.dow)) {
  9424. dowHash = {};
  9425. for (i = 0; i < dow.length; i++) {
  9426. dowHash[dow[i]] = true;
  9427. }
  9428. }
  9429. // iterate through every day in the current range
  9430. date = _rangeStart.clone().stripTime(); // holds the date of the current day
  9431. while (date.isBefore(_rangeEnd)) {
  9432. if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  9433. startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  9434. endTime = abstractEvent.end; // "
  9435. start = date.clone();
  9436. end = null;
  9437. if (startTime) {
  9438. start = start.time(startTime);
  9439. }
  9440. if (endTime) {
  9441. end = date.clone().time(endTime);
  9442. }
  9443. event = $.extend({}, abstractEvent); // make a copy of the original
  9444. assignDatesToEvent(
  9445. start, end,
  9446. !startTime && !endTime, // allDay?
  9447. event
  9448. );
  9449. events.push(event);
  9450. }
  9451. date.add(1, 'days');
  9452. }
  9453. }
  9454. else {
  9455. events.push(abstractEvent); // return the original event. will be a one-item array
  9456. }
  9457. }
  9458. return events;
  9459. }
  9460. t.expandEvent = expandEvent;
  9461. /* Event Modification Math
  9462. -----------------------------------------------------------------------------------------*/
  9463. // Modifies an event and all related events by applying the given properties.
  9464. // Special date-diffing logic is used for manipulation of dates.
  9465. // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  9466. // All date comparisons are done against the event's pristine _start and _end dates.
  9467. // Returns an object with delta information and a function to undo all operations.
  9468. // For making computations in a granularity greater than day/time, specify largeUnit.
  9469. // NOTE: The given `newProps` might be mutated for normalization purposes.
  9470. function mutateEvent(event, newProps, largeUnit) {
  9471. var miscProps = {};
  9472. var oldProps;
  9473. var clearEnd;
  9474. var startDelta;
  9475. var endDelta;
  9476. var durationDelta;
  9477. var undoFunc;
  9478. // diffs the dates in the appropriate way, returning a duration
  9479. function diffDates(date1, date0) { // date1 - date0
  9480. if (largeUnit) {
  9481. return diffByUnit(date1, date0, largeUnit);
  9482. }
  9483. else if (newProps.allDay) {
  9484. return diffDay(date1, date0);
  9485. }
  9486. else {
  9487. return diffDayTime(date1, date0);
  9488. }
  9489. }
  9490. newProps = newProps || {};
  9491. // normalize new date-related properties
  9492. if (!newProps.start) {
  9493. newProps.start = event.start.clone();
  9494. }
  9495. if (newProps.end === undefined) {
  9496. newProps.end = event.end ? event.end.clone() : null;
  9497. }
  9498. if (newProps.allDay == null) { // is null or undefined?
  9499. newProps.allDay = event.allDay;
  9500. }
  9501. normalizeEventDates(newProps);
  9502. // create normalized versions of the original props to compare against
  9503. // need a real end value, for diffing
  9504. oldProps = {
  9505. start: event._start.clone(),
  9506. end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  9507. allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  9508. };
  9509. normalizeEventDates(oldProps);
  9510. // need to clear the end date if explicitly changed to null
  9511. clearEnd = event._end !== null && newProps.end === null;
  9512. // compute the delta for moving the start date
  9513. startDelta = diffDates(newProps.start, oldProps.start);
  9514. // compute the delta for moving the end date
  9515. if (newProps.end) {
  9516. endDelta = diffDates(newProps.end, oldProps.end);
  9517. durationDelta = endDelta.subtract(startDelta);
  9518. }
  9519. else {
  9520. durationDelta = null;
  9521. }
  9522. // gather all non-date-related properties
  9523. $.each(newProps, function(name, val) {
  9524. if (isMiscEventPropName(name)) {
  9525. if (val !== undefined) {
  9526. miscProps[name] = val;
  9527. }
  9528. }
  9529. });
  9530. // apply the operations to the event and all related events
  9531. undoFunc = mutateEvents(
  9532. clientEvents(event._id), // get events with this ID
  9533. clearEnd,
  9534. newProps.allDay,
  9535. startDelta,
  9536. durationDelta,
  9537. miscProps
  9538. );
  9539. return {
  9540. dateDelta: startDelta,
  9541. durationDelta: durationDelta,
  9542. undo: undoFunc
  9543. };
  9544. }
  9545. // Modifies an array of events in the following ways (operations are in order):
  9546. // - clear the event's `end`
  9547. // - convert the event to allDay
  9548. // - add `dateDelta` to the start and end
  9549. // - add `durationDelta` to the event's duration
  9550. // - assign `miscProps` to the event
  9551. //
  9552. // Returns a function that can be called to undo all the operations.
  9553. //
  9554. // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  9555. //
  9556. function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  9557. var isAmbigTimezone = t.getIsAmbigTimezone();
  9558. var undoFunctions = [];
  9559. // normalize zero-length deltas to be null
  9560. if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  9561. if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  9562. $.each(events, function(i, event) {
  9563. var oldProps;
  9564. var newProps;
  9565. // build an object holding all the old values, both date-related and misc.
  9566. // for the undo function.
  9567. oldProps = {
  9568. start: event.start.clone(),
  9569. end: event.end ? event.end.clone() : null,
  9570. allDay: event.allDay
  9571. };
  9572. $.each(miscProps, function(name) {
  9573. oldProps[name] = event[name];
  9574. });
  9575. // new date-related properties. work off the original date snapshot.
  9576. // ok to use references because they will be thrown away when backupEventDates is called.
  9577. newProps = {
  9578. start: event._start,
  9579. end: event._end,
  9580. allDay: allDay // normalize the dates in the same regard as the new properties
  9581. };
  9582. normalizeEventDates(newProps); // massages start/end/allDay
  9583. // strip or ensure the end date
  9584. if (clearEnd) {
  9585. newProps.end = null;
  9586. }
  9587. else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  9588. newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  9589. }
  9590. if (dateDelta) {
  9591. newProps.start.add(dateDelta);
  9592. if (newProps.end) {
  9593. newProps.end.add(dateDelta);
  9594. }
  9595. }
  9596. if (durationDelta) {
  9597. newProps.end.add(durationDelta); // end already ensured above
  9598. }
  9599. // if the dates have changed, and we know it is impossible to recompute the
  9600. // timezone offsets, strip the zone.
  9601. if (
  9602. isAmbigTimezone &&
  9603. !newProps.allDay &&
  9604. (dateDelta || durationDelta)
  9605. ) {
  9606. newProps.start.stripZone();
  9607. if (newProps.end) {
  9608. newProps.end.stripZone();
  9609. }
  9610. }
  9611. $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  9612. backupEventDates(event); // regenerate internal _start/_end/_allDay
  9613. undoFunctions.push(function() {
  9614. $.extend(event, oldProps);
  9615. backupEventDates(event); // regenerate internal _start/_end/_allDay
  9616. });
  9617. });
  9618. return function() {
  9619. for (var i = 0; i < undoFunctions.length; i++) {
  9620. undoFunctions[i]();
  9621. }
  9622. };
  9623. }
  9624. }
  9625. // hook for external libs to manipulate event properties upon creation.
  9626. // should manipulate the event in-place.
  9627. Calendar.prototype.normalizeEvent = function(event) {
  9628. };
  9629. // Does the given span (start, end, and other location information)
  9630. // fully contain the other?
  9631. Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
  9632. var eventStart = outerSpan.start.clone().stripZone();
  9633. var eventEnd = this.getEventEnd(outerSpan).stripZone();
  9634. return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
  9635. };
  9636. // Returns a list of events that the given event should be compared against when being considered for a move to
  9637. // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  9638. Calendar.prototype.getPeerEvents = function(span, event) {
  9639. var cache = this.getEventCache();
  9640. var peerEvents = [];
  9641. var i, otherEvent;
  9642. for (i = 0; i < cache.length; i++) {
  9643. otherEvent = cache[i];
  9644. if (
  9645. !event ||
  9646. event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  9647. ) {
  9648. peerEvents.push(otherEvent);
  9649. }
  9650. }
  9651. return peerEvents;
  9652. };
  9653. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  9654. function backupEventDates(event) {
  9655. event._allDay = event.allDay;
  9656. event._start = event.start.clone();
  9657. event._end = event.end ? event.end.clone() : null;
  9658. }
  9659. /* Overlapping / Constraining
  9660. -----------------------------------------------------------------------------------------*/
  9661. // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
  9662. Calendar.prototype.isEventSpanAllowed = function(span, event) {
  9663. var source = event.source || {};
  9664. var constraint = firstDefined(
  9665. event.constraint,
  9666. source.constraint,
  9667. this.options.eventConstraint
  9668. );
  9669. var overlap = firstDefined(
  9670. event.overlap,
  9671. source.overlap,
  9672. this.options.eventOverlap
  9673. );
  9674. return this.isSpanAllowed(span, constraint, overlap, event) &&
  9675. (!this.options.eventAllow || this.options.eventAllow(span, event) !== false);
  9676. };
  9677. // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
  9678. Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) {
  9679. var eventInput;
  9680. var event;
  9681. // note: very similar logic is in View's reportExternalDrop
  9682. if (eventProps) {
  9683. eventInput = $.extend({}, eventProps, eventLocation);
  9684. event = this.expandEvent(
  9685. this.buildEventFromInput(eventInput)
  9686. )[0];
  9687. }
  9688. if (event) {
  9689. return this.isEventSpanAllowed(eventSpan, event);
  9690. }
  9691. else { // treat it as a selection
  9692. return this.isSelectionSpanAllowed(eventSpan);
  9693. }
  9694. };
  9695. // Determines the given span (unzoned start/end with other misc data) can be selected.
  9696. Calendar.prototype.isSelectionSpanAllowed = function(span) {
  9697. return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) &&
  9698. (!this.options.selectAllow || this.options.selectAllow(span) !== false);
  9699. };
  9700. // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
  9701. // according to the constraint/overlap settings.
  9702. // `event` is not required if checking a selection.
  9703. Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) {
  9704. var constraintEvents;
  9705. var anyContainment;
  9706. var peerEvents;
  9707. var i, peerEvent;
  9708. var peerOverlap;
  9709. // the range must be fully contained by at least one of produced constraint events
  9710. if (constraint != null) {
  9711. // not treated as an event! intermediate data structure
  9712. // TODO: use ranges in the future
  9713. constraintEvents = this.constraintToEvents(constraint);
  9714. if (constraintEvents) { // not invalid
  9715. anyContainment = false;
  9716. for (i = 0; i < constraintEvents.length; i++) {
  9717. if (this.spanContainsSpan(constraintEvents[i], span)) {
  9718. anyContainment = true;
  9719. break;
  9720. }
  9721. }
  9722. if (!anyContainment) {
  9723. return false;
  9724. }
  9725. }
  9726. }
  9727. peerEvents = this.getPeerEvents(span, event);
  9728. for (i = 0; i < peerEvents.length; i++) {
  9729. peerEvent = peerEvents[i];
  9730. // there needs to be an actual intersection before disallowing anything
  9731. if (this.eventIntersectsRange(peerEvent, span)) {
  9732. // evaluate overlap for the given range and short-circuit if necessary
  9733. if (overlap === false) {
  9734. return false;
  9735. }
  9736. // if the event's overlap is a test function, pass the peer event in question as the first param
  9737. else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  9738. return false;
  9739. }
  9740. // if we are computing if the given range is allowable for an event, consider the other event's
  9741. // EventObject-specific or Source-specific `overlap` property
  9742. if (event) {
  9743. peerOverlap = firstDefined(
  9744. peerEvent.overlap,
  9745. (peerEvent.source || {}).overlap
  9746. // we already considered the global `eventOverlap`
  9747. );
  9748. if (peerOverlap === false) {
  9749. return false;
  9750. }
  9751. // if the peer event's overlap is a test function, pass the subject event as the first param
  9752. if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  9753. return false;
  9754. }
  9755. }
  9756. }
  9757. }
  9758. return true;
  9759. };
  9760. // Given an event input from the API, produces an array of event objects. Possible event inputs:
  9761. // 'businessHours'
  9762. // An event ID (number or string)
  9763. // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  9764. Calendar.prototype.constraintToEvents = function(constraintInput) {
  9765. if (constraintInput === 'businessHours') {
  9766. return this.getCurrentBusinessHourEvents();
  9767. }
  9768. if (typeof constraintInput === 'object') {
  9769. if (constraintInput.start != null) { // needs to be event-like input
  9770. return this.expandEvent(this.buildEventFromInput(constraintInput));
  9771. }
  9772. else {
  9773. return null; // invalid
  9774. }
  9775. }
  9776. return this.clientEvents(constraintInput); // probably an ID
  9777. };
  9778. // Does the event's date range intersect with the given range?
  9779. // start/end already assumed to have stripped zones :(
  9780. Calendar.prototype.eventIntersectsRange = function(event, range) {
  9781. var eventStart = event.start.clone().stripZone();
  9782. var eventEnd = this.getEventEnd(event).stripZone();
  9783. return range.start < eventEnd && range.end > eventStart;
  9784. };
  9785. /* Business Hours
  9786. -----------------------------------------------------------------------------------------*/
  9787. var BUSINESS_HOUR_EVENT_DEFAULTS = {
  9788. id: '_fcBusinessHours', // will relate events from different calls to expandEvent
  9789. start: '09:00',
  9790. end: '17:00',
  9791. dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  9792. rendering: 'inverse-background'
  9793. // classNames are defined in businessHoursSegClasses
  9794. };
  9795. // Return events objects for business hours within the current view.
  9796. // Abuse of our event system :(
  9797. Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) {
  9798. return this.computeBusinessHourEvents(wholeDay, this.options.businessHours);
  9799. };
  9800. // Given a raw input value from options, return events objects for business hours within the current view.
  9801. Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) {
  9802. if (input === true) {
  9803. return this.expandBusinessHourEvents(wholeDay, [ {} ]);
  9804. }
  9805. else if ($.isPlainObject(input)) {
  9806. return this.expandBusinessHourEvents(wholeDay, [ input ]);
  9807. }
  9808. else if ($.isArray(input)) {
  9809. return this.expandBusinessHourEvents(wholeDay, input, true);
  9810. }
  9811. else {
  9812. return [];
  9813. }
  9814. };
  9815. // inputs expected to be an array of objects.
  9816. // if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
  9817. Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) {
  9818. var view = this.getView();
  9819. var events = [];
  9820. var i, input;
  9821. for (i = 0; i < inputs.length; i++) {
  9822. input = inputs[i];
  9823. if (ignoreNoDow && !input.dow) {
  9824. continue;
  9825. }
  9826. // give defaults. will make a copy
  9827. input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input);
  9828. // if a whole-day series is requested, clear the start/end times
  9829. if (wholeDay) {
  9830. input.start = null;
  9831. input.end = null;
  9832. }
  9833. events.push.apply(events, // append
  9834. this.expandEvent(
  9835. this.buildEventFromInput(input),
  9836. view.start,
  9837. view.end
  9838. )
  9839. );
  9840. }
  9841. return events;
  9842. };
  9843. ;;
  9844. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  9845. ----------------------------------------------------------------------------------------------------------------------*/
  9846. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  9847. // It is responsible for managing width/height.
  9848. var BasicView = FC.BasicView = View.extend({
  9849. scroller: null,
  9850. dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
  9851. dayGrid: null, // the main subcomponent that does most of the heavy lifting
  9852. dayNumbersVisible: false, // display day numbers on each day cell?
  9853. colWeekNumbersVisible: false, // display week numbers along the side?
  9854. cellWeekNumbersVisible: false, // display week numbers in day cell?
  9855. weekNumberWidth: null, // width of all the week-number cells running down the side
  9856. headContainerEl: null, // div that hold's the dayGrid's rendered date header
  9857. headRowEl: null, // the fake row element of the day-of-week header
  9858. initialize: function() {
  9859. this.dayGrid = this.instantiateDayGrid();
  9860. this.scroller = new Scroller({
  9861. overflowX: 'hidden',
  9862. overflowY: 'auto'
  9863. });
  9864. },
  9865. // Generates the DayGrid object this view needs. Draws from this.dayGridClass
  9866. instantiateDayGrid: function() {
  9867. // generate a subclass on the fly with BasicView-specific behavior
  9868. // TODO: cache this subclass
  9869. var subclass = this.dayGridClass.extend(basicDayGridMethods);
  9870. return new subclass(this);
  9871. },
  9872. // Sets the display range and computes all necessary dates
  9873. setRange: function(range) {
  9874. View.prototype.setRange.call(this, range); // call the super-method
  9875. this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
  9876. this.dayGrid.setRange(range);
  9877. },
  9878. // Compute the value to feed into setRange. Overrides superclass.
  9879. computeRange: function(date) {
  9880. var range = View.prototype.computeRange.call(this, date); // get value from the super-method
  9881. // year and month views should be aligned with weeks. this is already done for week
  9882. if (/year|month/.test(range.intervalUnit)) {
  9883. range.start.startOf('week');
  9884. range.start = this.skipHiddenDays(range.start);
  9885. // make end-of-week if not already
  9886. if (range.end.weekday()) {
  9887. range.end.add(1, 'week').startOf('week');
  9888. range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
  9889. }
  9890. }
  9891. return range;
  9892. },
  9893. // Renders the view into `this.el`, which should already be assigned
  9894. renderDates: function() {
  9895. this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
  9896. if (this.opt('weekNumbers')) {
  9897. if (this.opt('weekNumbersWithinDays')) {
  9898. this.cellWeekNumbersVisible = true;
  9899. this.colWeekNumbersVisible = false;
  9900. }
  9901. else {
  9902. this.cellWeekNumbersVisible = false;
  9903. this.colWeekNumbersVisible = true;
  9904. };
  9905. }
  9906. this.dayGrid.numbersVisible = this.dayNumbersVisible ||
  9907. this.cellWeekNumbersVisible || this.colWeekNumbersVisible;
  9908. this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
  9909. this.renderHead();
  9910. this.scroller.render();
  9911. var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
  9912. var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
  9913. this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
  9914. this.dayGrid.setElement(dayGridEl);
  9915. this.dayGrid.renderDates(this.hasRigidRows());
  9916. },
  9917. // render the day-of-week headers
  9918. renderHead: function() {
  9919. this.headContainerEl =
  9920. this.el.find('.fc-head-container')
  9921. .html(this.dayGrid.renderHeadHtml());
  9922. this.headRowEl = this.headContainerEl.find('.fc-row');
  9923. },
  9924. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  9925. // always completely kill the dayGrid's rendering.
  9926. unrenderDates: function() {
  9927. this.dayGrid.unrenderDates();
  9928. this.dayGrid.removeElement();
  9929. this.scroller.destroy();
  9930. },
  9931. renderBusinessHours: function() {
  9932. this.dayGrid.renderBusinessHours();
  9933. },
  9934. unrenderBusinessHours: function() {
  9935. this.dayGrid.unrenderBusinessHours();
  9936. },
  9937. // Builds the HTML skeleton for the view.
  9938. // The day-grid component will render inside of a container defined by this HTML.
  9939. renderSkeletonHtml: function() {
  9940. return '' +
  9941. '<table>' +
  9942. '<thead class="fc-head">' +
  9943. '<tr>' +
  9944. '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
  9945. '</tr>' +
  9946. '</thead>' +
  9947. '<tbody class="fc-body">' +
  9948. '<tr>' +
  9949. '<td class="' + this.widgetContentClass + '"></td>' +
  9950. '</tr>' +
  9951. '</tbody>' +
  9952. '</table>';
  9953. },
  9954. // Generates an HTML attribute string for setting the width of the week number column, if it is known
  9955. weekNumberStyleAttr: function() {
  9956. if (this.weekNumberWidth !== null) {
  9957. return 'style="width:' + this.weekNumberWidth + 'px"';
  9958. }
  9959. return '';
  9960. },
  9961. // Determines whether each row should have a constant height
  9962. hasRigidRows: function() {
  9963. var eventLimit = this.opt('eventLimit');
  9964. return eventLimit && typeof eventLimit !== 'number';
  9965. },
  9966. /* Dimensions
  9967. ------------------------------------------------------------------------------------------------------------------*/
  9968. // Refreshes the horizontal dimensions of the view
  9969. updateWidth: function() {
  9970. if (this.colWeekNumbersVisible) {
  9971. // Make sure all week number cells running down the side have the same width.
  9972. // Record the width for cells created later.
  9973. this.weekNumberWidth = matchCellWidths(
  9974. this.el.find('.fc-week-number')
  9975. );
  9976. }
  9977. },
  9978. // Adjusts the vertical dimensions of the view to the specified values
  9979. setHeight: function(totalHeight, isAuto) {
  9980. var eventLimit = this.opt('eventLimit');
  9981. var scrollerHeight;
  9982. var scrollbarWidths;
  9983. // reset all heights to be natural
  9984. this.scroller.clear();
  9985. uncompensateScroll(this.headRowEl);
  9986. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  9987. // is the event limit a constant level number?
  9988. if (eventLimit && typeof eventLimit === 'number') {
  9989. this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  9990. }
  9991. // distribute the height to the rows
  9992. // (totalHeight is a "recommended" value if isAuto)
  9993. scrollerHeight = this.computeScrollerHeight(totalHeight);
  9994. this.setGridHeight(scrollerHeight, isAuto);
  9995. // is the event limit dynamically calculated?
  9996. if (eventLimit && typeof eventLimit !== 'number') {
  9997. this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  9998. }
  9999. if (!isAuto) { // should we force dimensions of the scroll container?
  10000. this.scroller.setHeight(scrollerHeight);
  10001. scrollbarWidths = this.scroller.getScrollbarWidths();
  10002. if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
  10003. compensateScroll(this.headRowEl, scrollbarWidths);
  10004. // doing the scrollbar compensation might have created text overflow which created more height. redo
  10005. scrollerHeight = this.computeScrollerHeight(totalHeight);
  10006. this.scroller.setHeight(scrollerHeight);
  10007. }
  10008. // guarantees the same scrollbar widths
  10009. this.scroller.lockOverflow(scrollbarWidths);
  10010. }
  10011. },
  10012. // given a desired total height of the view, returns what the height of the scroller should be
  10013. computeScrollerHeight: function(totalHeight) {
  10014. return totalHeight -
  10015. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10016. },
  10017. // Sets the height of just the DayGrid component in this view
  10018. setGridHeight: function(height, isAuto) {
  10019. if (isAuto) {
  10020. undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  10021. }
  10022. else {
  10023. distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  10024. }
  10025. },
  10026. /* Scroll
  10027. ------------------------------------------------------------------------------------------------------------------*/
  10028. computeInitialScroll: function() {
  10029. return { top: 0 };
  10030. },
  10031. queryScroll: function() {
  10032. return { top: this.scroller.getScrollTop() };
  10033. },
  10034. setScroll: function(scroll) {
  10035. this.scroller.setScrollTop(scroll.top);
  10036. },
  10037. /* Hit Areas
  10038. ------------------------------------------------------------------------------------------------------------------*/
  10039. // forward all hit-related method calls to dayGrid
  10040. prepareHits: function() {
  10041. this.dayGrid.prepareHits();
  10042. },
  10043. releaseHits: function() {
  10044. this.dayGrid.releaseHits();
  10045. },
  10046. queryHit: function(left, top) {
  10047. return this.dayGrid.queryHit(left, top);
  10048. },
  10049. getHitSpan: function(hit) {
  10050. return this.dayGrid.getHitSpan(hit);
  10051. },
  10052. getHitEl: function(hit) {
  10053. return this.dayGrid.getHitEl(hit);
  10054. },
  10055. /* Events
  10056. ------------------------------------------------------------------------------------------------------------------*/
  10057. // Renders the given events onto the view and populates the segments array
  10058. renderEvents: function(events) {
  10059. this.dayGrid.renderEvents(events);
  10060. this.updateHeight(); // must compensate for events that overflow the row
  10061. },
  10062. // Retrieves all segment objects that are rendered in the view
  10063. getEventSegs: function() {
  10064. return this.dayGrid.getEventSegs();
  10065. },
  10066. // Unrenders all event elements and clears internal segment data
  10067. unrenderEvents: function() {
  10068. this.dayGrid.unrenderEvents();
  10069. // we DON'T need to call updateHeight() because
  10070. // a renderEvents() call always happens after this, which will eventually call updateHeight()
  10071. },
  10072. /* Dragging (for both events and external elements)
  10073. ------------------------------------------------------------------------------------------------------------------*/
  10074. // A returned value of `true` signals that a mock "helper" event has been rendered.
  10075. renderDrag: function(dropLocation, seg) {
  10076. return this.dayGrid.renderDrag(dropLocation, seg);
  10077. },
  10078. unrenderDrag: function() {
  10079. this.dayGrid.unrenderDrag();
  10080. },
  10081. /* Selection
  10082. ------------------------------------------------------------------------------------------------------------------*/
  10083. // Renders a visual indication of a selection
  10084. renderSelection: function(span) {
  10085. this.dayGrid.renderSelection(span);
  10086. },
  10087. // Unrenders a visual indications of a selection
  10088. unrenderSelection: function() {
  10089. this.dayGrid.unrenderSelection();
  10090. }
  10091. });
  10092. // Methods that will customize the rendering behavior of the BasicView's dayGrid
  10093. var basicDayGridMethods = {
  10094. // Generates the HTML that will go before the day-of week header cells
  10095. renderHeadIntroHtml: function() {
  10096. var view = this.view;
  10097. if (view.colWeekNumbersVisible) {
  10098. return '' +
  10099. '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
  10100. '<span>' + // needed for matchCellWidths
  10101. htmlEscape(view.opt('weekNumberTitle')) +
  10102. '</span>' +
  10103. '</th>';
  10104. }
  10105. return '';
  10106. },
  10107. // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
  10108. renderNumberIntroHtml: function(row) {
  10109. var view = this.view;
  10110. var weekStart = this.getCellDate(row, 0);
  10111. if (view.colWeekNumbersVisible) {
  10112. return '' +
  10113. '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
  10114. view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
  10115. { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
  10116. weekStart.format('w') // inner HTML
  10117. ) +
  10118. '</td>';
  10119. }
  10120. return '';
  10121. },
  10122. // Generates the HTML that goes before the day bg cells for each day-row
  10123. renderBgIntroHtml: function() {
  10124. var view = this.view;
  10125. if (view.colWeekNumbersVisible) {
  10126. return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
  10127. view.weekNumberStyleAttr() + '></td>';
  10128. }
  10129. return '';
  10130. },
  10131. // Generates the HTML that goes before every other type of row generated by DayGrid.
  10132. // Affects helper-skeleton and highlight-skeleton rows.
  10133. renderIntroHtml: function() {
  10134. var view = this.view;
  10135. if (view.colWeekNumbersVisible) {
  10136. return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
  10137. }
  10138. return '';
  10139. }
  10140. };
  10141. ;;
  10142. /* A month view with day cells running in rows (one-per-week) and columns
  10143. ----------------------------------------------------------------------------------------------------------------------*/
  10144. var MonthView = FC.MonthView = BasicView.extend({
  10145. // Produces information about what range to display
  10146. computeRange: function(date) {
  10147. var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
  10148. var rowCnt;
  10149. // ensure 6 weeks
  10150. if (this.isFixedWeeks()) {
  10151. rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
  10152. range.end.add(6 - rowCnt, 'weeks');
  10153. }
  10154. return range;
  10155. },
  10156. // Overrides the default BasicView behavior to have special multi-week auto-height logic
  10157. setGridHeight: function(height, isAuto) {
  10158. // if auto, make the height of each row the height that it would be if there were 6 weeks
  10159. if (isAuto) {
  10160. height *= this.rowCnt / 6;
  10161. }
  10162. distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  10163. },
  10164. isFixedWeeks: function() {
  10165. return this.opt('fixedWeekCount');
  10166. }
  10167. });
  10168. ;;
  10169. fcViews.basic = {
  10170. 'class': BasicView
  10171. };
  10172. fcViews.basicDay = {
  10173. type: 'basic',
  10174. duration: { days: 1 }
  10175. };
  10176. fcViews.basicWeek = {
  10177. type: 'basic',
  10178. duration: { weeks: 1 }
  10179. };
  10180. fcViews.month = {
  10181. 'class': MonthView,
  10182. duration: { months: 1 }, // important for prev/next
  10183. defaults: {
  10184. fixedWeekCount: true
  10185. }
  10186. };
  10187. ;;
  10188. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  10189. ----------------------------------------------------------------------------------------------------------------------*/
  10190. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  10191. // Responsible for managing width/height.
  10192. var AgendaView = FC.AgendaView = View.extend({
  10193. scroller: null,
  10194. timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
  10195. timeGrid: null, // the main time-grid subcomponent of this view
  10196. dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override
  10197. dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  10198. axisWidth: null, // the width of the time axis running down the side
  10199. headContainerEl: null, // div that hold's the timeGrid's rendered date header
  10200. noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
  10201. // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  10202. bottomRuleEl: null,
  10203. initialize: function() {
  10204. this.timeGrid = this.instantiateTimeGrid();
  10205. if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  10206. this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
  10207. }
  10208. this.scroller = new Scroller({
  10209. overflowX: 'hidden',
  10210. overflowY: 'auto'
  10211. });
  10212. },
  10213. // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
  10214. instantiateTimeGrid: function() {
  10215. var subclass = this.timeGridClass.extend(agendaTimeGridMethods);
  10216. return new subclass(this);
  10217. },
  10218. // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
  10219. instantiateDayGrid: function() {
  10220. var subclass = this.dayGridClass.extend(agendaDayGridMethods);
  10221. return new subclass(this);
  10222. },
  10223. /* Rendering
  10224. ------------------------------------------------------------------------------------------------------------------*/
  10225. // Sets the display range and computes all necessary dates
  10226. setRange: function(range) {
  10227. View.prototype.setRange.call(this, range); // call the super-method
  10228. this.timeGrid.setRange(range);
  10229. if (this.dayGrid) {
  10230. this.dayGrid.setRange(range);
  10231. }
  10232. },
  10233. // Renders the view into `this.el`, which has already been assigned
  10234. renderDates: function() {
  10235. this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
  10236. this.renderHead();
  10237. this.scroller.render();
  10238. var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
  10239. var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
  10240. this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
  10241. this.timeGrid.setElement(timeGridEl);
  10242. this.timeGrid.renderDates();
  10243. // the <hr> that sometimes displays under the time-grid
  10244. this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
  10245. .appendTo(this.timeGrid.el); // inject it into the time-grid
  10246. if (this.dayGrid) {
  10247. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  10248. this.dayGrid.renderDates();
  10249. // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  10250. this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  10251. }
  10252. this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  10253. },
  10254. // render the day-of-week headers
  10255. renderHead: function() {
  10256. this.headContainerEl =
  10257. this.el.find('.fc-head-container')
  10258. .html(this.timeGrid.renderHeadHtml());
  10259. },
  10260. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  10261. // always completely kill each grid's rendering.
  10262. unrenderDates: function() {
  10263. this.timeGrid.unrenderDates();
  10264. this.timeGrid.removeElement();
  10265. if (this.dayGrid) {
  10266. this.dayGrid.unrenderDates();
  10267. this.dayGrid.removeElement();
  10268. }
  10269. this.scroller.destroy();
  10270. },
  10271. // Builds the HTML skeleton for the view.
  10272. // The day-grid and time-grid components will render inside containers defined by this HTML.
  10273. renderSkeletonHtml: function() {
  10274. return '' +
  10275. '<table>' +
  10276. '<thead class="fc-head">' +
  10277. '<tr>' +
  10278. '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
  10279. '</tr>' +
  10280. '</thead>' +
  10281. '<tbody class="fc-body">' +
  10282. '<tr>' +
  10283. '<td class="' + this.widgetContentClass + '">' +
  10284. (this.dayGrid ?
  10285. '<div class="fc-day-grid"/>' +
  10286. '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
  10287. ''
  10288. ) +
  10289. '</td>' +
  10290. '</tr>' +
  10291. '</tbody>' +
  10292. '</table>';
  10293. },
  10294. // Generates an HTML attribute string for setting the width of the axis, if it is known
  10295. axisStyleAttr: function() {
  10296. if (this.axisWidth !== null) {
  10297. return 'style="width:' + this.axisWidth + 'px"';
  10298. }
  10299. return '';
  10300. },
  10301. /* Business Hours
  10302. ------------------------------------------------------------------------------------------------------------------*/
  10303. renderBusinessHours: function() {
  10304. this.timeGrid.renderBusinessHours();
  10305. if (this.dayGrid) {
  10306. this.dayGrid.renderBusinessHours();
  10307. }
  10308. },
  10309. unrenderBusinessHours: function() {
  10310. this.timeGrid.unrenderBusinessHours();
  10311. if (this.dayGrid) {
  10312. this.dayGrid.unrenderBusinessHours();
  10313. }
  10314. },
  10315. /* Now Indicator
  10316. ------------------------------------------------------------------------------------------------------------------*/
  10317. getNowIndicatorUnit: function() {
  10318. return this.timeGrid.getNowIndicatorUnit();
  10319. },
  10320. renderNowIndicator: function(date) {
  10321. this.timeGrid.renderNowIndicator(date);
  10322. },
  10323. unrenderNowIndicator: function() {
  10324. this.timeGrid.unrenderNowIndicator();
  10325. },
  10326. /* Dimensions
  10327. ------------------------------------------------------------------------------------------------------------------*/
  10328. updateSize: function(isResize) {
  10329. this.timeGrid.updateSize(isResize);
  10330. View.prototype.updateSize.call(this, isResize); // call the super-method
  10331. },
  10332. // Refreshes the horizontal dimensions of the view
  10333. updateWidth: function() {
  10334. // make all axis cells line up, and record the width so newly created axis cells will have it
  10335. this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  10336. },
  10337. // Adjusts the vertical dimensions of the view to the specified values
  10338. setHeight: function(totalHeight, isAuto) {
  10339. var eventLimit;
  10340. var scrollerHeight;
  10341. var scrollbarWidths;
  10342. // reset all dimensions back to the original state
  10343. this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  10344. this.scroller.clear(); // sets height to 'auto' and clears overflow
  10345. uncompensateScroll(this.noScrollRowEls);
  10346. // limit number of events in the all-day area
  10347. if (this.dayGrid) {
  10348. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  10349. eventLimit = this.opt('eventLimit');
  10350. if (eventLimit && typeof eventLimit !== 'number') {
  10351. eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  10352. }
  10353. if (eventLimit) {
  10354. this.dayGrid.limitRows(eventLimit);
  10355. }
  10356. }
  10357. if (!isAuto) { // should we force dimensions of the scroll container?
  10358. scrollerHeight = this.computeScrollerHeight(totalHeight);
  10359. this.scroller.setHeight(scrollerHeight);
  10360. scrollbarWidths = this.scroller.getScrollbarWidths();
  10361. if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
  10362. // make the all-day and header rows lines up
  10363. compensateScroll(this.noScrollRowEls, scrollbarWidths);
  10364. // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  10365. // and reapply the desired height to the scroller.
  10366. scrollerHeight = this.computeScrollerHeight(totalHeight);
  10367. this.scroller.setHeight(scrollerHeight);
  10368. }
  10369. // guarantees the same scrollbar widths
  10370. this.scroller.lockOverflow(scrollbarWidths);
  10371. // if there's any space below the slats, show the horizontal rule.
  10372. // this won't cause any new overflow, because lockOverflow already called.
  10373. if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
  10374. this.bottomRuleEl.show();
  10375. }
  10376. }
  10377. },
  10378. // given a desired total height of the view, returns what the height of the scroller should be
  10379. computeScrollerHeight: function(totalHeight) {
  10380. return totalHeight -
  10381. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10382. },
  10383. /* Scroll
  10384. ------------------------------------------------------------------------------------------------------------------*/
  10385. // Computes the initial pre-configured scroll state prior to allowing the user to change it
  10386. computeInitialScroll: function() {
  10387. var scrollTime = moment.duration(this.opt('scrollTime'));
  10388. var top = this.timeGrid.computeTimeTop(scrollTime);
  10389. // zoom can give weird floating-point values. rather scroll a little bit further
  10390. top = Math.ceil(top);
  10391. if (top) {
  10392. top++; // to overcome top border that slots beyond the first have. looks better
  10393. }
  10394. return { top: top };
  10395. },
  10396. queryScroll: function() {
  10397. return { top: this.scroller.getScrollTop() };
  10398. },
  10399. setScroll: function(scroll) {
  10400. this.scroller.setScrollTop(scroll.top);
  10401. },
  10402. /* Hit Areas
  10403. ------------------------------------------------------------------------------------------------------------------*/
  10404. // forward all hit-related method calls to the grids (dayGrid might not be defined)
  10405. prepareHits: function() {
  10406. this.timeGrid.prepareHits();
  10407. if (this.dayGrid) {
  10408. this.dayGrid.prepareHits();
  10409. }
  10410. },
  10411. releaseHits: function() {
  10412. this.timeGrid.releaseHits();
  10413. if (this.dayGrid) {
  10414. this.dayGrid.releaseHits();
  10415. }
  10416. },
  10417. queryHit: function(left, top) {
  10418. var hit = this.timeGrid.queryHit(left, top);
  10419. if (!hit && this.dayGrid) {
  10420. hit = this.dayGrid.queryHit(left, top);
  10421. }
  10422. return hit;
  10423. },
  10424. getHitSpan: function(hit) {
  10425. // TODO: hit.component is set as a hack to identify where the hit came from
  10426. return hit.component.getHitSpan(hit);
  10427. },
  10428. getHitEl: function(hit) {
  10429. // TODO: hit.component is set as a hack to identify where the hit came from
  10430. return hit.component.getHitEl(hit);
  10431. },
  10432. /* Events
  10433. ------------------------------------------------------------------------------------------------------------------*/
  10434. // Renders events onto the view and populates the View's segment array
  10435. renderEvents: function(events) {
  10436. var dayEvents = [];
  10437. var timedEvents = [];
  10438. var daySegs = [];
  10439. var timedSegs;
  10440. var i;
  10441. // separate the events into all-day and timed
  10442. for (i = 0; i < events.length; i++) {
  10443. if (events[i].allDay) {
  10444. dayEvents.push(events[i]);
  10445. }
  10446. else {
  10447. timedEvents.push(events[i]);
  10448. }
  10449. }
  10450. // render the events in the subcomponents
  10451. timedSegs = this.timeGrid.renderEvents(timedEvents);
  10452. if (this.dayGrid) {
  10453. daySegs = this.dayGrid.renderEvents(dayEvents);
  10454. }
  10455. // the all-day area is flexible and might have a lot of events, so shift the height
  10456. this.updateHeight();
  10457. },
  10458. // Retrieves all segment objects that are rendered in the view
  10459. getEventSegs: function() {
  10460. return this.timeGrid.getEventSegs().concat(
  10461. this.dayGrid ? this.dayGrid.getEventSegs() : []
  10462. );
  10463. },
  10464. // Unrenders all event elements and clears internal segment data
  10465. unrenderEvents: function() {
  10466. // unrender the events in the subcomponents
  10467. this.timeGrid.unrenderEvents();
  10468. if (this.dayGrid) {
  10469. this.dayGrid.unrenderEvents();
  10470. }
  10471. // we DON'T need to call updateHeight() because
  10472. // a renderEvents() call always happens after this, which will eventually call updateHeight()
  10473. },
  10474. /* Dragging (for events and external elements)
  10475. ------------------------------------------------------------------------------------------------------------------*/
  10476. // A returned value of `true` signals that a mock "helper" event has been rendered.
  10477. renderDrag: function(dropLocation, seg) {
  10478. if (dropLocation.start.hasTime()) {
  10479. return this.timeGrid.renderDrag(dropLocation, seg);
  10480. }
  10481. else if (this.dayGrid) {
  10482. return this.dayGrid.renderDrag(dropLocation, seg);
  10483. }
  10484. },
  10485. unrenderDrag: function() {
  10486. this.timeGrid.unrenderDrag();
  10487. if (this.dayGrid) {
  10488. this.dayGrid.unrenderDrag();
  10489. }
  10490. },
  10491. /* Selection
  10492. ------------------------------------------------------------------------------------------------------------------*/
  10493. // Renders a visual indication of a selection
  10494. renderSelection: function(span) {
  10495. if (span.start.hasTime() || span.end.hasTime()) {
  10496. this.timeGrid.renderSelection(span);
  10497. }
  10498. else if (this.dayGrid) {
  10499. this.dayGrid.renderSelection(span);
  10500. }
  10501. },
  10502. // Unrenders a visual indications of a selection
  10503. unrenderSelection: function() {
  10504. this.timeGrid.unrenderSelection();
  10505. if (this.dayGrid) {
  10506. this.dayGrid.unrenderSelection();
  10507. }
  10508. }
  10509. });
  10510. // Methods that will customize the rendering behavior of the AgendaView's timeGrid
  10511. // TODO: move into TimeGrid
  10512. var agendaTimeGridMethods = {
  10513. // Generates the HTML that will go before the day-of week header cells
  10514. renderHeadIntroHtml: function() {
  10515. var view = this.view;
  10516. var weekText;
  10517. if (view.opt('weekNumbers')) {
  10518. weekText = this.start.format(view.opt('smallWeekFormat'));
  10519. return '' +
  10520. '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
  10521. view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
  10522. { date: this.start, type: 'week', forceOff: this.colCnt > 1 },
  10523. htmlEscape(weekText) // inner HTML
  10524. ) +
  10525. '</th>';
  10526. }
  10527. else {
  10528. return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>';
  10529. }
  10530. },
  10531. // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  10532. renderBgIntroHtml: function() {
  10533. var view = this.view;
  10534. return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>';
  10535. },
  10536. // Generates the HTML that goes before all other types of cells.
  10537. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  10538. renderIntroHtml: function() {
  10539. var view = this.view;
  10540. return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
  10541. }
  10542. };
  10543. // Methods that will customize the rendering behavior of the AgendaView's dayGrid
  10544. var agendaDayGridMethods = {
  10545. // Generates the HTML that goes before the all-day cells
  10546. renderBgIntroHtml: function() {
  10547. var view = this.view;
  10548. return '' +
  10549. '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  10550. '<span>' + // needed for matchCellWidths
  10551. view.getAllDayHtml() +
  10552. '</span>' +
  10553. '</td>';
  10554. },
  10555. // Generates the HTML that goes before all other types of cells.
  10556. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  10557. renderIntroHtml: function() {
  10558. var view = this.view;
  10559. return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
  10560. }
  10561. };
  10562. ;;
  10563. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  10564. // potential nice values for the slot-duration and interval-duration
  10565. // from largest to smallest
  10566. var AGENDA_STOCK_SUB_DURATIONS = [
  10567. { hours: 1 },
  10568. { minutes: 30 },
  10569. { minutes: 15 },
  10570. { seconds: 30 },
  10571. { seconds: 15 }
  10572. ];
  10573. fcViews.agenda = {
  10574. 'class': AgendaView,
  10575. defaults: {
  10576. allDaySlot: true,
  10577. slotDuration: '00:30:00',
  10578. minTime: '00:00:00',
  10579. maxTime: '24:00:00',
  10580. slotEventOverlap: true // a bad name. confused with overlap/constraint system
  10581. }
  10582. };
  10583. fcViews.agendaDay = {
  10584. type: 'agenda',
  10585. duration: { days: 1 }
  10586. };
  10587. fcViews.agendaWeek = {
  10588. type: 'agenda',
  10589. duration: { weeks: 1 }
  10590. };
  10591. ;;
  10592. /*
  10593. Responsible for the scroller, and forwarding event-related actions into the "grid"
  10594. */
  10595. var ListView = View.extend({
  10596. grid: null,
  10597. scroller: null,
  10598. initialize: function() {
  10599. this.grid = new ListViewGrid(this);
  10600. this.scroller = new Scroller({
  10601. overflowX: 'hidden',
  10602. overflowY: 'auto'
  10603. });
  10604. },
  10605. setRange: function(range) {
  10606. View.prototype.setRange.call(this, range); // super
  10607. this.grid.setRange(range); // needs to process range-related options
  10608. },
  10609. renderSkeleton: function() {
  10610. this.el.addClass(
  10611. 'fc-list-view ' +
  10612. this.widgetContentClass
  10613. );
  10614. this.scroller.render();
  10615. this.scroller.el.appendTo(this.el);
  10616. this.grid.setElement(this.scroller.scrollEl);
  10617. },
  10618. unrenderSkeleton: function() {
  10619. this.scroller.destroy(); // will remove the Grid too
  10620. },
  10621. setHeight: function(totalHeight, isAuto) {
  10622. this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
  10623. },
  10624. computeScrollerHeight: function(totalHeight) {
  10625. return totalHeight -
  10626. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10627. },
  10628. renderEvents: function(events) {
  10629. this.grid.renderEvents(events);
  10630. },
  10631. unrenderEvents: function() {
  10632. this.grid.unrenderEvents();
  10633. },
  10634. isEventResizable: function(event) {
  10635. return false;
  10636. },
  10637. isEventDraggable: function(event) {
  10638. return false;
  10639. }
  10640. });
  10641. /*
  10642. Responsible for event rendering and user-interaction.
  10643. Its "el" is the inner-content of the above view's scroller.
  10644. */
  10645. var ListViewGrid = Grid.extend({
  10646. segSelector: '.fc-list-item', // which elements accept event actions
  10647. hasDayInteractions: false, // no day selection or day clicking
  10648. // slices by day
  10649. spanToSegs: function(span) {
  10650. var view = this.view;
  10651. var dayStart = view.start.clone().time(0); // timed, so segs get times!
  10652. var dayIndex = 0;
  10653. var seg;
  10654. var segs = [];
  10655. while (dayStart < view.end) {
  10656. seg = intersectRanges(span, {
  10657. start: dayStart,
  10658. end: dayStart.clone().add(1, 'day')
  10659. });
  10660. if (seg) {
  10661. seg.dayIndex = dayIndex;
  10662. segs.push(seg);
  10663. }
  10664. dayStart.add(1, 'day');
  10665. dayIndex++;
  10666. // detect when span won't go fully into the next day,
  10667. // and mutate the latest seg to the be the end.
  10668. if (
  10669. seg && !seg.isEnd && span.end.hasTime() &&
  10670. span.end < dayStart.clone().add(this.view.nextDayThreshold)
  10671. ) {
  10672. seg.end = span.end.clone();
  10673. seg.isEnd = true;
  10674. break;
  10675. }
  10676. }
  10677. return segs;
  10678. },
  10679. // like "4:00am"
  10680. computeEventTimeFormat: function() {
  10681. return this.view.opt('mediumTimeFormat');
  10682. },
  10683. // for events with a url, the whole <tr> should be clickable,
  10684. // but it's impossible to wrap with an <a> tag. simulate this.
  10685. handleSegClick: function(seg, ev) {
  10686. var url;
  10687. Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action
  10688. // not clicking on or within an <a> with an href
  10689. if (!$(ev.target).closest('a[href]').length) {
  10690. url = seg.event.url;
  10691. if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
  10692. window.location.href = url; // simulate link click
  10693. }
  10694. }
  10695. },
  10696. // returns list of foreground segs that were actually rendered
  10697. renderFgSegs: function(segs) {
  10698. segs = this.renderFgSegEls(segs); // might filter away hidden events
  10699. if (!segs.length) {
  10700. this.renderEmptyMessage();
  10701. }
  10702. else {
  10703. this.renderSegList(segs);
  10704. }
  10705. return segs;
  10706. },
  10707. renderEmptyMessage: function() {
  10708. this.el.html(
  10709. '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
  10710. '<div class="fc-list-empty-wrap1">' +
  10711. '<div class="fc-list-empty">' +
  10712. htmlEscape(this.view.opt('noEventsMessage')) +
  10713. '</div>' +
  10714. '</div>' +
  10715. '</div>'
  10716. );
  10717. },
  10718. // render the event segments in the view
  10719. renderSegList: function(allSegs) {
  10720. var segsByDay = this.groupSegsByDay(allSegs); // sparse array
  10721. var dayIndex;
  10722. var daySegs;
  10723. var i;
  10724. var tableEl = $('<table class="fc-list-table"><tbody/></table>');
  10725. var tbodyEl = tableEl.find('tbody');
  10726. for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
  10727. daySegs = segsByDay[dayIndex];
  10728. if (daySegs) { // sparse array, so might be undefined
  10729. // append a day header
  10730. tbodyEl.append(this.dayHeaderHtml(
  10731. this.view.start.clone().add(dayIndex, 'days')
  10732. ));
  10733. this.sortEventSegs(daySegs);
  10734. for (i = 0; i < daySegs.length; i++) {
  10735. tbodyEl.append(daySegs[i].el); // append event row
  10736. }
  10737. }
  10738. }
  10739. this.el.empty().append(tableEl);
  10740. },
  10741. // Returns a sparse array of arrays, segs grouped by their dayIndex
  10742. groupSegsByDay: function(segs) {
  10743. var segsByDay = []; // sparse array
  10744. var i, seg;
  10745. for (i = 0; i < segs.length; i++) {
  10746. seg = segs[i];
  10747. (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
  10748. .push(seg);
  10749. }
  10750. return segsByDay;
  10751. },
  10752. // generates the HTML for the day headers that live amongst the event rows
  10753. dayHeaderHtml: function(dayDate) {
  10754. var view = this.view;
  10755. var mainFormat = view.opt('listDayFormat');
  10756. var altFormat = view.opt('listDayAltFormat');
  10757. return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
  10758. '<td class="' + view.widgetHeaderClass + '" colspan="3">' +
  10759. (mainFormat ?
  10760. view.buildGotoAnchorHtml(
  10761. dayDate,
  10762. { 'class': 'fc-list-heading-main' },
  10763. htmlEscape(dayDate.format(mainFormat)) // inner HTML
  10764. ) :
  10765. '') +
  10766. (altFormat ?
  10767. view.buildGotoAnchorHtml(
  10768. dayDate,
  10769. { 'class': 'fc-list-heading-alt' },
  10770. htmlEscape(dayDate.format(altFormat)) // inner HTML
  10771. ) :
  10772. '') +
  10773. '</td>' +
  10774. '</tr>';
  10775. },
  10776. // generates the HTML for a single event row
  10777. fgSegHtml: function(seg) {
  10778. var view = this.view;
  10779. var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg));
  10780. var bgColor = this.getSegBackgroundColor(seg);
  10781. var event = seg.event;
  10782. var url = event.url;
  10783. var timeHtml;
  10784. if (event.allDay) {
  10785. timeHtml = view.getAllDayHtml();
  10786. }
  10787. else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day
  10788. if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
  10789. timeHtml = htmlEscape(this.getEventTimeText(seg));
  10790. }
  10791. else { // inner segment that lasts the whole day
  10792. timeHtml = view.getAllDayHtml();
  10793. }
  10794. }
  10795. else {
  10796. // Display the normal time text for the *event's* times
  10797. timeHtml = htmlEscape(this.getEventTimeText(event));
  10798. }
  10799. if (url) {
  10800. classes.push('fc-has-url');
  10801. }
  10802. return '<tr class="' + classes.join(' ') + '">' +
  10803. (this.displayEventTime ?
  10804. '<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
  10805. (timeHtml || '') +
  10806. '</td>' :
  10807. '') +
  10808. '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
  10809. '<span class="fc-event-dot"' +
  10810. (bgColor ?
  10811. ' style="background-color:' + bgColor + '"' :
  10812. '') +
  10813. '></span>' +
  10814. '</td>' +
  10815. '<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
  10816. '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
  10817. htmlEscape(seg.event.title || '') +
  10818. '</a>' +
  10819. '</td>' +
  10820. '</tr>';
  10821. }
  10822. });
  10823. ;;
  10824. fcViews.list = {
  10825. 'class': ListView,
  10826. buttonTextKey: 'list', // what to lookup in locale files
  10827. defaults: {
  10828. buttonText: 'list', // text to display for English
  10829. listDayFormat: 'LL', // like "January 1, 2016"
  10830. noEventsMessage: 'No events to display'
  10831. }
  10832. };
  10833. fcViews.listDay = {
  10834. type: 'list',
  10835. duration: { days: 1 },
  10836. defaults: {
  10837. listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header
  10838. }
  10839. };
  10840. fcViews.listWeek = {
  10841. type: 'list',
  10842. duration: { weeks: 1 },
  10843. defaults: {
  10844. listDayFormat: 'dddd', // day-of-week is more important
  10845. listDayAltFormat: 'LL'
  10846. }
  10847. };
  10848. fcViews.listMonth = {
  10849. type: 'list',
  10850. duration: { month: 1 },
  10851. defaults: {
  10852. listDayAltFormat: 'dddd' // day-of-week is nice-to-have
  10853. }
  10854. };
  10855. fcViews.listYear = {
  10856. type: 'list',
  10857. duration: { year: 1 },
  10858. defaults: {
  10859. listDayAltFormat: 'dddd' // day-of-week is nice-to-have
  10860. }
  10861. };
  10862. ;;
  10863. return FC; // export for Node/CommonJS
  10864. });