fullcalendar.js 384 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189
  1. /*!
  2. * FullCalendar v3.1.0
  3. * Docs & License: http://fullcalendar.io/
  4. * (c) 2016 Adam Shaw
  5. */
  6. var FC = $.fullCalendar = {
  7. version: "3.1.0",
  8. internalApiVersion: 7
  9. };
  10. var fcViews = FC.views = {};
  11. $.fn.fullCalendar = function(options) {
  12. var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  13. var res = this; // what this function will return (this jQuery object by default)
  14. this.each(function(i, _element) { // loop each DOM element involved
  15. var element = $(_element);
  16. var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  17. var singleRes; // the returned value of this single method call
  18. // a method call
  19. if (typeof options === 'string') {
  20. if (calendar && $.isFunction(calendar[options])) {
  21. singleRes = calendar[options].apply(calendar, args);
  22. if (!i) {
  23. res = singleRes; // record the first method call result
  24. }
  25. if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  26. element.removeData('fullCalendar');
  27. }
  28. }
  29. }
  30. // a new calendar initialization
  31. else if (!calendar) { // don't initialize twice
  32. calendar = new Calendar(element, options);
  33. element.data('fullCalendar', calendar);
  34. calendar.render();
  35. }
  36. });
  37. return res;
  38. };
  39. var complexOptions = [ // names of options that are objects whose properties should be combined
  40. 'header',
  41. 'footer',
  42. 'buttonText',
  43. 'buttonIcons',
  44. 'themeButtonIcons'
  45. ];
  46. // Merges an array of option objects into a single object
  47. function mergeOptions(optionObjs) {
  48. return mergeProps(optionObjs, complexOptions);
  49. }
  50. ;;
  51. // exports
  52. FC.intersectRanges = intersectRanges;
  53. FC.applyAll = applyAll;
  54. FC.debounce = debounce;
  55. FC.isInt = isInt;
  56. FC.htmlEscape = htmlEscape;
  57. FC.cssToStr = cssToStr;
  58. FC.proxy = proxy;
  59. FC.capitaliseFirstLetter = capitaliseFirstLetter;
  60. /* FullCalendar-specific DOM Utilities
  61. ----------------------------------------------------------------------------------------------------------------------*/
  62. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  63. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  64. function compensateScroll(rowEls, scrollbarWidths) {
  65. if (scrollbarWidths.left) {
  66. rowEls.css({
  67. 'border-left-width': 1,
  68. 'margin-left': scrollbarWidths.left - 1
  69. });
  70. }
  71. if (scrollbarWidths.right) {
  72. rowEls.css({
  73. 'border-right-width': 1,
  74. 'margin-right': scrollbarWidths.right - 1
  75. });
  76. }
  77. }
  78. // Undoes compensateScroll and restores all borders/margins
  79. function uncompensateScroll(rowEls) {
  80. rowEls.css({
  81. 'margin-left': '',
  82. 'margin-right': '',
  83. 'border-left-width': '',
  84. 'border-right-width': ''
  85. });
  86. }
  87. // Make the mouse cursor express that an event is not allowed in the current area
  88. function disableCursor() {
  89. $('body').addClass('fc-not-allowed');
  90. }
  91. // Returns the mouse cursor to its original look
  92. function enableCursor() {
  93. $('body').removeClass('fc-not-allowed');
  94. }
  95. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  96. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  97. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  98. // reduces the available height.
  99. function distributeHeight(els, availableHeight, shouldRedistribute) {
  100. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  101. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  102. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  103. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  104. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  105. var flexOffsets = []; // amount of vertical space it takes up
  106. var flexHeights = []; // actual css height
  107. var usedHeight = 0;
  108. undistributeHeight(els); // give all elements their natural height
  109. // find elements that are below the recommended height (expandable).
  110. // important to query for heights in a single first pass (to avoid reflow oscillation).
  111. els.each(function(i, el) {
  112. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  113. var naturalOffset = $(el).outerHeight(true);
  114. if (naturalOffset < minOffset) {
  115. flexEls.push(el);
  116. flexOffsets.push(naturalOffset);
  117. flexHeights.push($(el).height());
  118. }
  119. else {
  120. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  121. usedHeight += naturalOffset;
  122. }
  123. });
  124. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  125. if (shouldRedistribute) {
  126. availableHeight -= usedHeight;
  127. minOffset1 = Math.floor(availableHeight / flexEls.length);
  128. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  129. }
  130. // assign heights to all expandable elements
  131. $(flexEls).each(function(i, el) {
  132. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  133. var naturalOffset = flexOffsets[i];
  134. var naturalHeight = flexHeights[i];
  135. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  136. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  137. $(el).height(newHeight);
  138. }
  139. });
  140. }
  141. // Undoes distrubuteHeight, restoring all els to their natural height
  142. function undistributeHeight(els) {
  143. els.height('');
  144. }
  145. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  146. // cells to be that width.
  147. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  148. function matchCellWidths(els) {
  149. var maxInnerWidth = 0;
  150. els.find('> *').each(function(i, innerEl) {
  151. var innerWidth = $(innerEl).outerWidth();
  152. if (innerWidth > maxInnerWidth) {
  153. maxInnerWidth = innerWidth;
  154. }
  155. });
  156. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  157. els.width(maxInnerWidth);
  158. return maxInnerWidth;
  159. }
  160. // Given one element that resides inside another,
  161. // Subtracts the height of the inner element from the outer element.
  162. function subtractInnerElHeight(outerEl, innerEl) {
  163. var both = outerEl.add(innerEl);
  164. var diff;
  165. // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  166. both.css({
  167. position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  168. left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  169. });
  170. diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
  171. both.css({ position: '', left: '' }); // undo hack
  172. return diff;
  173. }
  174. /* Element Geom Utilities
  175. ----------------------------------------------------------------------------------------------------------------------*/
  176. FC.getOuterRect = getOuterRect;
  177. FC.getClientRect = getClientRect;
  178. FC.getContentRect = getContentRect;
  179. FC.getScrollbarWidths = getScrollbarWidths;
  180. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  181. function getScrollParent(el) {
  182. var position = el.css('position'),
  183. scrollParent = el.parents().filter(function() {
  184. var parent = $(this);
  185. return (/(auto|scroll)/).test(
  186. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  187. );
  188. }).eq(0);
  189. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  190. }
  191. // Queries the outer bounding area of a jQuery element.
  192. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  193. // Origin is optional.
  194. function getOuterRect(el, origin) {
  195. var offset = el.offset();
  196. var left = offset.left - (origin ? origin.left : 0);
  197. var top = offset.top - (origin ? origin.top : 0);
  198. return {
  199. left: left,
  200. right: left + el.outerWidth(),
  201. top: top,
  202. bottom: top + el.outerHeight()
  203. };
  204. }
  205. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  206. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  207. // Origin is optional.
  208. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  209. function getClientRect(el, origin) {
  210. var offset = el.offset();
  211. var scrollbarWidths = getScrollbarWidths(el);
  212. var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
  213. var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
  214. return {
  215. left: left,
  216. right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  217. top: top,
  218. bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  219. };
  220. }
  221. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  222. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  223. // Origin is optional.
  224. function getContentRect(el, origin) {
  225. var offset = el.offset(); // just outside of border, margin not included
  226. var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
  227. (origin ? origin.left : 0);
  228. var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
  229. (origin ? origin.top : 0);
  230. return {
  231. left: left,
  232. right: left + el.width(),
  233. top: top,
  234. bottom: top + el.height()
  235. };
  236. }
  237. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  238. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  239. function getScrollbarWidths(el) {
  240. var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
  241. var widths = {
  242. left: 0,
  243. right: 0,
  244. top: 0,
  245. bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
  246. };
  247. if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  248. widths.left = leftRightWidth;
  249. }
  250. else {
  251. widths.right = leftRightWidth;
  252. }
  253. return widths;
  254. }
  255. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  256. var _isLeftRtlScrollbars = null;
  257. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  258. if (_isLeftRtlScrollbars === null) {
  259. _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  260. }
  261. return _isLeftRtlScrollbars;
  262. }
  263. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  264. var el = $('<div><div/></div>')
  265. .css({
  266. position: 'absolute',
  267. top: -1000,
  268. left: 0,
  269. border: 0,
  270. padding: 0,
  271. overflow: 'scroll',
  272. direction: 'rtl'
  273. })
  274. .appendTo('body');
  275. var innerEl = el.children();
  276. var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  277. el.remove();
  278. return res;
  279. }
  280. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  281. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  282. function getCssFloat(el, prop) {
  283. return parseFloat(el.css(prop)) || 0;
  284. }
  285. /* Mouse / Touch Utilities
  286. ----------------------------------------------------------------------------------------------------------------------*/
  287. FC.preventDefault = preventDefault;
  288. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  289. function isPrimaryMouseButton(ev) {
  290. return ev.which == 1 && !ev.ctrlKey;
  291. }
  292. function getEvX(ev) {
  293. if (ev.pageX !== undefined) {
  294. return ev.pageX;
  295. }
  296. var touches = ev.originalEvent.touches;
  297. if (touches) {
  298. return touches[0].pageX;
  299. }
  300. }
  301. function getEvY(ev) {
  302. if (ev.pageY !== undefined) {
  303. return ev.pageY;
  304. }
  305. var touches = ev.originalEvent.touches;
  306. if (touches) {
  307. return touches[0].pageY;
  308. }
  309. }
  310. function getEvIsTouch(ev) {
  311. return /^touch/.test(ev.type);
  312. }
  313. function preventSelection(el) {
  314. el.addClass('fc-unselectable')
  315. .on('selectstart', preventDefault);
  316. }
  317. // Stops a mouse/touch event from doing it's native browser action
  318. function preventDefault(ev) {
  319. ev.preventDefault();
  320. }
  321. // attach a handler to get called when ANY scroll action happens on the page.
  322. // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
  323. // http://stackoverflow.com/a/32954565/96342
  324. // returns `true` on success.
  325. function bindAnyScroll(handler) {
  326. if (window.addEventListener) {
  327. window.addEventListener('scroll', handler, true); // useCapture=true
  328. return true;
  329. }
  330. return false;
  331. }
  332. // undoes bindAnyScroll. must pass in the original function.
  333. // returns `true` on success.
  334. function unbindAnyScroll(handler) {
  335. if (window.removeEventListener) {
  336. window.removeEventListener('scroll', handler, true); // useCapture=true
  337. return true;
  338. }
  339. return false;
  340. }
  341. /* General Geometry Utils
  342. ----------------------------------------------------------------------------------------------------------------------*/
  343. FC.intersectRects = intersectRects;
  344. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  345. function intersectRects(rect1, rect2) {
  346. var res = {
  347. left: Math.max(rect1.left, rect2.left),
  348. right: Math.min(rect1.right, rect2.right),
  349. top: Math.max(rect1.top, rect2.top),
  350. bottom: Math.min(rect1.bottom, rect2.bottom)
  351. };
  352. if (res.left < res.right && res.top < res.bottom) {
  353. return res;
  354. }
  355. return false;
  356. }
  357. // Returns a new point that will have been moved to reside within the given rectangle
  358. function constrainPoint(point, rect) {
  359. return {
  360. left: Math.min(Math.max(point.left, rect.left), rect.right),
  361. top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  362. };
  363. }
  364. // Returns a point that is the center of the given rectangle
  365. function getRectCenter(rect) {
  366. return {
  367. left: (rect.left + rect.right) / 2,
  368. top: (rect.top + rect.bottom) / 2
  369. };
  370. }
  371. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  372. function diffPoints(point1, point2) {
  373. return {
  374. left: point1.left - point2.left,
  375. top: point1.top - point2.top
  376. };
  377. }
  378. /* Object Ordering by Field
  379. ----------------------------------------------------------------------------------------------------------------------*/
  380. FC.parseFieldSpecs = parseFieldSpecs;
  381. FC.compareByFieldSpecs = compareByFieldSpecs;
  382. FC.compareByFieldSpec = compareByFieldSpec;
  383. FC.flexibleCompare = flexibleCompare;
  384. function parseFieldSpecs(input) {
  385. var specs = [];
  386. var tokens = [];
  387. var i, token;
  388. if (typeof input === 'string') {
  389. tokens = input.split(/\s*,\s*/);
  390. }
  391. else if (typeof input === 'function') {
  392. tokens = [ input ];
  393. }
  394. else if ($.isArray(input)) {
  395. tokens = input;
  396. }
  397. for (i = 0; i < tokens.length; i++) {
  398. token = tokens[i];
  399. if (typeof token === 'string') {
  400. specs.push(
  401. token.charAt(0) == '-' ?
  402. { field: token.substring(1), order: -1 } :
  403. { field: token, order: 1 }
  404. );
  405. }
  406. else if (typeof token === 'function') {
  407. specs.push({ func: token });
  408. }
  409. }
  410. return specs;
  411. }
  412. function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
  413. var i;
  414. var cmp;
  415. for (i = 0; i < fieldSpecs.length; i++) {
  416. cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
  417. if (cmp) {
  418. return cmp;
  419. }
  420. }
  421. return 0;
  422. }
  423. function compareByFieldSpec(obj1, obj2, fieldSpec) {
  424. if (fieldSpec.func) {
  425. return fieldSpec.func(obj1, obj2);
  426. }
  427. return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
  428. (fieldSpec.order || 1);
  429. }
  430. function flexibleCompare(a, b) {
  431. if (!a && !b) {
  432. return 0;
  433. }
  434. if (b == null) {
  435. return -1;
  436. }
  437. if (a == null) {
  438. return 1;
  439. }
  440. if ($.type(a) === 'string' || $.type(b) === 'string') {
  441. return String(a).localeCompare(String(b));
  442. }
  443. return a - b;
  444. }
  445. /* FullCalendar-specific Misc Utilities
  446. ----------------------------------------------------------------------------------------------------------------------*/
  447. // Computes the intersection of the two ranges. Will return fresh date clones in a range.
  448. // Returns undefined if no intersection.
  449. // Expects all dates to be normalized to the same timezone beforehand.
  450. // TODO: move to date section?
  451. function intersectRanges(subjectRange, constraintRange) {
  452. var subjectStart = subjectRange.start;
  453. var subjectEnd = subjectRange.end;
  454. var constraintStart = constraintRange.start;
  455. var constraintEnd = constraintRange.end;
  456. var segStart, segEnd;
  457. var isStart, isEnd;
  458. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  459. if (subjectStart >= constraintStart) {
  460. segStart = subjectStart.clone();
  461. isStart = true;
  462. }
  463. else {
  464. segStart = constraintStart.clone();
  465. isStart = false;
  466. }
  467. if (subjectEnd <= constraintEnd) {
  468. segEnd = subjectEnd.clone();
  469. isEnd = true;
  470. }
  471. else {
  472. segEnd = constraintEnd.clone();
  473. isEnd = false;
  474. }
  475. return {
  476. start: segStart,
  477. end: segEnd,
  478. isStart: isStart,
  479. isEnd: isEnd
  480. };
  481. }
  482. }
  483. /* Date Utilities
  484. ----------------------------------------------------------------------------------------------------------------------*/
  485. FC.computeIntervalUnit = computeIntervalUnit;
  486. FC.divideRangeByDuration = divideRangeByDuration;
  487. FC.divideDurationByDuration = divideDurationByDuration;
  488. FC.multiplyDuration = multiplyDuration;
  489. FC.durationHasTime = durationHasTime;
  490. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  491. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  492. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  493. // Moments will have their timezones normalized.
  494. function diffDayTime(a, b) {
  495. return moment.duration({
  496. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  497. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  498. });
  499. }
  500. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  501. function diffDay(a, b) {
  502. return moment.duration({
  503. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  504. });
  505. }
  506. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  507. function diffByUnit(a, b, unit) {
  508. return moment.duration(
  509. Math.round(a.diff(b, unit, true)), // returnFloat=true
  510. unit
  511. );
  512. }
  513. // Computes the unit name of the largest whole-unit period of time.
  514. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  515. // Accepts start/end, a range object, or an original duration object.
  516. function computeIntervalUnit(start, end) {
  517. var i, unit;
  518. var val;
  519. for (i = 0; i < intervalUnits.length; i++) {
  520. unit = intervalUnits[i];
  521. val = computeRangeAs(unit, start, end);
  522. if (val >= 1 && isInt(val)) {
  523. break;
  524. }
  525. }
  526. return unit; // will be "milliseconds" if nothing else matches
  527. }
  528. // Computes the number of units (like "hours") in the given range.
  529. // Range can be a {start,end} object, separate start/end args, or a Duration.
  530. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  531. // of month-diffing logic (which tends to vary from version to version).
  532. function computeRangeAs(unit, start, end) {
  533. if (end != null) { // given start, end
  534. return end.diff(start, unit, true);
  535. }
  536. else if (moment.isDuration(start)) { // given duration
  537. return start.as(unit);
  538. }
  539. else { // given { start, end } range object
  540. return start.end.diff(start.start, unit, true);
  541. }
  542. }
  543. // Intelligently divides a range (specified by a start/end params) by a duration
  544. function divideRangeByDuration(start, end, dur) {
  545. var months;
  546. if (durationHasTime(dur)) {
  547. return (end - start) / dur;
  548. }
  549. months = dur.asMonths();
  550. if (Math.abs(months) >= 1 && isInt(months)) {
  551. return end.diff(start, 'months', true) / months;
  552. }
  553. return end.diff(start, 'days', true) / dur.asDays();
  554. }
  555. // Intelligently divides one duration by another
  556. function divideDurationByDuration(dur1, dur2) {
  557. var months1, months2;
  558. if (durationHasTime(dur1) || durationHasTime(dur2)) {
  559. return dur1 / dur2;
  560. }
  561. months1 = dur1.asMonths();
  562. months2 = dur2.asMonths();
  563. if (
  564. Math.abs(months1) >= 1 && isInt(months1) &&
  565. Math.abs(months2) >= 1 && isInt(months2)
  566. ) {
  567. return months1 / months2;
  568. }
  569. return dur1.asDays() / dur2.asDays();
  570. }
  571. // Intelligently multiplies a duration by a number
  572. function multiplyDuration(dur, n) {
  573. var months;
  574. if (durationHasTime(dur)) {
  575. return moment.duration(dur * n);
  576. }
  577. months = dur.asMonths();
  578. if (Math.abs(months) >= 1 && isInt(months)) {
  579. return moment.duration({ months: months * n });
  580. }
  581. return moment.duration({ days: dur.asDays() * n });
  582. }
  583. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  584. function durationHasTime(dur) {
  585. return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  586. }
  587. function isNativeDate(input) {
  588. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  589. }
  590. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  591. function isTimeString(str) {
  592. return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  593. }
  594. /* Logging and Debug
  595. ----------------------------------------------------------------------------------------------------------------------*/
  596. FC.log = function() {
  597. var console = window.console;
  598. if (console && console.log) {
  599. return console.log.apply(console, arguments);
  600. }
  601. };
  602. FC.warn = function() {
  603. var console = window.console;
  604. if (console && console.warn) {
  605. return console.warn.apply(console, arguments);
  606. }
  607. else {
  608. return FC.log.apply(FC, arguments);
  609. }
  610. };
  611. /* General Utilities
  612. ----------------------------------------------------------------------------------------------------------------------*/
  613. var hasOwnPropMethod = {}.hasOwnProperty;
  614. // Merges an array of objects into a single object.
  615. // The second argument allows for an array of property names who's object values will be merged together.
  616. function mergeProps(propObjs, complexProps) {
  617. var dest = {};
  618. var i, name;
  619. var complexObjs;
  620. var j, val;
  621. var props;
  622. if (complexProps) {
  623. for (i = 0; i < complexProps.length; i++) {
  624. name = complexProps[i];
  625. complexObjs = [];
  626. // collect the trailing object values, stopping when a non-object is discovered
  627. for (j = propObjs.length - 1; j >= 0; j--) {
  628. val = propObjs[j][name];
  629. if (typeof val === 'object') {
  630. complexObjs.unshift(val);
  631. }
  632. else if (val !== undefined) {
  633. dest[name] = val; // if there were no objects, this value will be used
  634. break;
  635. }
  636. }
  637. // if the trailing values were objects, use the merged value
  638. if (complexObjs.length) {
  639. dest[name] = mergeProps(complexObjs);
  640. }
  641. }
  642. }
  643. // copy values into the destination, going from last to first
  644. for (i = propObjs.length - 1; i >= 0; i--) {
  645. props = propObjs[i];
  646. for (name in props) {
  647. if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  648. dest[name] = props[name];
  649. }
  650. }
  651. }
  652. return dest;
  653. }
  654. // Create an object that has the given prototype. Just like Object.create
  655. function createObject(proto) {
  656. var f = function() {};
  657. f.prototype = proto;
  658. return new f();
  659. }
  660. FC.createObject = createObject;
  661. function copyOwnProps(src, dest) {
  662. for (var name in src) {
  663. if (hasOwnProp(src, name)) {
  664. dest[name] = src[name];
  665. }
  666. }
  667. }
  668. function hasOwnProp(obj, name) {
  669. return hasOwnPropMethod.call(obj, name);
  670. }
  671. // Is the given value a non-object non-function value?
  672. function isAtomic(val) {
  673. return /undefined|null|boolean|number|string/.test($.type(val));
  674. }
  675. function applyAll(functions, thisObj, args) {
  676. if ($.isFunction(functions)) {
  677. functions = [ functions ];
  678. }
  679. if (functions) {
  680. var i;
  681. var ret;
  682. for (i=0; i<functions.length; i++) {
  683. ret = functions[i].apply(thisObj, args) || ret;
  684. }
  685. return ret;
  686. }
  687. }
  688. function firstDefined() {
  689. for (var i=0; i<arguments.length; i++) {
  690. if (arguments[i] !== undefined) {
  691. return arguments[i];
  692. }
  693. }
  694. }
  695. function htmlEscape(s) {
  696. return (s + '').replace(/&/g, '&amp;')
  697. .replace(/</g, '&lt;')
  698. .replace(/>/g, '&gt;')
  699. .replace(/'/g, '&#039;')
  700. .replace(/"/g, '&quot;')
  701. .replace(/\n/g, '<br />');
  702. }
  703. function stripHtmlEntities(text) {
  704. return text.replace(/&.*?;/g, '');
  705. }
  706. // Given a hash of CSS properties, returns a string of CSS.
  707. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  708. function cssToStr(cssProps) {
  709. var statements = [];
  710. $.each(cssProps, function(name, val) {
  711. if (val != null) {
  712. statements.push(name + ':' + val);
  713. }
  714. });
  715. return statements.join(';');
  716. }
  717. // Given an object hash of HTML attribute names to values,
  718. // generates a string that can be injected between < > in HTML
  719. function attrsToStr(attrs) {
  720. var parts = [];
  721. $.each(attrs, function(name, val) {
  722. if (val != null) {
  723. parts.push(name + '="' + htmlEscape(val) + '"');
  724. }
  725. });
  726. return parts.join(' ');
  727. }
  728. function capitaliseFirstLetter(str) {
  729. return str.charAt(0).toUpperCase() + str.slice(1);
  730. }
  731. function compareNumbers(a, b) { // for .sort()
  732. return a - b;
  733. }
  734. function isInt(n) {
  735. return n % 1 === 0;
  736. }
  737. // Returns a method bound to the given object context.
  738. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  739. // different contexts as identical when binding/unbinding events.
  740. function proxy(obj, methodName) {
  741. var method = obj[methodName];
  742. return function() {
  743. return method.apply(obj, arguments);
  744. };
  745. }
  746. // Returns a function, that, as long as it continues to be invoked, will not
  747. // be triggered. The function will be called after it stops being called for
  748. // N milliseconds. If `immediate` is passed, trigger the function on the
  749. // leading edge, instead of the trailing.
  750. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  751. function debounce(func, wait, immediate) {
  752. var timeout, args, context, timestamp, result;
  753. var later = function() {
  754. var last = +new Date() - timestamp;
  755. if (last < wait) {
  756. timeout = setTimeout(later, wait - last);
  757. }
  758. else {
  759. timeout = null;
  760. if (!immediate) {
  761. result = func.apply(context, args);
  762. context = args = null;
  763. }
  764. }
  765. };
  766. return function() {
  767. context = this;
  768. args = arguments;
  769. timestamp = +new Date();
  770. var callNow = immediate && !timeout;
  771. if (!timeout) {
  772. timeout = setTimeout(later, wait);
  773. }
  774. if (callNow) {
  775. result = func.apply(context, args);
  776. context = args = null;
  777. }
  778. return result;
  779. };
  780. }
  781. ;;
  782. /*
  783. GENERAL NOTE on moments throughout the *entire rest* of the codebase:
  784. All moments are assumed to be ambiguously-zoned unless otherwise noted,
  785. with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
  786. Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
  787. */
  788. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  789. var ambigTimeOrZoneRegex =
  790. /^\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+)?)?)?)?)?$/;
  791. var newMomentProto = moment.fn; // where we will attach our new methods
  792. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  793. // tell momentjs to transfer these properties upon clone
  794. var momentProperties = moment.momentProperties;
  795. momentProperties.push('_fullCalendar');
  796. momentProperties.push('_ambigTime');
  797. momentProperties.push('_ambigZone');
  798. // Creating
  799. // -------------------------------------------------------------------------------------------------
  800. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  801. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  802. // it will function as a clone (and retain the zone of the moment). Anything else will
  803. // result in a moment in the local zone.
  804. FC.moment = function() {
  805. return makeMoment(arguments);
  806. };
  807. // Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
  808. FC.moment.utc = function() {
  809. var mom = makeMoment(arguments, true);
  810. // Force it into UTC because makeMoment doesn't guarantee it
  811. // (if given a pre-existing moment for example)
  812. if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  813. mom.utc();
  814. }
  815. return mom;
  816. };
  817. // Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
  818. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  819. FC.moment.parseZone = function() {
  820. return makeMoment(arguments, true, true);
  821. };
  822. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  823. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  824. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  825. // parseAsUTC - if there is no zone information, should we parse the input in UTC?
  826. // parseZone - if there is zone information, should we force the zone of the moment?
  827. function makeMoment(args, parseAsUTC, parseZone) {
  828. var input = args[0];
  829. var isSingleString = args.length == 1 && typeof input === 'string';
  830. var isAmbigTime;
  831. var isAmbigZone;
  832. var ambigMatch;
  833. var mom;
  834. if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
  835. mom = moment.apply(null, args);
  836. }
  837. else { // "parsing" is required
  838. isAmbigTime = false;
  839. isAmbigZone = false;
  840. if (isSingleString) {
  841. if (ambigDateOfMonthRegex.test(input)) {
  842. // accept strings like '2014-05', but convert to the first of the month
  843. input += '-01';
  844. args = [ input ]; // for when we pass it on to moment's constructor
  845. isAmbigTime = true;
  846. isAmbigZone = true;
  847. }
  848. else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  849. isAmbigTime = !ambigMatch[5]; // no time part?
  850. isAmbigZone = true;
  851. }
  852. }
  853. else if ($.isArray(input)) {
  854. // arrays have no timezone information, so assume ambiguous zone
  855. isAmbigZone = true;
  856. }
  857. // otherwise, probably a string with a format
  858. if (parseAsUTC || isAmbigTime) {
  859. mom = moment.utc.apply(moment, args);
  860. }
  861. else {
  862. mom = moment.apply(null, args);
  863. }
  864. if (isAmbigTime) {
  865. mom._ambigTime = true;
  866. mom._ambigZone = true; // ambiguous time always means ambiguous zone
  867. }
  868. else if (parseZone) { // let's record the inputted zone somehow
  869. if (isAmbigZone) {
  870. mom._ambigZone = true;
  871. }
  872. else if (isSingleString) {
  873. mom.utcOffset(input); // if not a valid zone, will assign UTC
  874. }
  875. }
  876. }
  877. mom._fullCalendar = true; // flag for extended functionality
  878. return mom;
  879. }
  880. // Week Number
  881. // -------------------------------------------------------------------------------------------------
  882. // Returns the week number, considering the locale's custom week number calcuation
  883. // `weeks` is an alias for `week`
  884. newMomentProto.week = newMomentProto.weeks = function(input) {
  885. var weekCalc = this._locale._fullCalendar_weekCalc;
  886. if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
  887. return weekCalc(this);
  888. }
  889. else if (weekCalc === 'ISO') {
  890. return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
  891. }
  892. return oldMomentProto.week.apply(this, arguments); // local getter/setter
  893. };
  894. // Time-of-day
  895. // -------------------------------------------------------------------------------------------------
  896. // GETTER
  897. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  898. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  899. //
  900. // SETTER
  901. // You can supply a Duration, a Moment, or a Duration-like argument.
  902. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  903. newMomentProto.time = function(time) {
  904. // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  905. // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  906. if (!this._fullCalendar) {
  907. return oldMomentProto.time.apply(this, arguments);
  908. }
  909. if (time == null) { // getter
  910. return moment.duration({
  911. hours: this.hours(),
  912. minutes: this.minutes(),
  913. seconds: this.seconds(),
  914. milliseconds: this.milliseconds()
  915. });
  916. }
  917. else { // setter
  918. this._ambigTime = false; // mark that the moment now has a time
  919. if (!moment.isDuration(time) && !moment.isMoment(time)) {
  920. time = moment.duration(time);
  921. }
  922. // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  923. // Only for Duration times, not Moment times.
  924. var dayHours = 0;
  925. if (moment.isDuration(time)) {
  926. dayHours = Math.floor(time.asDays()) * 24;
  927. }
  928. // We need to set the individual fields.
  929. // Can't use startOf('day') then add duration. In case of DST at start of day.
  930. return this.hours(dayHours + time.hours())
  931. .minutes(time.minutes())
  932. .seconds(time.seconds())
  933. .milliseconds(time.milliseconds());
  934. }
  935. };
  936. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  937. // but preserving its YMD. A moment with a stripped time will display no time
  938. // nor timezone offset when .format() is called.
  939. newMomentProto.stripTime = function() {
  940. if (!this._ambigTime) {
  941. this.utc(true); // keepLocalTime=true (for keeping *date* value)
  942. // set time to zero
  943. this.set({
  944. hours: 0,
  945. minutes: 0,
  946. seconds: 0,
  947. ms: 0
  948. });
  949. // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  950. // which clears all ambig flags.
  951. this._ambigTime = true;
  952. this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  953. }
  954. return this; // for chaining
  955. };
  956. // Returns if the moment has a non-ambiguous time (boolean)
  957. newMomentProto.hasTime = function() {
  958. return !this._ambigTime;
  959. };
  960. // Timezone
  961. // -------------------------------------------------------------------------------------------------
  962. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  963. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  964. // timezone offset when .format() is called.
  965. newMomentProto.stripZone = function() {
  966. var wasAmbigTime;
  967. if (!this._ambigZone) {
  968. wasAmbigTime = this._ambigTime;
  969. this.utc(true); // keepLocalTime=true (for keeping date and time values)
  970. // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
  971. this._ambigTime = wasAmbigTime || false;
  972. // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  973. // which clears the ambig flags.
  974. this._ambigZone = true;
  975. }
  976. return this; // for chaining
  977. };
  978. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  979. newMomentProto.hasZone = function() {
  980. return !this._ambigZone;
  981. };
  982. // implicitly marks a zone
  983. newMomentProto.local = function(keepLocalTime) {
  984. // for when converting from ambiguously-zoned to local,
  985. // keep the time values when converting from UTC -> local
  986. oldMomentProto.local.call(this, this._ambigZone || keepLocalTime);
  987. // ensure non-ambiguous
  988. // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
  989. this._ambigTime = false;
  990. this._ambigZone = false;
  991. return this; // for chaining
  992. };
  993. // implicitly marks a zone
  994. newMomentProto.utc = function(keepLocalTime) {
  995. oldMomentProto.utc.call(this, keepLocalTime);
  996. // ensure non-ambiguous
  997. // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
  998. this._ambigTime = false;
  999. this._ambigZone = false;
  1000. return this;
  1001. };
  1002. // implicitly marks a zone (will probably get called upon .utc() and .local())
  1003. newMomentProto.utcOffset = function(tzo) {
  1004. if (tzo != null) { // setter
  1005. // these assignments needs to happen before the original zone method is called.
  1006. // I forget why, something to do with a browser crash.
  1007. this._ambigTime = false;
  1008. this._ambigZone = false;
  1009. }
  1010. return oldMomentProto.utcOffset.apply(this, arguments);
  1011. };
  1012. // Formatting
  1013. // -------------------------------------------------------------------------------------------------
  1014. newMomentProto.format = function() {
  1015. if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  1016. return formatDate(this, arguments[0]); // our extended formatting
  1017. }
  1018. if (this._ambigTime) {
  1019. return oldMomentFormat(this, 'YYYY-MM-DD');
  1020. }
  1021. if (this._ambigZone) {
  1022. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1023. }
  1024. return oldMomentProto.format.apply(this, arguments);
  1025. };
  1026. newMomentProto.toISOString = function() {
  1027. if (this._ambigTime) {
  1028. return oldMomentFormat(this, 'YYYY-MM-DD');
  1029. }
  1030. if (this._ambigZone) {
  1031. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1032. }
  1033. return oldMomentProto.toISOString.apply(this, arguments);
  1034. };
  1035. ;;
  1036. // Single Date Formatting
  1037. // -------------------------------------------------------------------------------------------------
  1038. // call this if you want Moment's original format method to be used
  1039. function oldMomentFormat(mom, formatStr) {
  1040. return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  1041. }
  1042. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1043. // additional token.
  1044. function formatDate(date, formatStr) {
  1045. return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1046. }
  1047. function formatDateWithChunks(date, chunks) {
  1048. var s = '';
  1049. var i;
  1050. for (i=0; i<chunks.length; i++) {
  1051. s += formatDateWithChunk(date, chunks[i]);
  1052. }
  1053. return s;
  1054. }
  1055. // addition formatting tokens we want recognized
  1056. var tokenOverrides = {
  1057. t: function(date) { // "a" or "p"
  1058. return oldMomentFormat(date, 'a').charAt(0);
  1059. },
  1060. T: function(date) { // "A" or "P"
  1061. return oldMomentFormat(date, 'A').charAt(0);
  1062. }
  1063. };
  1064. function formatDateWithChunk(date, chunk) {
  1065. var token;
  1066. var maybeStr;
  1067. if (typeof chunk === 'string') { // a literal string
  1068. return chunk;
  1069. }
  1070. else if ((token = chunk.token)) { // a token, like "YYYY"
  1071. if (tokenOverrides[token]) {
  1072. return tokenOverrides[token](date); // use our custom token
  1073. }
  1074. return oldMomentFormat(date, token);
  1075. }
  1076. else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  1077. maybeStr = formatDateWithChunks(date, chunk.maybe);
  1078. if (maybeStr.match(/[1-9]/)) {
  1079. return maybeStr;
  1080. }
  1081. }
  1082. return '';
  1083. }
  1084. // Date Range Formatting
  1085. // -------------------------------------------------------------------------------------------------
  1086. // TODO: make it work with timezone offset
  1087. // Using a formatting string meant for a single date, generate a range string, like
  1088. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  1089. // If the dates are the same as far as the format string is concerned, just return a single
  1090. // rendering of one date, without any separator.
  1091. function formatRange(date1, date2, formatStr, separator, isRTL) {
  1092. var localeData;
  1093. date1 = FC.moment.parseZone(date1);
  1094. date2 = FC.moment.parseZone(date2);
  1095. localeData = date1.localeData();
  1096. // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  1097. formatStr = localeData.longDateFormat(formatStr) || formatStr;
  1098. // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  1099. // or non-zero areas in Moment's localized format strings.
  1100. separator = separator || ' - ';
  1101. return formatRangeWithChunks(
  1102. date1,
  1103. date2,
  1104. getFormatStringChunks(formatStr),
  1105. separator,
  1106. isRTL
  1107. );
  1108. }
  1109. FC.formatRange = formatRange; // expose
  1110. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  1111. var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk
  1112. var unzonedDate2 = date2.clone().stripZone(); // "
  1113. var chunkStr; // the rendering of the chunk
  1114. var leftI;
  1115. var leftStr = '';
  1116. var rightI;
  1117. var rightStr = '';
  1118. var middleI;
  1119. var middleStr1 = '';
  1120. var middleStr2 = '';
  1121. var middleStr = '';
  1122. // Start at the leftmost side of the formatting string and continue until you hit a token
  1123. // that is not the same between dates.
  1124. for (leftI=0; leftI<chunks.length; leftI++) {
  1125. chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]);
  1126. if (chunkStr === false) {
  1127. break;
  1128. }
  1129. leftStr += chunkStr;
  1130. }
  1131. // Similarly, start at the rightmost side of the formatting string and move left
  1132. for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  1133. chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]);
  1134. if (chunkStr === false) {
  1135. break;
  1136. }
  1137. rightStr = chunkStr + rightStr;
  1138. }
  1139. // The area in the middle is different for both of the dates.
  1140. // Collect them distinctly so we can jam them together later.
  1141. for (middleI=leftI; middleI<=rightI; middleI++) {
  1142. middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  1143. middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  1144. }
  1145. if (middleStr1 || middleStr2) {
  1146. if (isRTL) {
  1147. middleStr = middleStr2 + separator + middleStr1;
  1148. }
  1149. else {
  1150. middleStr = middleStr1 + separator + middleStr2;
  1151. }
  1152. }
  1153. return leftStr + middleStr + rightStr;
  1154. }
  1155. var similarUnitMap = {
  1156. Y: 'year',
  1157. M: 'month',
  1158. D: 'day', // day of month
  1159. d: 'day', // day of week
  1160. // prevents a separator between anything time-related...
  1161. A: 'second', // AM/PM
  1162. a: 'second', // am/pm
  1163. T: 'second', // A/P
  1164. t: 'second', // a/p
  1165. H: 'second', // hour (24)
  1166. h: 'second', // hour (12)
  1167. m: 'second', // minute
  1168. s: 'second' // second
  1169. };
  1170. // TODO: week maybe?
  1171. // Given a formatting chunk, and given that both dates are similar in the regard the
  1172. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  1173. function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) {
  1174. var token;
  1175. var unit;
  1176. if (typeof chunk === 'string') { // a literal string
  1177. return chunk;
  1178. }
  1179. else if ((token = chunk.token)) {
  1180. unit = similarUnitMap[token.charAt(0)];
  1181. // are the dates the same for this unit of measurement?
  1182. // use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
  1183. if (unit && unzonedDate1.isSame(unzonedDate2, unit)) {
  1184. return oldMomentFormat(date1, token); // would be the same if we used `date2`
  1185. // BTW, don't support custom tokens
  1186. }
  1187. }
  1188. return false; // the chunk is NOT the same for the two dates
  1189. // BTW, don't support splitting on non-zero areas
  1190. }
  1191. // Chunking Utils
  1192. // -------------------------------------------------------------------------------------------------
  1193. var formatStringChunkCache = {};
  1194. function getFormatStringChunks(formatStr) {
  1195. if (formatStr in formatStringChunkCache) {
  1196. return formatStringChunkCache[formatStr];
  1197. }
  1198. return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  1199. }
  1200. // Break the formatting string into an array of chunks
  1201. function chunkFormatString(formatStr) {
  1202. var chunks = [];
  1203. var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  1204. var match;
  1205. while ((match = chunker.exec(formatStr))) {
  1206. if (match[1]) { // a literal string inside [ ... ]
  1207. chunks.push(match[1]);
  1208. }
  1209. else if (match[2]) { // non-zero formatting inside ( ... )
  1210. chunks.push({ maybe: chunkFormatString(match[2]) });
  1211. }
  1212. else if (match[3]) { // a formatting token
  1213. chunks.push({ token: match[3] });
  1214. }
  1215. else if (match[5]) { // an unenclosed literal string
  1216. chunks.push(match[5]);
  1217. }
  1218. }
  1219. return chunks;
  1220. }
  1221. // Misc Utils
  1222. // -------------------------------------------------------------------------------------------------
  1223. // granularity only goes up until day
  1224. // TODO: unify with similarUnitMap
  1225. var tokenGranularities = {
  1226. Y: { value: 1, unit: 'year' },
  1227. M: { value: 2, unit: 'month' },
  1228. W: { value: 3, unit: 'week' },
  1229. w: { value: 3, unit: 'week' },
  1230. D: { value: 4, unit: 'day' }, // day of month
  1231. d: { value: 4, unit: 'day' } // day of week
  1232. };
  1233. // returns a unit string, either 'year', 'month', 'day', or null
  1234. // for the most granular formatting token in the string.
  1235. FC.queryMostGranularFormatUnit = function(formatStr) {
  1236. var chunks = getFormatStringChunks(formatStr);
  1237. var i, chunk;
  1238. var candidate;
  1239. var best;
  1240. for (i = 0; i < chunks.length; i++) {
  1241. chunk = chunks[i];
  1242. if (chunk.token) {
  1243. candidate = tokenGranularities[chunk.token.charAt(0)];
  1244. if (candidate) {
  1245. if (!best || candidate.value > best.value) {
  1246. best = candidate;
  1247. }
  1248. }
  1249. }
  1250. }
  1251. if (best) {
  1252. return best.unit;
  1253. }
  1254. return null;
  1255. };
  1256. ;;
  1257. FC.Class = Class; // export
  1258. // Class that all other classes will inherit from
  1259. function Class() { }
  1260. // Called on a class to create a subclass.
  1261. // Last argument contains instance methods. Any argument before the last are considered mixins.
  1262. Class.extend = function() {
  1263. var len = arguments.length;
  1264. var i;
  1265. var members;
  1266. for (i = 0; i < len; i++) {
  1267. members = arguments[i];
  1268. if (i < len - 1) { // not the last argument?
  1269. mixIntoClass(this, members);
  1270. }
  1271. }
  1272. return extendClass(this, members || {}); // members will be undefined if no arguments
  1273. };
  1274. // Adds new member variables/methods to the class's prototype.
  1275. // Can be called with another class, or a plain object hash containing new members.
  1276. Class.mixin = function(members) {
  1277. mixIntoClass(this, members);
  1278. };
  1279. function extendClass(superClass, members) {
  1280. var subClass;
  1281. // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
  1282. if (hasOwnProp(members, 'constructor')) {
  1283. subClass = members.constructor;
  1284. }
  1285. if (typeof subClass !== 'function') {
  1286. subClass = members.constructor = function() {
  1287. superClass.apply(this, arguments);
  1288. };
  1289. }
  1290. // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
  1291. subClass.prototype = createObject(superClass.prototype);
  1292. // copy each member variable/method onto the the subclass's prototype
  1293. copyOwnProps(members, subClass.prototype);
  1294. // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
  1295. copyOwnProps(superClass, subClass);
  1296. return subClass;
  1297. }
  1298. function mixIntoClass(theClass, members) {
  1299. copyOwnProps(members, theClass.prototype);
  1300. }
  1301. ;;
  1302. /*
  1303. Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
  1304. With the added non-standard feature of synchronously executing handlers on resolved promises,
  1305. which doesn't always happen otherwise (esp with nested .then handlers!?),
  1306. so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
  1307. TODO: write tests and more comments
  1308. */
  1309. function Promise(executor) {
  1310. var deferred = $.Deferred();
  1311. var promise = deferred.promise();
  1312. if (typeof executor === 'function') {
  1313. executor(
  1314. function(value) { // resolve
  1315. if (Promise.immediate) {
  1316. promise._value = value;
  1317. }
  1318. deferred.resolve(value);
  1319. },
  1320. function() { // reject
  1321. deferred.reject();
  1322. }
  1323. );
  1324. }
  1325. if (Promise.immediate) {
  1326. var origThen = promise.then;
  1327. promise.then = function(onFulfilled, onRejected) {
  1328. var state = promise.state();
  1329. if (state === 'resolved') {
  1330. if (typeof onFulfilled === 'function') {
  1331. return Promise.resolve(onFulfilled(promise._value));
  1332. }
  1333. }
  1334. else if (state === 'rejected') {
  1335. if (typeof onRejected === 'function') {
  1336. onRejected();
  1337. return promise; // already rejected
  1338. }
  1339. }
  1340. return origThen.call(promise, onFulfilled, onRejected);
  1341. };
  1342. }
  1343. return promise; // instanceof Promise will break :( TODO: make Promise a real class
  1344. }
  1345. FC.Promise = Promise;
  1346. Promise.immediate = true;
  1347. Promise.resolve = function(value) {
  1348. if (value && typeof value.resolve === 'function') {
  1349. return value.promise();
  1350. }
  1351. if (value && typeof value.then === 'function') {
  1352. return value;
  1353. }
  1354. else {
  1355. var deferred = $.Deferred().resolve(value);
  1356. var promise = deferred.promise();
  1357. if (Promise.immediate) {
  1358. var origThen = promise.then;
  1359. promise._value = value;
  1360. promise.then = function(onFulfilled, onRejected) {
  1361. if (typeof onFulfilled === 'function') {
  1362. return Promise.resolve(onFulfilled(value));
  1363. }
  1364. return origThen.call(promise, onFulfilled, onRejected);
  1365. };
  1366. }
  1367. return promise;
  1368. }
  1369. };
  1370. Promise.reject = function() {
  1371. return $.Deferred().reject().promise();
  1372. };
  1373. Promise.all = function(inputs) {
  1374. var hasAllValues = false;
  1375. var values;
  1376. var i, input;
  1377. if (Promise.immediate) {
  1378. hasAllValues = true;
  1379. values = [];
  1380. for (i = 0; i < inputs.length; i++) {
  1381. input = inputs[i];
  1382. if (input && typeof input.state === 'function' && input.state() === 'resolved' && ('_value' in input)) {
  1383. values.push(input._value);
  1384. }
  1385. else if (input && typeof input.then === 'function') {
  1386. hasAllValues = false;
  1387. break;
  1388. }
  1389. else {
  1390. values.push(input);
  1391. }
  1392. }
  1393. }
  1394. if (hasAllValues) {
  1395. return Promise.resolve(values);
  1396. }
  1397. else {
  1398. return $.when.apply($.when, inputs).then(function() {
  1399. return $.when($.makeArray(arguments));
  1400. });
  1401. }
  1402. };
  1403. ;;
  1404. // TODO: write tests and clean up code
  1405. function TaskQueue(debounceWait) {
  1406. var q = []; // array of runFuncs
  1407. function addTask(taskFunc) {
  1408. return new Promise(function(resolve) {
  1409. // should run this function when it's taskFunc's turn to run.
  1410. // responsible for popping itself off the queue.
  1411. var runFunc = function() {
  1412. Promise.resolve(taskFunc()) // result might be async, coerce to promise
  1413. .then(resolve) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
  1414. .then(function() {
  1415. q.shift(); // pop itself off
  1416. // run the next task, if any
  1417. if (q.length) {
  1418. q[0]();
  1419. }
  1420. });
  1421. };
  1422. // always put the task at the end of the queue, BEFORE running the task
  1423. q.push(runFunc);
  1424. // if it's the only task in the queue, run immediately
  1425. if (q.length === 1) {
  1426. runFunc();
  1427. }
  1428. });
  1429. }
  1430. this.add = // potentially debounce, for the public method
  1431. typeof debounceWait === 'number' ?
  1432. debounce(addTask, debounceWait) :
  1433. addTask; // if not a number (null/undefined/false), no debounce at all
  1434. this.addQuickly = addTask; // guaranteed no debounce
  1435. }
  1436. FC.TaskQueue = TaskQueue;
  1437. /*
  1438. q = new TaskQueue();
  1439. function work(i) {
  1440. return q.push(function() {
  1441. trigger();
  1442. console.log('work' + i);
  1443. });
  1444. }
  1445. var cnt = 0;
  1446. function trigger() {
  1447. if (cnt < 5) {
  1448. cnt++;
  1449. work(cnt);
  1450. }
  1451. }
  1452. work(9);
  1453. */
  1454. ;;
  1455. var EmitterMixin = FC.EmitterMixin = {
  1456. // jQuery-ification via $(this) allows a non-DOM object to have
  1457. // the same event handling capabilities (including namespaces).
  1458. on: function(types, handler) {
  1459. $(this).on(types, this._prepareIntercept(handler));
  1460. return this; // for chaining
  1461. },
  1462. one: function(types, handler) {
  1463. $(this).one(types, this._prepareIntercept(handler));
  1464. return this; // for chaining
  1465. },
  1466. _prepareIntercept: function(handler) {
  1467. // handlers are always called with an "event" object as their first param.
  1468. // sneak the `this` context and arguments into the extra parameter object
  1469. // and forward them on to the original handler.
  1470. var intercept = function(ev, extra) {
  1471. return handler.apply(
  1472. extra.context || this,
  1473. extra.args || []
  1474. );
  1475. };
  1476. // mimick jQuery's internal "proxy" system (risky, I know)
  1477. // causing all functions with the same .guid to appear to be the same.
  1478. // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
  1479. // this is needed for calling .off with the original non-intercept handler.
  1480. if (!handler.guid) {
  1481. handler.guid = $.guid++;
  1482. }
  1483. intercept.guid = handler.guid;
  1484. return intercept;
  1485. },
  1486. off: function(types, handler) {
  1487. $(this).off(types, handler);
  1488. return this; // for chaining
  1489. },
  1490. trigger: function(types) {
  1491. var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
  1492. // pass in "extra" info to the intercept
  1493. $(this).triggerHandler(types, { args: args });
  1494. return this; // for chaining
  1495. },
  1496. triggerWith: function(types, context, args) {
  1497. // `triggerHandler` is less reliant on the DOM compared to `trigger`.
  1498. // pass in "extra" info to the intercept.
  1499. $(this).triggerHandler(types, { context: context, args: args });
  1500. return this; // for chaining
  1501. }
  1502. };
  1503. ;;
  1504. /*
  1505. Utility methods for easily listening to events on another object,
  1506. and more importantly, easily unlistening from them.
  1507. */
  1508. var ListenerMixin = FC.ListenerMixin = (function() {
  1509. var guid = 0;
  1510. var ListenerMixin = {
  1511. listenerId: null,
  1512. /*
  1513. Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
  1514. The `callback` will be called with the `this` context of the object that .listenTo is being called on.
  1515. Can be called:
  1516. .listenTo(other, eventName, callback)
  1517. OR
  1518. .listenTo(other, {
  1519. eventName1: callback1,
  1520. eventName2: callback2
  1521. })
  1522. */
  1523. listenTo: function(other, arg, callback) {
  1524. if (typeof arg === 'object') { // given dictionary of callbacks
  1525. for (var eventName in arg) {
  1526. if (arg.hasOwnProperty(eventName)) {
  1527. this.listenTo(other, eventName, arg[eventName]);
  1528. }
  1529. }
  1530. }
  1531. else if (typeof arg === 'string') {
  1532. other.on(
  1533. arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
  1534. $.proxy(callback, this) // always use `this` context
  1535. // the usually-undesired jQuery guid behavior doesn't matter,
  1536. // because we always unbind via namespace
  1537. );
  1538. }
  1539. },
  1540. /*
  1541. Causes the current object to stop listening to events on the `other` object.
  1542. `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
  1543. */
  1544. stopListeningTo: function(other, eventName) {
  1545. other.off((eventName || '') + '.' + this.getListenerNamespace());
  1546. },
  1547. /*
  1548. Returns a string, unique to this object, to be used for event namespacing
  1549. */
  1550. getListenerNamespace: function() {
  1551. if (this.listenerId == null) {
  1552. this.listenerId = guid++;
  1553. }
  1554. return '_listener' + this.listenerId;
  1555. }
  1556. };
  1557. return ListenerMixin;
  1558. })();
  1559. ;;
  1560. // simple class for toggle a `isIgnoringMouse` flag on delay
  1561. // initMouseIgnoring must first be called, with a millisecond delay setting.
  1562. var MouseIgnorerMixin = {
  1563. isIgnoringMouse: false, // bool
  1564. delayUnignoreMouse: null, // method
  1565. initMouseIgnoring: function(delay) {
  1566. this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
  1567. },
  1568. // temporarily ignore mouse actions on segments
  1569. tempIgnoreMouse: function() {
  1570. this.isIgnoringMouse = true;
  1571. this.delayUnignoreMouse();
  1572. },
  1573. // delayUnignoreMouse eventually calls this
  1574. unignoreMouse: function() {
  1575. this.isIgnoringMouse = false;
  1576. }
  1577. };
  1578. ;;
  1579. /* A rectangular panel that is absolutely positioned over other content
  1580. ------------------------------------------------------------------------------------------------------------------------
  1581. Options:
  1582. - className (string)
  1583. - content (HTML string or jQuery element set)
  1584. - parentEl
  1585. - top
  1586. - left
  1587. - right (the x coord of where the right edge should be. not a "CSS" right)
  1588. - autoHide (boolean)
  1589. - show (callback)
  1590. - hide (callback)
  1591. */
  1592. var Popover = Class.extend(ListenerMixin, {
  1593. isHidden: true,
  1594. options: null,
  1595. el: null, // the container element for the popover. generated by this object
  1596. margin: 10, // the space required between the popover and the edges of the scroll container
  1597. constructor: function(options) {
  1598. this.options = options || {};
  1599. },
  1600. // Shows the popover on the specified position. Renders it if not already
  1601. show: function() {
  1602. if (this.isHidden) {
  1603. if (!this.el) {
  1604. this.render();
  1605. }
  1606. this.el.show();
  1607. this.position();
  1608. this.isHidden = false;
  1609. this.trigger('show');
  1610. }
  1611. },
  1612. // Hides the popover, through CSS, but does not remove it from the DOM
  1613. hide: function() {
  1614. if (!this.isHidden) {
  1615. this.el.hide();
  1616. this.isHidden = true;
  1617. this.trigger('hide');
  1618. }
  1619. },
  1620. // Creates `this.el` and renders content inside of it
  1621. render: function() {
  1622. var _this = this;
  1623. var options = this.options;
  1624. this.el = $('<div class="fc-popover"/>')
  1625. .addClass(options.className || '')
  1626. .css({
  1627. // position initially to the top left to avoid creating scrollbars
  1628. top: 0,
  1629. left: 0
  1630. })
  1631. .append(options.content)
  1632. .appendTo(options.parentEl);
  1633. // when a click happens on anything inside with a 'fc-close' className, hide the popover
  1634. this.el.on('click', '.fc-close', function() {
  1635. _this.hide();
  1636. });
  1637. if (options.autoHide) {
  1638. this.listenTo($(document), 'mousedown', this.documentMousedown);
  1639. }
  1640. },
  1641. // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  1642. documentMousedown: function(ev) {
  1643. // only hide the popover if the click happened outside the popover
  1644. if (this.el && !$(ev.target).closest(this.el).length) {
  1645. this.hide();
  1646. }
  1647. },
  1648. // Hides and unregisters any handlers
  1649. removeElement: function() {
  1650. this.hide();
  1651. if (this.el) {
  1652. this.el.remove();
  1653. this.el = null;
  1654. }
  1655. this.stopListeningTo($(document), 'mousedown');
  1656. },
  1657. // Positions the popover optimally, using the top/left/right options
  1658. position: function() {
  1659. var options = this.options;
  1660. var origin = this.el.offsetParent().offset();
  1661. var width = this.el.outerWidth();
  1662. var height = this.el.outerHeight();
  1663. var windowEl = $(window);
  1664. var viewportEl = getScrollParent(this.el);
  1665. var viewportTop;
  1666. var viewportLeft;
  1667. var viewportOffset;
  1668. var top; // the "position" (not "offset") values for the popover
  1669. var left; //
  1670. // compute top and left
  1671. top = options.top || 0;
  1672. if (options.left !== undefined) {
  1673. left = options.left;
  1674. }
  1675. else if (options.right !== undefined) {
  1676. left = options.right - width; // derive the left value from the right value
  1677. }
  1678. else {
  1679. left = 0;
  1680. }
  1681. if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  1682. viewportEl = windowEl;
  1683. viewportTop = 0; // the window is always at the top left
  1684. viewportLeft = 0; // (and .offset() won't work if called here)
  1685. }
  1686. else {
  1687. viewportOffset = viewportEl.offset();
  1688. viewportTop = viewportOffset.top;
  1689. viewportLeft = viewportOffset.left;
  1690. }
  1691. // if the window is scrolled, it causes the visible area to be further down
  1692. viewportTop += windowEl.scrollTop();
  1693. viewportLeft += windowEl.scrollLeft();
  1694. // constrain to the view port. if constrained by two edges, give precedence to top/left
  1695. if (options.viewportConstrain !== false) {
  1696. top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  1697. top = Math.max(top, viewportTop + this.margin);
  1698. left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  1699. left = Math.max(left, viewportLeft + this.margin);
  1700. }
  1701. this.el.css({
  1702. top: top - origin.top,
  1703. left: left - origin.left
  1704. });
  1705. },
  1706. // Triggers a callback. Calls a function in the option hash of the same name.
  1707. // Arguments beyond the first `name` are forwarded on.
  1708. // TODO: better code reuse for this. Repeat code
  1709. trigger: function(name) {
  1710. if (this.options[name]) {
  1711. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1712. }
  1713. }
  1714. });
  1715. ;;
  1716. /*
  1717. A cache for the left/right/top/bottom/width/height values for one or more elements.
  1718. Works with both offset (from topleft document) and position (from offsetParent).
  1719. options:
  1720. - els
  1721. - isHorizontal
  1722. - isVertical
  1723. */
  1724. var CoordCache = FC.CoordCache = Class.extend({
  1725. els: null, // jQuery set (assumed to be siblings)
  1726. forcedOffsetParentEl: null, // options can override the natural offsetParent
  1727. origin: null, // {left,top} position of offsetParent of els
  1728. boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
  1729. isHorizontal: false, // whether to query for left/right/width
  1730. isVertical: false, // whether to query for top/bottom/height
  1731. // arrays of coordinates (offsets from topleft of document)
  1732. lefts: null,
  1733. rights: null,
  1734. tops: null,
  1735. bottoms: null,
  1736. constructor: function(options) {
  1737. this.els = $(options.els);
  1738. this.isHorizontal = options.isHorizontal;
  1739. this.isVertical = options.isVertical;
  1740. this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
  1741. },
  1742. // Queries the els for coordinates and stores them.
  1743. // Call this method before using and of the get* methods below.
  1744. build: function() {
  1745. var offsetParentEl = this.forcedOffsetParentEl;
  1746. if (!offsetParentEl && this.els.length > 0) {
  1747. offsetParentEl = this.els.eq(0).offsetParent();
  1748. }
  1749. this.origin = offsetParentEl ?
  1750. offsetParentEl.offset() :
  1751. null;
  1752. this.boundingRect = this.queryBoundingRect();
  1753. if (this.isHorizontal) {
  1754. this.buildElHorizontals();
  1755. }
  1756. if (this.isVertical) {
  1757. this.buildElVerticals();
  1758. }
  1759. },
  1760. // Destroys all internal data about coordinates, freeing memory
  1761. clear: function() {
  1762. this.origin = null;
  1763. this.boundingRect = null;
  1764. this.lefts = null;
  1765. this.rights = null;
  1766. this.tops = null;
  1767. this.bottoms = null;
  1768. },
  1769. // When called, if coord caches aren't built, builds them
  1770. ensureBuilt: function() {
  1771. if (!this.origin) {
  1772. this.build();
  1773. }
  1774. },
  1775. // Populates the left/right internal coordinate arrays
  1776. buildElHorizontals: function() {
  1777. var lefts = [];
  1778. var rights = [];
  1779. this.els.each(function(i, node) {
  1780. var el = $(node);
  1781. var left = el.offset().left;
  1782. var width = el.outerWidth();
  1783. lefts.push(left);
  1784. rights.push(left + width);
  1785. });
  1786. this.lefts = lefts;
  1787. this.rights = rights;
  1788. },
  1789. // Populates the top/bottom internal coordinate arrays
  1790. buildElVerticals: function() {
  1791. var tops = [];
  1792. var bottoms = [];
  1793. this.els.each(function(i, node) {
  1794. var el = $(node);
  1795. var top = el.offset().top;
  1796. var height = el.outerHeight();
  1797. tops.push(top);
  1798. bottoms.push(top + height);
  1799. });
  1800. this.tops = tops;
  1801. this.bottoms = bottoms;
  1802. },
  1803. // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
  1804. // If no intersection is made, returns undefined.
  1805. getHorizontalIndex: function(leftOffset) {
  1806. this.ensureBuilt();
  1807. var lefts = this.lefts;
  1808. var rights = this.rights;
  1809. var len = lefts.length;
  1810. var i;
  1811. for (i = 0; i < len; i++) {
  1812. if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
  1813. return i;
  1814. }
  1815. }
  1816. },
  1817. // Given a top offset (from document top), returns the index of the el that it vertically intersects.
  1818. // If no intersection is made, returns undefined.
  1819. getVerticalIndex: function(topOffset) {
  1820. this.ensureBuilt();
  1821. var tops = this.tops;
  1822. var bottoms = this.bottoms;
  1823. var len = tops.length;
  1824. var i;
  1825. for (i = 0; i < len; i++) {
  1826. if (topOffset >= tops[i] && topOffset < bottoms[i]) {
  1827. return i;
  1828. }
  1829. }
  1830. },
  1831. // Gets the left offset (from document left) of the element at the given index
  1832. getLeftOffset: function(leftIndex) {
  1833. this.ensureBuilt();
  1834. return this.lefts[leftIndex];
  1835. },
  1836. // Gets the left position (from offsetParent left) of the element at the given index
  1837. getLeftPosition: function(leftIndex) {
  1838. this.ensureBuilt();
  1839. return this.lefts[leftIndex] - this.origin.left;
  1840. },
  1841. // Gets the right offset (from document left) of the element at the given index.
  1842. // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
  1843. getRightOffset: function(leftIndex) {
  1844. this.ensureBuilt();
  1845. return this.rights[leftIndex];
  1846. },
  1847. // Gets the right position (from offsetParent left) of the element at the given index.
  1848. // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
  1849. getRightPosition: function(leftIndex) {
  1850. this.ensureBuilt();
  1851. return this.rights[leftIndex] - this.origin.left;
  1852. },
  1853. // Gets the width of the element at the given index
  1854. getWidth: function(leftIndex) {
  1855. this.ensureBuilt();
  1856. return this.rights[leftIndex] - this.lefts[leftIndex];
  1857. },
  1858. // Gets the top offset (from document top) of the element at the given index
  1859. getTopOffset: function(topIndex) {
  1860. this.ensureBuilt();
  1861. return this.tops[topIndex];
  1862. },
  1863. // Gets the top position (from offsetParent top) of the element at the given position
  1864. getTopPosition: function(topIndex) {
  1865. this.ensureBuilt();
  1866. return this.tops[topIndex] - this.origin.top;
  1867. },
  1868. // Gets the bottom offset (from the document top) of the element at the given index.
  1869. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
  1870. getBottomOffset: function(topIndex) {
  1871. this.ensureBuilt();
  1872. return this.bottoms[topIndex];
  1873. },
  1874. // Gets the bottom position (from the offsetParent top) of the element at the given index.
  1875. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
  1876. getBottomPosition: function(topIndex) {
  1877. this.ensureBuilt();
  1878. return this.bottoms[topIndex] - this.origin.top;
  1879. },
  1880. // Gets the height of the element at the given index
  1881. getHeight: function(topIndex) {
  1882. this.ensureBuilt();
  1883. return this.bottoms[topIndex] - this.tops[topIndex];
  1884. },
  1885. // Bounding Rect
  1886. // TODO: decouple this from CoordCache
  1887. // Compute and return what the elements' bounding rectangle is, from the user's perspective.
  1888. // Right now, only returns a rectangle if constrained by an overflow:scroll element.
  1889. // Returns null if there are no elements
  1890. queryBoundingRect: function() {
  1891. var scrollParentEl;
  1892. if (this.els.length > 0) {
  1893. scrollParentEl = getScrollParent(this.els.eq(0));
  1894. if (!scrollParentEl.is(document)) {
  1895. return getClientRect(scrollParentEl);
  1896. }
  1897. }
  1898. return null;
  1899. },
  1900. isPointInBounds: function(leftOffset, topOffset) {
  1901. return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
  1902. },
  1903. isLeftInBounds: function(leftOffset) {
  1904. return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
  1905. },
  1906. isTopInBounds: function(topOffset) {
  1907. return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
  1908. }
  1909. });
  1910. ;;
  1911. /* Tracks a drag's mouse movement, firing various handlers
  1912. ----------------------------------------------------------------------------------------------------------------------*/
  1913. // TODO: use Emitter
  1914. var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
  1915. options: null,
  1916. subjectEl: null,
  1917. // coordinates of the initial mousedown
  1918. originX: null,
  1919. originY: null,
  1920. // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
  1921. // TODO: do this for wrappers that have overflow:hidden as well.
  1922. scrollEl: null,
  1923. isInteracting: false,
  1924. isDistanceSurpassed: false,
  1925. isDelayEnded: false,
  1926. isDragging: false,
  1927. isTouch: false,
  1928. delay: null,
  1929. delayTimeoutId: null,
  1930. minDistance: null,
  1931. handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
  1932. constructor: function(options) {
  1933. this.options = options || {};
  1934. this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
  1935. this.initMouseIgnoring(500);
  1936. },
  1937. // Interaction (high-level)
  1938. // -----------------------------------------------------------------------------------------------------------------
  1939. startInteraction: function(ev, extraOptions) {
  1940. var isTouch = getEvIsTouch(ev);
  1941. if (ev.type === 'mousedown') {
  1942. if (this.isIgnoringMouse) {
  1943. return;
  1944. }
  1945. else if (!isPrimaryMouseButton(ev)) {
  1946. return;
  1947. }
  1948. else {
  1949. ev.preventDefault(); // prevents native selection in most browsers
  1950. }
  1951. }
  1952. if (!this.isInteracting) {
  1953. // process options
  1954. extraOptions = extraOptions || {};
  1955. this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
  1956. this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
  1957. this.subjectEl = this.options.subjectEl;
  1958. this.isInteracting = true;
  1959. this.isTouch = isTouch;
  1960. this.isDelayEnded = false;
  1961. this.isDistanceSurpassed = false;
  1962. this.originX = getEvX(ev);
  1963. this.originY = getEvY(ev);
  1964. this.scrollEl = getScrollParent($(ev.target));
  1965. this.bindHandlers();
  1966. this.initAutoScroll();
  1967. this.handleInteractionStart(ev);
  1968. this.startDelay(ev);
  1969. if (!this.minDistance) {
  1970. this.handleDistanceSurpassed(ev);
  1971. }
  1972. }
  1973. },
  1974. handleInteractionStart: function(ev) {
  1975. this.trigger('interactionStart', ev);
  1976. },
  1977. endInteraction: function(ev, isCancelled) {
  1978. if (this.isInteracting) {
  1979. this.endDrag(ev);
  1980. if (this.delayTimeoutId) {
  1981. clearTimeout(this.delayTimeoutId);
  1982. this.delayTimeoutId = null;
  1983. }
  1984. this.destroyAutoScroll();
  1985. this.unbindHandlers();
  1986. this.isInteracting = false;
  1987. this.handleInteractionEnd(ev, isCancelled);
  1988. // a touchstart+touchend on the same element will result in the following addition simulated events:
  1989. // mouseover + mouseout + click
  1990. // let's ignore these bogus events
  1991. if (this.isTouch) {
  1992. this.tempIgnoreMouse();
  1993. }
  1994. }
  1995. },
  1996. handleInteractionEnd: function(ev, isCancelled) {
  1997. this.trigger('interactionEnd', ev, isCancelled || false);
  1998. },
  1999. // Binding To DOM
  2000. // -----------------------------------------------------------------------------------------------------------------
  2001. bindHandlers: function() {
  2002. var _this = this;
  2003. var touchStartIgnores = 1;
  2004. if (this.isTouch) {
  2005. this.listenTo($(document), {
  2006. touchmove: this.handleTouchMove,
  2007. touchend: this.endInteraction,
  2008. touchcancel: this.endInteraction,
  2009. // Sometimes touchend doesn't fire
  2010. // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
  2011. // If another touchstart happens, we know it's bogus, so cancel the drag.
  2012. // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
  2013. touchstart: function(ev) {
  2014. if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
  2015. touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
  2016. }
  2017. else {
  2018. _this.endInteraction(ev, true); // isCancelled=true
  2019. }
  2020. }
  2021. });
  2022. // listen to ALL scroll actions on the page
  2023. if (
  2024. !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
  2025. this.scrollEl // otherwise, attach a single handler to this
  2026. ) {
  2027. this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
  2028. }
  2029. }
  2030. else {
  2031. this.listenTo($(document), {
  2032. mousemove: this.handleMouseMove,
  2033. mouseup: this.endInteraction
  2034. });
  2035. }
  2036. this.listenTo($(document), {
  2037. selectstart: preventDefault, // don't allow selection while dragging
  2038. contextmenu: preventDefault // long taps would open menu on Chrome dev tools
  2039. });
  2040. },
  2041. unbindHandlers: function() {
  2042. this.stopListeningTo($(document));
  2043. // unbind scroll listening
  2044. unbindAnyScroll(this.handleTouchScrollProxy);
  2045. if (this.scrollEl) {
  2046. this.stopListeningTo(this.scrollEl, 'scroll');
  2047. }
  2048. },
  2049. // Drag (high-level)
  2050. // -----------------------------------------------------------------------------------------------------------------
  2051. // extraOptions ignored if drag already started
  2052. startDrag: function(ev, extraOptions) {
  2053. this.startInteraction(ev, extraOptions); // ensure interaction began
  2054. if (!this.isDragging) {
  2055. this.isDragging = true;
  2056. this.handleDragStart(ev);
  2057. }
  2058. },
  2059. handleDragStart: function(ev) {
  2060. this.trigger('dragStart', ev);
  2061. },
  2062. handleMove: function(ev) {
  2063. var dx = getEvX(ev) - this.originX;
  2064. var dy = getEvY(ev) - this.originY;
  2065. var minDistance = this.minDistance;
  2066. var distanceSq; // current distance from the origin, squared
  2067. if (!this.isDistanceSurpassed) {
  2068. distanceSq = dx * dx + dy * dy;
  2069. if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  2070. this.handleDistanceSurpassed(ev);
  2071. }
  2072. }
  2073. if (this.isDragging) {
  2074. this.handleDrag(dx, dy, ev);
  2075. }
  2076. },
  2077. // Called while the mouse is being moved and when we know a legitimate drag is taking place
  2078. handleDrag: function(dx, dy, ev) {
  2079. this.trigger('drag', dx, dy, ev);
  2080. this.updateAutoScroll(ev); // will possibly cause scrolling
  2081. },
  2082. endDrag: function(ev) {
  2083. if (this.isDragging) {
  2084. this.isDragging = false;
  2085. this.handleDragEnd(ev);
  2086. }
  2087. },
  2088. handleDragEnd: function(ev) {
  2089. this.trigger('dragEnd', ev);
  2090. },
  2091. // Delay
  2092. // -----------------------------------------------------------------------------------------------------------------
  2093. startDelay: function(initialEv) {
  2094. var _this = this;
  2095. if (this.delay) {
  2096. this.delayTimeoutId = setTimeout(function() {
  2097. _this.handleDelayEnd(initialEv);
  2098. }, this.delay);
  2099. }
  2100. else {
  2101. this.handleDelayEnd(initialEv);
  2102. }
  2103. },
  2104. handleDelayEnd: function(initialEv) {
  2105. this.isDelayEnded = true;
  2106. if (this.isDistanceSurpassed) {
  2107. this.startDrag(initialEv);
  2108. }
  2109. },
  2110. // Distance
  2111. // -----------------------------------------------------------------------------------------------------------------
  2112. handleDistanceSurpassed: function(ev) {
  2113. this.isDistanceSurpassed = true;
  2114. if (this.isDelayEnded) {
  2115. this.startDrag(ev);
  2116. }
  2117. },
  2118. // Mouse / Touch
  2119. // -----------------------------------------------------------------------------------------------------------------
  2120. handleTouchMove: function(ev) {
  2121. // prevent inertia and touchmove-scrolling while dragging
  2122. if (this.isDragging) {
  2123. ev.preventDefault();
  2124. }
  2125. this.handleMove(ev);
  2126. },
  2127. handleMouseMove: function(ev) {
  2128. this.handleMove(ev);
  2129. },
  2130. // Scrolling (unrelated to auto-scroll)
  2131. // -----------------------------------------------------------------------------------------------------------------
  2132. handleTouchScroll: function(ev) {
  2133. // if the drag is being initiated by touch, but a scroll happens before
  2134. // the drag-initiating delay is over, cancel the drag
  2135. if (!this.isDragging) {
  2136. this.endInteraction(ev, true); // isCancelled=true
  2137. }
  2138. },
  2139. // Utils
  2140. // -----------------------------------------------------------------------------------------------------------------
  2141. // Triggers a callback. Calls a function in the option hash of the same name.
  2142. // Arguments beyond the first `name` are forwarded on.
  2143. trigger: function(name) {
  2144. if (this.options[name]) {
  2145. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  2146. }
  2147. // makes _methods callable by event name. TODO: kill this
  2148. if (this['_' + name]) {
  2149. this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
  2150. }
  2151. }
  2152. });
  2153. ;;
  2154. /*
  2155. this.scrollEl is set in DragListener
  2156. */
  2157. DragListener.mixin({
  2158. isAutoScroll: false,
  2159. scrollBounds: null, // { top, bottom, left, right }
  2160. scrollTopVel: null, // pixels per second
  2161. scrollLeftVel: null, // pixels per second
  2162. scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  2163. // defaults
  2164. scrollSensitivity: 30, // pixels from edge for scrolling to start
  2165. scrollSpeed: 200, // pixels per second, at maximum speed
  2166. scrollIntervalMs: 50, // millisecond wait between scroll increment
  2167. initAutoScroll: function() {
  2168. var scrollEl = this.scrollEl;
  2169. this.isAutoScroll =
  2170. this.options.scroll &&
  2171. scrollEl &&
  2172. !scrollEl.is(window) &&
  2173. !scrollEl.is(document);
  2174. if (this.isAutoScroll) {
  2175. // debounce makes sure rapid calls don't happen
  2176. this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
  2177. }
  2178. },
  2179. destroyAutoScroll: function() {
  2180. this.endAutoScroll(); // kill any animation loop
  2181. // remove the scroll handler if there is a scrollEl
  2182. if (this.isAutoScroll) {
  2183. this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
  2184. }
  2185. },
  2186. // Computes and stores the bounding rectangle of scrollEl
  2187. computeScrollBounds: function() {
  2188. if (this.isAutoScroll) {
  2189. this.scrollBounds = getOuterRect(this.scrollEl);
  2190. // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  2191. }
  2192. },
  2193. // Called when the dragging is in progress and scrolling should be updated
  2194. updateAutoScroll: function(ev) {
  2195. var sensitivity = this.scrollSensitivity;
  2196. var bounds = this.scrollBounds;
  2197. var topCloseness, bottomCloseness;
  2198. var leftCloseness, rightCloseness;
  2199. var topVel = 0;
  2200. var leftVel = 0;
  2201. if (bounds) { // only scroll if scrollEl exists
  2202. // compute closeness to edges. valid range is from 0.0 - 1.0
  2203. topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
  2204. bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
  2205. leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
  2206. rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
  2207. // translate vertical closeness into velocity.
  2208. // mouse must be completely in bounds for velocity to happen.
  2209. if (topCloseness >= 0 && topCloseness <= 1) {
  2210. topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  2211. }
  2212. else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  2213. topVel = bottomCloseness * this.scrollSpeed;
  2214. }
  2215. // translate horizontal closeness into velocity
  2216. if (leftCloseness >= 0 && leftCloseness <= 1) {
  2217. leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  2218. }
  2219. else if (rightCloseness >= 0 && rightCloseness <= 1) {
  2220. leftVel = rightCloseness * this.scrollSpeed;
  2221. }
  2222. }
  2223. this.setScrollVel(topVel, leftVel);
  2224. },
  2225. // Sets the speed-of-scrolling for the scrollEl
  2226. setScrollVel: function(topVel, leftVel) {
  2227. this.scrollTopVel = topVel;
  2228. this.scrollLeftVel = leftVel;
  2229. this.constrainScrollVel(); // massages into realistic values
  2230. // if there is non-zero velocity, and an animation loop hasn't already started, then START
  2231. if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  2232. this.scrollIntervalId = setInterval(
  2233. proxy(this, 'scrollIntervalFunc'), // scope to `this`
  2234. this.scrollIntervalMs
  2235. );
  2236. }
  2237. },
  2238. // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  2239. constrainScrollVel: function() {
  2240. var el = this.scrollEl;
  2241. if (this.scrollTopVel < 0) { // scrolling up?
  2242. if (el.scrollTop() <= 0) { // already scrolled all the way up?
  2243. this.scrollTopVel = 0;
  2244. }
  2245. }
  2246. else if (this.scrollTopVel > 0) { // scrolling down?
  2247. if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  2248. this.scrollTopVel = 0;
  2249. }
  2250. }
  2251. if (this.scrollLeftVel < 0) { // scrolling left?
  2252. if (el.scrollLeft() <= 0) { // already scrolled all the left?
  2253. this.scrollLeftVel = 0;
  2254. }
  2255. }
  2256. else if (this.scrollLeftVel > 0) { // scrolling right?
  2257. if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  2258. this.scrollLeftVel = 0;
  2259. }
  2260. }
  2261. },
  2262. // This function gets called during every iteration of the scrolling animation loop
  2263. scrollIntervalFunc: function() {
  2264. var el = this.scrollEl;
  2265. var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  2266. // change the value of scrollEl's scroll
  2267. if (this.scrollTopVel) {
  2268. el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  2269. }
  2270. if (this.scrollLeftVel) {
  2271. el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  2272. }
  2273. this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  2274. // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  2275. if (!this.scrollTopVel && !this.scrollLeftVel) {
  2276. this.endAutoScroll();
  2277. }
  2278. },
  2279. // Kills any existing scrolling animation loop
  2280. endAutoScroll: function() {
  2281. if (this.scrollIntervalId) {
  2282. clearInterval(this.scrollIntervalId);
  2283. this.scrollIntervalId = null;
  2284. this.handleScrollEnd();
  2285. }
  2286. },
  2287. // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  2288. handleDebouncedScroll: function() {
  2289. // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  2290. if (!this.scrollIntervalId) {
  2291. this.handleScrollEnd();
  2292. }
  2293. },
  2294. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2295. handleScrollEnd: function() {
  2296. }
  2297. });
  2298. ;;
  2299. /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
  2300. ------------------------------------------------------------------------------------------------------------------------
  2301. options:
  2302. - subjectEl
  2303. - subjectCenter
  2304. */
  2305. var HitDragListener = DragListener.extend({
  2306. component: null, // converts coordinates to hits
  2307. // methods: prepareHits, releaseHits, queryHit
  2308. origHit: null, // the hit the mouse was over when listening started
  2309. hit: null, // the hit the mouse is over
  2310. coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
  2311. constructor: function(component, options) {
  2312. DragListener.call(this, options); // call the super-constructor
  2313. this.component = component;
  2314. },
  2315. // Called when drag listening starts (but a real drag has not necessarily began).
  2316. // ev might be undefined if dragging was started manually.
  2317. handleInteractionStart: function(ev) {
  2318. var subjectEl = this.subjectEl;
  2319. var subjectRect;
  2320. var origPoint;
  2321. var point;
  2322. this.computeCoords();
  2323. if (ev) {
  2324. origPoint = { left: getEvX(ev), top: getEvY(ev) };
  2325. point = origPoint;
  2326. // constrain the point to bounds of the element being dragged
  2327. if (subjectEl) {
  2328. subjectRect = getOuterRect(subjectEl); // used for centering as well
  2329. point = constrainPoint(point, subjectRect);
  2330. }
  2331. this.origHit = this.queryHit(point.left, point.top);
  2332. // treat the center of the subject as the collision point?
  2333. if (subjectEl && this.options.subjectCenter) {
  2334. // only consider the area the subject overlaps the hit. best for large subjects.
  2335. // TODO: skip this if hit didn't supply left/right/top/bottom
  2336. if (this.origHit) {
  2337. subjectRect = intersectRects(this.origHit, subjectRect) ||
  2338. subjectRect; // in case there is no intersection
  2339. }
  2340. point = getRectCenter(subjectRect);
  2341. }
  2342. this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
  2343. }
  2344. else {
  2345. this.origHit = null;
  2346. this.coordAdjust = null;
  2347. }
  2348. // call the super-method. do it after origHit has been computed
  2349. DragListener.prototype.handleInteractionStart.apply(this, arguments);
  2350. },
  2351. // Recomputes the drag-critical positions of elements
  2352. computeCoords: function() {
  2353. this.component.prepareHits();
  2354. this.computeScrollBounds(); // why is this here??????
  2355. },
  2356. // Called when the actual drag has started
  2357. handleDragStart: function(ev) {
  2358. var hit;
  2359. DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
  2360. // might be different from this.origHit if the min-distance is large
  2361. hit = this.queryHit(getEvX(ev), getEvY(ev));
  2362. // report the initial hit the mouse is over
  2363. // especially important if no min-distance and drag starts immediately
  2364. if (hit) {
  2365. this.handleHitOver(hit);
  2366. }
  2367. },
  2368. // Called when the drag moves
  2369. handleDrag: function(dx, dy, ev) {
  2370. var hit;
  2371. DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
  2372. hit = this.queryHit(getEvX(ev), getEvY(ev));
  2373. if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
  2374. if (this.hit) {
  2375. this.handleHitOut();
  2376. }
  2377. if (hit) {
  2378. this.handleHitOver(hit);
  2379. }
  2380. }
  2381. },
  2382. // Called when dragging has been stopped
  2383. handleDragEnd: function() {
  2384. this.handleHitDone();
  2385. DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
  2386. },
  2387. // Called when a the mouse has just moved over a new hit
  2388. handleHitOver: function(hit) {
  2389. var isOrig = isHitsEqual(hit, this.origHit);
  2390. this.hit = hit;
  2391. this.trigger('hitOver', this.hit, isOrig, this.origHit);
  2392. },
  2393. // Called when the mouse has just moved out of a hit
  2394. handleHitOut: function() {
  2395. if (this.hit) {
  2396. this.trigger('hitOut', this.hit);
  2397. this.handleHitDone();
  2398. this.hit = null;
  2399. }
  2400. },
  2401. // Called after a hitOut. Also called before a dragStop
  2402. handleHitDone: function() {
  2403. if (this.hit) {
  2404. this.trigger('hitDone', this.hit);
  2405. }
  2406. },
  2407. // Called when the interaction ends, whether there was a real drag or not
  2408. handleInteractionEnd: function() {
  2409. DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
  2410. this.origHit = null;
  2411. this.hit = null;
  2412. this.component.releaseHits();
  2413. },
  2414. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2415. handleScrollEnd: function() {
  2416. DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
  2417. this.computeCoords(); // hits' absolute positions will be in new places. recompute
  2418. },
  2419. // Gets the hit underneath the coordinates for the given mouse event
  2420. queryHit: function(left, top) {
  2421. if (this.coordAdjust) {
  2422. left += this.coordAdjust.left;
  2423. top += this.coordAdjust.top;
  2424. }
  2425. return this.component.queryHit(left, top);
  2426. }
  2427. });
  2428. // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
  2429. // Two null values will be considered equal, as two "out of the component" states are the same.
  2430. function isHitsEqual(hit0, hit1) {
  2431. if (!hit0 && !hit1) {
  2432. return true;
  2433. }
  2434. if (hit0 && hit1) {
  2435. return hit0.component === hit1.component &&
  2436. isHitPropsWithin(hit0, hit1) &&
  2437. isHitPropsWithin(hit1, hit0); // ensures all props are identical
  2438. }
  2439. return false;
  2440. }
  2441. // Returns true if all of subHit's non-standard properties are within superHit
  2442. function isHitPropsWithin(subHit, superHit) {
  2443. for (var propName in subHit) {
  2444. if (!/^(component|left|right|top|bottom)$/.test(propName)) {
  2445. if (subHit[propName] !== superHit[propName]) {
  2446. return false;
  2447. }
  2448. }
  2449. }
  2450. return true;
  2451. }
  2452. ;;
  2453. /* Creates a clone of an element and lets it track the mouse as it moves
  2454. ----------------------------------------------------------------------------------------------------------------------*/
  2455. var MouseFollower = Class.extend(ListenerMixin, {
  2456. options: null,
  2457. sourceEl: null, // the element that will be cloned and made to look like it is dragging
  2458. el: null, // the clone of `sourceEl` that will track the mouse
  2459. parentEl: null, // the element that `el` (the clone) will be attached to
  2460. // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  2461. top0: null,
  2462. left0: null,
  2463. // the absolute coordinates of the initiating touch/mouse action
  2464. y0: null,
  2465. x0: null,
  2466. // the number of pixels the mouse has moved from its initial position
  2467. topDelta: null,
  2468. leftDelta: null,
  2469. isFollowing: false,
  2470. isHidden: false,
  2471. isAnimating: false, // doing the revert animation?
  2472. constructor: function(sourceEl, options) {
  2473. this.options = options = options || {};
  2474. this.sourceEl = sourceEl;
  2475. this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  2476. },
  2477. // Causes the element to start following the mouse
  2478. start: function(ev) {
  2479. if (!this.isFollowing) {
  2480. this.isFollowing = true;
  2481. this.y0 = getEvY(ev);
  2482. this.x0 = getEvX(ev);
  2483. this.topDelta = 0;
  2484. this.leftDelta = 0;
  2485. if (!this.isHidden) {
  2486. this.updatePosition();
  2487. }
  2488. if (getEvIsTouch(ev)) {
  2489. this.listenTo($(document), 'touchmove', this.handleMove);
  2490. }
  2491. else {
  2492. this.listenTo($(document), 'mousemove', this.handleMove);
  2493. }
  2494. }
  2495. },
  2496. // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  2497. // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  2498. stop: function(shouldRevert, callback) {
  2499. var _this = this;
  2500. var revertDuration = this.options.revertDuration;
  2501. function complete() { // might be called by .animate(), which might change `this` context
  2502. _this.isAnimating = false;
  2503. _this.removeElement();
  2504. _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
  2505. if (callback) {
  2506. callback();
  2507. }
  2508. }
  2509. if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  2510. this.isFollowing = false;
  2511. this.stopListeningTo($(document));
  2512. if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  2513. this.isAnimating = true;
  2514. this.el.animate({
  2515. top: this.top0,
  2516. left: this.left0
  2517. }, {
  2518. duration: revertDuration,
  2519. complete: complete
  2520. });
  2521. }
  2522. else {
  2523. complete();
  2524. }
  2525. }
  2526. },
  2527. // Gets the tracking element. Create it if necessary
  2528. getEl: function() {
  2529. var el = this.el;
  2530. if (!el) {
  2531. el = this.el = this.sourceEl.clone()
  2532. .addClass(this.options.additionalClass || '')
  2533. .css({
  2534. position: 'absolute',
  2535. visibility: '', // in case original element was hidden (commonly through hideEvents())
  2536. display: this.isHidden ? 'none' : '', // for when initially hidden
  2537. margin: 0,
  2538. right: 'auto', // erase and set width instead
  2539. bottom: 'auto', // erase and set height instead
  2540. width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  2541. height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  2542. opacity: this.options.opacity || '',
  2543. zIndex: this.options.zIndex
  2544. });
  2545. // we don't want long taps or any mouse interaction causing selection/menus.
  2546. // would use preventSelection(), but that prevents selectstart, causing problems.
  2547. el.addClass('fc-unselectable');
  2548. el.appendTo(this.parentEl);
  2549. }
  2550. return el;
  2551. },
  2552. // Removes the tracking element if it has already been created
  2553. removeElement: function() {
  2554. if (this.el) {
  2555. this.el.remove();
  2556. this.el = null;
  2557. }
  2558. },
  2559. // Update the CSS position of the tracking element
  2560. updatePosition: function() {
  2561. var sourceOffset;
  2562. var origin;
  2563. this.getEl(); // ensure this.el
  2564. // make sure origin info was computed
  2565. if (this.top0 === null) {
  2566. sourceOffset = this.sourceEl.offset();
  2567. origin = this.el.offsetParent().offset();
  2568. this.top0 = sourceOffset.top - origin.top;
  2569. this.left0 = sourceOffset.left - origin.left;
  2570. }
  2571. this.el.css({
  2572. top: this.top0 + this.topDelta,
  2573. left: this.left0 + this.leftDelta
  2574. });
  2575. },
  2576. // Gets called when the user moves the mouse
  2577. handleMove: function(ev) {
  2578. this.topDelta = getEvY(ev) - this.y0;
  2579. this.leftDelta = getEvX(ev) - this.x0;
  2580. if (!this.isHidden) {
  2581. this.updatePosition();
  2582. }
  2583. },
  2584. // Temporarily makes the tracking element invisible. Can be called before following starts
  2585. hide: function() {
  2586. if (!this.isHidden) {
  2587. this.isHidden = true;
  2588. if (this.el) {
  2589. this.el.hide();
  2590. }
  2591. }
  2592. },
  2593. // Show the tracking element after it has been temporarily hidden
  2594. show: function() {
  2595. if (this.isHidden) {
  2596. this.isHidden = false;
  2597. this.updatePosition();
  2598. this.getEl().show();
  2599. }
  2600. }
  2601. });
  2602. ;;
  2603. /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
  2604. ----------------------------------------------------------------------------------------------------------------------*/
  2605. var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
  2606. // self-config, overridable by subclasses
  2607. hasDayInteractions: true, // can user click/select ranges of time?
  2608. view: null, // a View object
  2609. isRTL: null, // shortcut to the view's isRTL option
  2610. start: null,
  2611. end: null,
  2612. el: null, // the containing element
  2613. elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  2614. // derived from options
  2615. eventTimeFormat: null,
  2616. displayEventTime: null,
  2617. displayEventEnd: null,
  2618. minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
  2619. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  2620. // of the date areas. if not defined, assumes to be day and time granularity.
  2621. // TODO: port isTimeScale into same system?
  2622. largeUnit: null,
  2623. dayDragListener: null,
  2624. segDragListener: null,
  2625. segResizeListener: null,
  2626. externalDragListener: null,
  2627. constructor: function(view) {
  2628. this.view = view;
  2629. this.isRTL = view.opt('isRTL');
  2630. this.elsByFill = {};
  2631. this.dayDragListener = this.buildDayDragListener();
  2632. this.initMouseIgnoring();
  2633. },
  2634. /* Options
  2635. ------------------------------------------------------------------------------------------------------------------*/
  2636. // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  2637. computeEventTimeFormat: function() {
  2638. return this.view.opt('smallTimeFormat');
  2639. },
  2640. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
  2641. // Only applies to non-all-day events.
  2642. computeDisplayEventTime: function() {
  2643. return true;
  2644. },
  2645. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  2646. computeDisplayEventEnd: function() {
  2647. return true;
  2648. },
  2649. /* Dates
  2650. ------------------------------------------------------------------------------------------------------------------*/
  2651. // Tells the grid about what period of time to display.
  2652. // Any date-related internal data should be generated.
  2653. setRange: function(range) {
  2654. this.start = range.start.clone();
  2655. this.end = range.end.clone();
  2656. this.rangeUpdated();
  2657. this.processRangeOptions();
  2658. },
  2659. // Called when internal variables that rely on the range should be updated
  2660. rangeUpdated: function() {
  2661. },
  2662. // Updates values that rely on options and also relate to range
  2663. processRangeOptions: function() {
  2664. var view = this.view;
  2665. var displayEventTime;
  2666. var displayEventEnd;
  2667. this.eventTimeFormat =
  2668. view.opt('eventTimeFormat') ||
  2669. view.opt('timeFormat') || // deprecated
  2670. this.computeEventTimeFormat();
  2671. displayEventTime = view.opt('displayEventTime');
  2672. if (displayEventTime == null) {
  2673. displayEventTime = this.computeDisplayEventTime(); // might be based off of range
  2674. }
  2675. displayEventEnd = view.opt('displayEventEnd');
  2676. if (displayEventEnd == null) {
  2677. displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
  2678. }
  2679. this.displayEventTime = displayEventTime;
  2680. this.displayEventEnd = displayEventEnd;
  2681. },
  2682. // Converts a span (has unzoned start/end and any other grid-specific location information)
  2683. // into an array of segments (pieces of events whose format is decided by the grid).
  2684. spanToSegs: function(span) {
  2685. // subclasses must implement
  2686. },
  2687. // Diffs the two dates, returning a duration, based on granularity of the grid
  2688. // TODO: port isTimeScale into this system?
  2689. diffDates: function(a, b) {
  2690. if (this.largeUnit) {
  2691. return diffByUnit(a, b, this.largeUnit);
  2692. }
  2693. else {
  2694. return diffDayTime(a, b);
  2695. }
  2696. },
  2697. /* Hit Area
  2698. ------------------------------------------------------------------------------------------------------------------*/
  2699. // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
  2700. prepareHits: function() {
  2701. },
  2702. // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
  2703. releaseHits: function() {
  2704. },
  2705. // Given coordinates from the topleft of the document, return data about the date-related area underneath.
  2706. // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
  2707. // Must have a `grid` property, a reference to this current grid. TODO: avoid this
  2708. // The returned object will be processed by getHitSpan and getHitEl.
  2709. queryHit: function(leftOffset, topOffset) {
  2710. },
  2711. // Given position-level information about a date-related area within the grid,
  2712. // should return an object with at least a start/end date. Can provide other information as well.
  2713. getHitSpan: function(hit) {
  2714. },
  2715. // Given position-level information about a date-related area within the grid,
  2716. // should return a jQuery element that best represents it. passed to dayClick callback.
  2717. getHitEl: function(hit) {
  2718. },
  2719. /* Rendering
  2720. ------------------------------------------------------------------------------------------------------------------*/
  2721. // Sets the container element that the grid should render inside of.
  2722. // Does other DOM-related initializations.
  2723. setElement: function(el) {
  2724. this.el = el;
  2725. if (this.hasDayInteractions) {
  2726. preventSelection(el);
  2727. this.bindDayHandler('touchstart', this.dayTouchStart);
  2728. this.bindDayHandler('mousedown', this.dayMousedown);
  2729. }
  2730. // attach event-element-related handlers. in Grid.events
  2731. // same garbage collection note as above.
  2732. this.bindSegHandlers();
  2733. this.bindGlobalHandlers();
  2734. },
  2735. bindDayHandler: function(name, handler) {
  2736. var _this = this;
  2737. // attach a handler to the grid's root element.
  2738. // jQuery will take care of unregistering them when removeElement gets called.
  2739. this.el.on(name, function(ev) {
  2740. if (
  2741. !$(ev.target).is(
  2742. _this.segSelector + ',' + // directly on an event element
  2743. _this.segSelector + ' *,' + // within an event element
  2744. '.fc-more,' + // a "more.." link
  2745. 'a[data-goto]' // a clickable nav link
  2746. )
  2747. ) {
  2748. return handler.call(_this, ev);
  2749. }
  2750. });
  2751. },
  2752. // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
  2753. // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
  2754. removeElement: function() {
  2755. this.unbindGlobalHandlers();
  2756. this.clearDragListeners();
  2757. this.el.remove();
  2758. // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
  2759. },
  2760. // Renders the basic structure of grid view before any content is rendered
  2761. renderSkeleton: function() {
  2762. // subclasses should implement
  2763. },
  2764. // Renders the grid's date-related content (like areas that represent days/times).
  2765. // Assumes setRange has already been called and the skeleton has already been rendered.
  2766. renderDates: function() {
  2767. // subclasses should implement
  2768. },
  2769. // Unrenders the grid's date-related content
  2770. unrenderDates: function() {
  2771. // subclasses should implement
  2772. },
  2773. /* Handlers
  2774. ------------------------------------------------------------------------------------------------------------------*/
  2775. // Binds DOM handlers to elements that reside outside the grid, such as the document
  2776. bindGlobalHandlers: function() {
  2777. this.listenTo($(document), {
  2778. dragstart: this.externalDragStart, // jqui
  2779. sortstart: this.externalDragStart // jqui
  2780. });
  2781. },
  2782. // Unbinds DOM handlers from elements that reside outside the grid
  2783. unbindGlobalHandlers: function() {
  2784. this.stopListeningTo($(document));
  2785. },
  2786. // Process a mousedown on an element that represents a day. For day clicking and selecting.
  2787. dayMousedown: function(ev) {
  2788. if (!this.isIgnoringMouse) {
  2789. this.dayDragListener.startInteraction(ev, {
  2790. //distance: 5, // needs more work if we want dayClick to fire correctly
  2791. });
  2792. }
  2793. },
  2794. dayTouchStart: function(ev) {
  2795. var view = this.view;
  2796. var selectLongPressDelay = view.opt('selectLongPressDelay');
  2797. // HACK to prevent a user's clickaway for unselecting a range or an event
  2798. // from causing a dayClick.
  2799. if (view.isSelected || view.selectedEvent) {
  2800. this.tempIgnoreMouse();
  2801. }
  2802. if (selectLongPressDelay == null) {
  2803. selectLongPressDelay = view.opt('longPressDelay'); // fallback
  2804. }
  2805. this.dayDragListener.startInteraction(ev, {
  2806. delay: selectLongPressDelay
  2807. });
  2808. },
  2809. // Creates a listener that tracks the user's drag across day elements.
  2810. // For day clicking and selecting.
  2811. buildDayDragListener: function() {
  2812. var _this = this;
  2813. var view = this.view;
  2814. var isSelectable = view.opt('selectable');
  2815. var dayClickHit; // null if invalid dayClick
  2816. var selectionSpan; // null if invalid selection
  2817. // this listener tracks a mousedown on a day element, and a subsequent drag.
  2818. // if the drag ends on the same day, it is a 'dayClick'.
  2819. // if 'selectable' is enabled, this listener also detects selections.
  2820. var dragListener = new HitDragListener(this, {
  2821. scroll: view.opt('dragScroll'),
  2822. interactionStart: function() {
  2823. dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
  2824. selectionSpan = null;
  2825. },
  2826. dragStart: function() {
  2827. view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  2828. },
  2829. hitOver: function(hit, isOrig, origHit) {
  2830. if (origHit) { // click needs to have started on a hit
  2831. // if user dragged to another cell at any point, it can no longer be a dayClick
  2832. if (!isOrig) {
  2833. dayClickHit = null;
  2834. }
  2835. if (isSelectable) {
  2836. selectionSpan = _this.computeSelection(
  2837. _this.getHitSpan(origHit),
  2838. _this.getHitSpan(hit)
  2839. );
  2840. if (selectionSpan) {
  2841. _this.renderSelection(selectionSpan);
  2842. }
  2843. else if (selectionSpan === false) {
  2844. disableCursor();
  2845. }
  2846. }
  2847. }
  2848. },
  2849. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  2850. dayClickHit = null;
  2851. selectionSpan = null;
  2852. _this.unrenderSelection();
  2853. },
  2854. hitDone: function() { // called after a hitOut OR before a dragEnd
  2855. enableCursor();
  2856. },
  2857. interactionEnd: function(ev, isCancelled) {
  2858. if (!isCancelled) {
  2859. if (
  2860. dayClickHit &&
  2861. !_this.isIgnoringMouse // see hack in dayTouchStart
  2862. ) {
  2863. view.triggerDayClick(
  2864. _this.getHitSpan(dayClickHit),
  2865. _this.getHitEl(dayClickHit),
  2866. ev
  2867. );
  2868. }
  2869. if (selectionSpan) {
  2870. // the selection will already have been rendered. just report it
  2871. view.reportSelection(selectionSpan, ev);
  2872. }
  2873. }
  2874. }
  2875. });
  2876. return dragListener;
  2877. },
  2878. // Kills all in-progress dragging.
  2879. // Useful for when public API methods that result in re-rendering are invoked during a drag.
  2880. // Also useful for when touch devices misbehave and don't fire their touchend.
  2881. clearDragListeners: function() {
  2882. this.dayDragListener.endInteraction();
  2883. if (this.segDragListener) {
  2884. this.segDragListener.endInteraction(); // will clear this.segDragListener
  2885. }
  2886. if (this.segResizeListener) {
  2887. this.segResizeListener.endInteraction(); // will clear this.segResizeListener
  2888. }
  2889. if (this.externalDragListener) {
  2890. this.externalDragListener.endInteraction(); // will clear this.externalDragListener
  2891. }
  2892. },
  2893. /* Event Helper
  2894. ------------------------------------------------------------------------------------------------------------------*/
  2895. // TODO: should probably move this to Grid.events, like we did event dragging / resizing
  2896. // Renders a mock event at the given event location, which contains zoned start/end properties.
  2897. // Returns all mock event elements.
  2898. renderEventLocationHelper: function(eventLocation, sourceSeg) {
  2899. var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
  2900. return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  2901. },
  2902. // Builds a fake event given zoned event date properties and a segment is should be inspired from.
  2903. // The range's end can be null, in which case the mock event that is rendered will have a null end time.
  2904. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  2905. fabricateHelperEvent: function(eventLocation, sourceSeg) {
  2906. var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  2907. fakeEvent.start = eventLocation.start.clone();
  2908. fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
  2909. fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
  2910. this.view.calendar.normalizeEventDates(fakeEvent);
  2911. // this extra className will be useful for differentiating real events from mock events in CSS
  2912. fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  2913. // if something external is being dragged in, don't render a resizer
  2914. if (!sourceSeg) {
  2915. fakeEvent.editable = false;
  2916. }
  2917. return fakeEvent;
  2918. },
  2919. // Renders a mock event. Given zoned event date properties.
  2920. // Must return all mock event elements.
  2921. renderHelper: function(eventLocation, sourceSeg) {
  2922. // subclasses must implement
  2923. },
  2924. // Unrenders a mock event
  2925. unrenderHelper: function() {
  2926. // subclasses must implement
  2927. },
  2928. /* Selection
  2929. ------------------------------------------------------------------------------------------------------------------*/
  2930. // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  2931. // Given a span (unzoned start/end and other misc data)
  2932. renderSelection: function(span) {
  2933. this.renderHighlight(span);
  2934. },
  2935. // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  2936. unrenderSelection: function() {
  2937. this.unrenderHighlight();
  2938. },
  2939. // Given the first and last date-spans of a selection, returns another date-span object.
  2940. // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
  2941. // Will return false if the selection is invalid and this should be indicated to the user.
  2942. // Will return null/undefined if a selection invalid but no error should be reported.
  2943. computeSelection: function(span0, span1) {
  2944. var span = this.computeSelectionSpan(span0, span1);
  2945. if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
  2946. return false;
  2947. }
  2948. return span;
  2949. },
  2950. // Given two spans, must return the combination of the two.
  2951. // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
  2952. computeSelectionSpan: function(span0, span1) {
  2953. var dates = [ span0.start, span0.end, span1.start, span1.end ];
  2954. dates.sort(compareNumbers); // sorts chronologically. works with Moments
  2955. return { start: dates[0].clone(), end: dates[3].clone() };
  2956. },
  2957. /* Highlight
  2958. ------------------------------------------------------------------------------------------------------------------*/
  2959. // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
  2960. renderHighlight: function(span) {
  2961. this.renderFill('highlight', this.spanToSegs(span));
  2962. },
  2963. // Unrenders the emphasis on a date range
  2964. unrenderHighlight: function() {
  2965. this.unrenderFill('highlight');
  2966. },
  2967. // Generates an array of classNames for rendering the highlight. Used by the fill system.
  2968. highlightSegClasses: function() {
  2969. return [ 'fc-highlight' ];
  2970. },
  2971. /* Business Hours
  2972. ------------------------------------------------------------------------------------------------------------------*/
  2973. renderBusinessHours: function() {
  2974. },
  2975. unrenderBusinessHours: function() {
  2976. },
  2977. /* Now Indicator
  2978. ------------------------------------------------------------------------------------------------------------------*/
  2979. getNowIndicatorUnit: function() {
  2980. },
  2981. renderNowIndicator: function(date) {
  2982. },
  2983. unrenderNowIndicator: function() {
  2984. },
  2985. /* Fill System (highlight, background events, business hours)
  2986. --------------------------------------------------------------------------------------------------------------------
  2987. TODO: remove this system. like we did in TimeGrid
  2988. */
  2989. // Renders a set of rectangles over the given segments of time.
  2990. // MUST RETURN a subset of segs, the segs that were actually rendered.
  2991. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  2992. renderFill: function(type, segs) {
  2993. // subclasses must implement
  2994. },
  2995. // Unrenders a specific type of fill that is currently rendered on the grid
  2996. unrenderFill: function(type) {
  2997. var el = this.elsByFill[type];
  2998. if (el) {
  2999. el.remove();
  3000. delete this.elsByFill[type];
  3001. }
  3002. },
  3003. // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  3004. // Only returns segments that successfully rendered.
  3005. // To be harnessed by renderFill (implemented by subclasses).
  3006. // Analagous to renderFgSegEls.
  3007. renderFillSegEls: function(type, segs) {
  3008. var _this = this;
  3009. var segElMethod = this[type + 'SegEl'];
  3010. var html = '';
  3011. var renderedSegs = [];
  3012. var i;
  3013. if (segs.length) {
  3014. // build a large concatenation of segment HTML
  3015. for (i = 0; i < segs.length; i++) {
  3016. html += this.fillSegHtml(type, segs[i]);
  3017. }
  3018. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3019. // Then, compute the 'el' for each segment.
  3020. $(html).each(function(i, node) {
  3021. var seg = segs[i];
  3022. var el = $(node);
  3023. // allow custom filter methods per-type
  3024. if (segElMethod) {
  3025. el = segElMethod.call(_this, seg, el);
  3026. }
  3027. if (el) { // custom filters did not cancel the render
  3028. el = $(el); // allow custom filter to return raw DOM node
  3029. // correct element type? (would be bad if a non-TD were inserted into a table for example)
  3030. if (el.is(_this.fillSegTag)) {
  3031. seg.el = el;
  3032. renderedSegs.push(seg);
  3033. }
  3034. }
  3035. });
  3036. }
  3037. return renderedSegs;
  3038. },
  3039. fillSegTag: 'div', // subclasses can override
  3040. // Builds the HTML needed for one fill segment. Generic enough to work with different types.
  3041. fillSegHtml: function(type, seg) {
  3042. // custom hooks per-type
  3043. var classesMethod = this[type + 'SegClasses'];
  3044. var cssMethod = this[type + 'SegCss'];
  3045. var classes = classesMethod ? classesMethod.call(this, seg) : [];
  3046. var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
  3047. return '<' + this.fillSegTag +
  3048. (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  3049. (css ? ' style="' + css + '"' : '') +
  3050. ' />';
  3051. },
  3052. /* Generic rendering utilities for subclasses
  3053. ------------------------------------------------------------------------------------------------------------------*/
  3054. // Computes HTML classNames for a single-day element
  3055. getDayClasses: function(date, noThemeHighlight) {
  3056. var view = this.view;
  3057. var today = view.calendar.getNow();
  3058. var classes = [ 'fc-' + dayIDs[date.day()] ];
  3059. if (
  3060. view.intervalDuration.as('months') == 1 &&
  3061. date.month() != view.intervalStart.month()
  3062. ) {
  3063. classes.push('fc-other-month');
  3064. }
  3065. if (date.isSame(today, 'day')) {
  3066. classes.push('fc-today');
  3067. if (noThemeHighlight !== true) {
  3068. classes.push(view.highlightStateClass);
  3069. }
  3070. }
  3071. else if (date < today) {
  3072. classes.push('fc-past');
  3073. }
  3074. else {
  3075. classes.push('fc-future');
  3076. }
  3077. return classes;
  3078. }
  3079. });
  3080. ;;
  3081. /* Event-rendering and event-interaction methods for the abstract Grid class
  3082. ----------------------------------------------------------------------------------------------------------------------*/
  3083. Grid.mixin({
  3084. // self-config, overridable by subclasses
  3085. segSelector: '.fc-event-container > *', // what constitutes an event element?
  3086. mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  3087. isDraggingSeg: false, // is a segment being dragged? boolean
  3088. isResizingSeg: false, // is a segment being resized? boolean
  3089. isDraggingExternal: false, // jqui-dragging an external element? boolean
  3090. segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
  3091. // Renders the given events onto the grid
  3092. renderEvents: function(events) {
  3093. var bgEvents = [];
  3094. var fgEvents = [];
  3095. var i;
  3096. for (i = 0; i < events.length; i++) {
  3097. (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]);
  3098. }
  3099. this.segs = [].concat( // record all segs
  3100. this.renderBgEvents(bgEvents),
  3101. this.renderFgEvents(fgEvents)
  3102. );
  3103. },
  3104. renderBgEvents: function(events) {
  3105. var segs = this.eventsToSegs(events);
  3106. // renderBgSegs might return a subset of segs, segs that were actually rendered
  3107. return this.renderBgSegs(segs) || segs;
  3108. },
  3109. renderFgEvents: function(events) {
  3110. var segs = this.eventsToSegs(events);
  3111. // renderFgSegs might return a subset of segs, segs that were actually rendered
  3112. return this.renderFgSegs(segs) || segs;
  3113. },
  3114. // Unrenders all events currently rendered on the grid
  3115. unrenderEvents: function() {
  3116. this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  3117. this.clearDragListeners();
  3118. this.unrenderFgSegs();
  3119. this.unrenderBgSegs();
  3120. this.segs = null;
  3121. },
  3122. // Retrieves all rendered segment objects currently rendered on the grid
  3123. getEventSegs: function() {
  3124. return this.segs || [];
  3125. },
  3126. /* Foreground Segment Rendering
  3127. ------------------------------------------------------------------------------------------------------------------*/
  3128. // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  3129. renderFgSegs: function(segs) {
  3130. // subclasses must implement
  3131. },
  3132. // Unrenders all currently rendered foreground segments
  3133. unrenderFgSegs: function() {
  3134. // subclasses must implement
  3135. },
  3136. // Renders and assigns an `el` property for each foreground event segment.
  3137. // Only returns segments that successfully rendered.
  3138. // A utility that subclasses may use.
  3139. renderFgSegEls: function(segs, disableResizing) {
  3140. var view = this.view;
  3141. var html = '';
  3142. var renderedSegs = [];
  3143. var i;
  3144. if (segs.length) { // don't build an empty html string
  3145. // build a large concatenation of event segment HTML
  3146. for (i = 0; i < segs.length; i++) {
  3147. html += this.fgSegHtml(segs[i], disableResizing);
  3148. }
  3149. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3150. // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  3151. $(html).each(function(i, node) {
  3152. var seg = segs[i];
  3153. var el = view.resolveEventEl(seg.event, $(node));
  3154. if (el) {
  3155. el.data('fc-seg', seg); // used by handlers
  3156. seg.el = el;
  3157. renderedSegs.push(seg);
  3158. }
  3159. });
  3160. }
  3161. return renderedSegs;
  3162. },
  3163. // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  3164. fgSegHtml: function(seg, disableResizing) {
  3165. // subclasses should implement
  3166. },
  3167. /* Background Segment Rendering
  3168. ------------------------------------------------------------------------------------------------------------------*/
  3169. // Renders the given background event segments onto the grid.
  3170. // Returns a subset of the segs that were actually rendered.
  3171. renderBgSegs: function(segs) {
  3172. return this.renderFill('bgEvent', segs);
  3173. },
  3174. // Unrenders all the currently rendered background event segments
  3175. unrenderBgSegs: function() {
  3176. this.unrenderFill('bgEvent');
  3177. },
  3178. // Renders a background event element, given the default rendering. Called by the fill system.
  3179. bgEventSegEl: function(seg, el) {
  3180. return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  3181. },
  3182. // Generates an array of classNames to be used for the default rendering of a background event.
  3183. // Called by fillSegHtml.
  3184. bgEventSegClasses: function(seg) {
  3185. var event = seg.event;
  3186. var source = event.source || {};
  3187. return [ 'fc-bgevent' ].concat(
  3188. event.className,
  3189. source.className || []
  3190. );
  3191. },
  3192. // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  3193. // Called by fillSegHtml.
  3194. bgEventSegCss: function(seg) {
  3195. return {
  3196. 'background-color': this.getSegSkinCss(seg)['background-color']
  3197. };
  3198. },
  3199. // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  3200. // Called by fillSegHtml.
  3201. businessHoursSegClasses: function(seg) {
  3202. return [ 'fc-nonbusiness', 'fc-bgevent' ];
  3203. },
  3204. /* Business Hours
  3205. ------------------------------------------------------------------------------------------------------------------*/
  3206. // Compute business hour segs for the grid's current date range.
  3207. // Caller must ask if whole-day business hours are needed.
  3208. // If no `businessHours` configuration value is specified, assumes the calendar default.
  3209. buildBusinessHourSegs: function(wholeDay, businessHours) {
  3210. return this.eventsToSegs(
  3211. this.buildBusinessHourEvents(wholeDay, businessHours)
  3212. );
  3213. },
  3214. // Compute business hour *events* for the grid's current date range.
  3215. // Caller must ask if whole-day business hours are needed.
  3216. // If no `businessHours` configuration value is specified, assumes the calendar default.
  3217. buildBusinessHourEvents: function(wholeDay, businessHours) {
  3218. var calendar = this.view.calendar;
  3219. var events;
  3220. if (businessHours == null) {
  3221. // fallback
  3222. // access from calendawr. don't access from view. doesn't update with dynamic options.
  3223. businessHours = calendar.options.businessHours;
  3224. }
  3225. events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
  3226. // HACK. Eventually refactor business hours "events" system.
  3227. // If no events are given, but businessHours is activated, this means the entire visible range should be
  3228. // marked as *not* business-hours, via inverse-background rendering.
  3229. if (!events.length && businessHours) {
  3230. events = [
  3231. $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
  3232. start: this.view.end, // guaranteed out-of-range
  3233. end: this.view.end, // "
  3234. dow: null
  3235. })
  3236. ];
  3237. }
  3238. return events;
  3239. },
  3240. /* Handlers
  3241. ------------------------------------------------------------------------------------------------------------------*/
  3242. // Attaches event-element-related handlers for *all* rendered event segments of the view.
  3243. bindSegHandlers: function() {
  3244. this.bindSegHandlersToEl(this.el);
  3245. },
  3246. // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
  3247. bindSegHandlersToEl: function(el) {
  3248. this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
  3249. this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
  3250. this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
  3251. this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
  3252. this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
  3253. this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
  3254. },
  3255. // Executes a handler for any a user-interaction on a segment.
  3256. // Handler gets called with (seg, ev), and with the `this` context of the Grid
  3257. bindSegHandlerToEl: function(el, name, handler) {
  3258. var _this = this;
  3259. el.on(name, this.segSelector, function(ev) {
  3260. var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  3261. // only call the handlers if there is not a drag/resize in progress
  3262. if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  3263. return handler.call(_this, seg, ev); // context will be the Grid
  3264. }
  3265. });
  3266. },
  3267. handleSegClick: function(seg, ev) {
  3268. var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
  3269. if (res === false) {
  3270. ev.preventDefault();
  3271. }
  3272. },
  3273. // Updates internal state and triggers handlers for when an event element is moused over
  3274. handleSegMouseover: function(seg, ev) {
  3275. if (
  3276. !this.isIgnoringMouse &&
  3277. !this.mousedOverSeg
  3278. ) {
  3279. this.mousedOverSeg = seg;
  3280. if (this.view.isEventResizable(seg.event)) {
  3281. seg.el.addClass('fc-allow-mouse-resize');
  3282. }
  3283. this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
  3284. }
  3285. },
  3286. // Updates internal state and triggers handlers for when an event element is moused out.
  3287. // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  3288. handleSegMouseout: function(seg, ev) {
  3289. ev = ev || {}; // if given no args, make a mock mouse event
  3290. if (this.mousedOverSeg) {
  3291. seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  3292. this.mousedOverSeg = null;
  3293. if (this.view.isEventResizable(seg.event)) {
  3294. seg.el.removeClass('fc-allow-mouse-resize');
  3295. }
  3296. this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
  3297. }
  3298. },
  3299. handleSegMousedown: function(seg, ev) {
  3300. var isResizing = this.startSegResize(seg, ev, { distance: 5 });
  3301. if (!isResizing && this.view.isEventDraggable(seg.event)) {
  3302. this.buildSegDragListener(seg)
  3303. .startInteraction(ev, {
  3304. distance: 5
  3305. });
  3306. }
  3307. },
  3308. handleSegTouchStart: function(seg, ev) {
  3309. var view = this.view;
  3310. var event = seg.event;
  3311. var isSelected = view.isEventSelected(event);
  3312. var isDraggable = view.isEventDraggable(event);
  3313. var isResizable = view.isEventResizable(event);
  3314. var isResizing = false;
  3315. var dragListener;
  3316. var eventLongPressDelay;
  3317. if (isSelected && isResizable) {
  3318. // only allow resizing of the event is selected
  3319. isResizing = this.startSegResize(seg, ev);
  3320. }
  3321. if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
  3322. eventLongPressDelay = view.opt('eventLongPressDelay');
  3323. if (eventLongPressDelay == null) {
  3324. eventLongPressDelay = view.opt('longPressDelay'); // fallback
  3325. }
  3326. dragListener = isDraggable ?
  3327. this.buildSegDragListener(seg) :
  3328. this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
  3329. dragListener.startInteraction(ev, { // won't start if already started
  3330. delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
  3331. });
  3332. }
  3333. // a long tap simulates a mouseover. ignore this bogus mouseover.
  3334. this.tempIgnoreMouse();
  3335. },
  3336. handleSegTouchEnd: function(seg, ev) {
  3337. // touchstart+touchend = click, which simulates a mouseover.
  3338. // ignore this bogus mouseover.
  3339. this.tempIgnoreMouse();
  3340. },
  3341. // returns boolean whether resizing actually started or not.
  3342. // assumes the seg allows resizing.
  3343. // `dragOptions` are optional.
  3344. startSegResize: function(seg, ev, dragOptions) {
  3345. if ($(ev.target).is('.fc-resizer')) {
  3346. this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
  3347. .startInteraction(ev, dragOptions);
  3348. return true;
  3349. }
  3350. return false;
  3351. },
  3352. /* Event Dragging
  3353. ------------------------------------------------------------------------------------------------------------------*/
  3354. // Builds a listener that will track user-dragging on an event segment.
  3355. // Generic enough to work with any type of Grid.
  3356. // Has side effect of setting/unsetting `segDragListener`
  3357. buildSegDragListener: function(seg) {
  3358. var _this = this;
  3359. var view = this.view;
  3360. var calendar = view.calendar;
  3361. var el = seg.el;
  3362. var event = seg.event;
  3363. var isDragging;
  3364. var mouseFollower; // A clone of the original element that will move with the mouse
  3365. var dropLocation; // zoned event date properties
  3366. if (this.segDragListener) {
  3367. return this.segDragListener;
  3368. }
  3369. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  3370. // of the view.
  3371. var dragListener = this.segDragListener = new HitDragListener(view, {
  3372. scroll: view.opt('dragScroll'),
  3373. subjectEl: el,
  3374. subjectCenter: true,
  3375. interactionStart: function(ev) {
  3376. seg.component = _this; // for renderDrag
  3377. isDragging = false;
  3378. mouseFollower = new MouseFollower(seg.el, {
  3379. additionalClass: 'fc-dragging',
  3380. parentEl: view.el,
  3381. opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
  3382. revertDuration: view.opt('dragRevertDuration'),
  3383. zIndex: 2 // one above the .fc-view
  3384. });
  3385. mouseFollower.hide(); // don't show until we know this is a real drag
  3386. mouseFollower.start(ev);
  3387. },
  3388. dragStart: function(ev) {
  3389. if (dragListener.isTouch && !view.isEventSelected(event)) {
  3390. // if not previously selected, will fire after a delay. then, select the event
  3391. view.selectEvent(event);
  3392. }
  3393. isDragging = true;
  3394. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3395. _this.segDragStart(seg, ev);
  3396. view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  3397. },
  3398. hitOver: function(hit, isOrig, origHit) {
  3399. var dragHelperEls;
  3400. // starting hit could be forced (DayGrid.limit)
  3401. if (seg.hit) {
  3402. origHit = seg.hit;
  3403. }
  3404. // since we are querying the parent view, might not belong to this grid
  3405. dropLocation = _this.computeEventDrop(
  3406. origHit.component.getHitSpan(origHit),
  3407. hit.component.getHitSpan(hit),
  3408. event
  3409. );
  3410. if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
  3411. disableCursor();
  3412. dropLocation = null;
  3413. }
  3414. // if a valid drop location, have the subclass render a visual indication
  3415. if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
  3416. dragHelperEls.addClass('fc-dragging');
  3417. if (!dragListener.isTouch) {
  3418. _this.applyDragOpacity(dragHelperEls);
  3419. }
  3420. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  3421. }
  3422. else {
  3423. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  3424. }
  3425. if (isOrig) {
  3426. dropLocation = null; // needs to have moved hits to be a valid drop
  3427. }
  3428. },
  3429. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  3430. view.unrenderDrag(); // unrender whatever was done in renderDrag
  3431. mouseFollower.show(); // show in case we are moving out of all hits
  3432. dropLocation = null;
  3433. },
  3434. hitDone: function() { // Called after a hitOut OR before a dragEnd
  3435. enableCursor();
  3436. },
  3437. interactionEnd: function(ev) {
  3438. delete seg.component; // prevent side effects
  3439. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  3440. mouseFollower.stop(!dropLocation, function() {
  3441. if (isDragging) {
  3442. view.unrenderDrag();
  3443. _this.segDragStop(seg, ev);
  3444. }
  3445. if (dropLocation) {
  3446. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  3447. view.reportEventDrop(event, dropLocation, _this.largeUnit, el, ev);
  3448. }
  3449. else {
  3450. view.showEvent(event);
  3451. }
  3452. });
  3453. _this.segDragListener = null;
  3454. }
  3455. });
  3456. return dragListener;
  3457. },
  3458. // seg isn't draggable, but let's use a generic DragListener
  3459. // simply for the delay, so it can be selected.
  3460. // Has side effect of setting/unsetting `segDragListener`
  3461. buildSegSelectListener: function(seg) {
  3462. var _this = this;
  3463. var view = this.view;
  3464. var event = seg.event;
  3465. if (this.segDragListener) {
  3466. return this.segDragListener;
  3467. }
  3468. var dragListener = this.segDragListener = new DragListener({
  3469. dragStart: function(ev) {
  3470. if (dragListener.isTouch && !view.isEventSelected(event)) {
  3471. // if not previously selected, will fire after a delay. then, select the event
  3472. view.selectEvent(event);
  3473. }
  3474. },
  3475. interactionEnd: function(ev) {
  3476. _this.segDragListener = null;
  3477. }
  3478. });
  3479. return dragListener;
  3480. },
  3481. // Called before event segment dragging starts
  3482. segDragStart: function(seg, ev) {
  3483. this.isDraggingSeg = true;
  3484. this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3485. },
  3486. // Called after event segment dragging stops
  3487. segDragStop: function(seg, ev) {
  3488. this.isDraggingSeg = false;
  3489. this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3490. },
  3491. // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
  3492. // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
  3493. // A falsy returned value indicates an invalid drop.
  3494. // DOES NOT consider overlap/constraint.
  3495. computeEventDrop: function(startSpan, endSpan, event) {
  3496. var calendar = this.view.calendar;
  3497. var dragStart = startSpan.start;
  3498. var dragEnd = endSpan.start;
  3499. var delta;
  3500. var dropLocation; // zoned event date properties
  3501. if (dragStart.hasTime() === dragEnd.hasTime()) {
  3502. delta = this.diffDates(dragEnd, dragStart);
  3503. // if an all-day event was in a timed area and it was dragged to a different time,
  3504. // guarantee an end and adjust start/end to have times
  3505. if (event.allDay && durationHasTime(delta)) {
  3506. dropLocation = {
  3507. start: event.start.clone(),
  3508. end: calendar.getEventEnd(event), // will be an ambig day
  3509. allDay: false // for normalizeEventTimes
  3510. };
  3511. calendar.normalizeEventTimes(dropLocation);
  3512. }
  3513. // othewise, work off existing values
  3514. else {
  3515. dropLocation = pluckEventDateProps(event);
  3516. }
  3517. dropLocation.start.add(delta);
  3518. if (dropLocation.end) {
  3519. dropLocation.end.add(delta);
  3520. }
  3521. }
  3522. else {
  3523. // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  3524. dropLocation = {
  3525. start: dragEnd.clone(),
  3526. end: null, // end should be cleared
  3527. allDay: !dragEnd.hasTime()
  3528. };
  3529. }
  3530. return dropLocation;
  3531. },
  3532. // Utility for apply dragOpacity to a jQuery set
  3533. applyDragOpacity: function(els) {
  3534. var opacity = this.view.opt('dragOpacity');
  3535. if (opacity != null) {
  3536. els.css('opacity', opacity);
  3537. }
  3538. },
  3539. /* External Element Dragging
  3540. ------------------------------------------------------------------------------------------------------------------*/
  3541. // Called when a jQuery UI drag is initiated anywhere in the DOM
  3542. externalDragStart: function(ev, ui) {
  3543. var view = this.view;
  3544. var el;
  3545. var accept;
  3546. if (view.opt('droppable')) { // only listen if this setting is on
  3547. el = $((ui ? ui.item : null) || ev.target);
  3548. // Test that the dragged element passes the dropAccept selector or filter function.
  3549. // FYI, the default is "*" (matches all)
  3550. accept = view.opt('dropAccept');
  3551. if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  3552. if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  3553. this.listenToExternalDrag(el, ev, ui);
  3554. }
  3555. }
  3556. }
  3557. },
  3558. // Called when a jQuery UI drag starts and it needs to be monitored for dropping
  3559. listenToExternalDrag: function(el, ev, ui) {
  3560. var _this = this;
  3561. var calendar = this.view.calendar;
  3562. var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  3563. var dropLocation; // a null value signals an unsuccessful drag
  3564. // listener that tracks mouse movement over date-associated pixel regions
  3565. var dragListener = _this.externalDragListener = new HitDragListener(this, {
  3566. interactionStart: function() {
  3567. _this.isDraggingExternal = true;
  3568. },
  3569. hitOver: function(hit) {
  3570. dropLocation = _this.computeExternalDrop(
  3571. hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
  3572. meta
  3573. );
  3574. if ( // invalid hit?
  3575. dropLocation &&
  3576. !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps)
  3577. ) {
  3578. disableCursor();
  3579. dropLocation = null;
  3580. }
  3581. if (dropLocation) {
  3582. _this.renderDrag(dropLocation); // called without a seg parameter
  3583. }
  3584. },
  3585. hitOut: function() {
  3586. dropLocation = null; // signal unsuccessful
  3587. },
  3588. hitDone: function() { // Called after a hitOut OR before a dragEnd
  3589. enableCursor();
  3590. _this.unrenderDrag();
  3591. },
  3592. interactionEnd: function(ev) {
  3593. if (dropLocation) { // element was dropped on a valid hit
  3594. _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
  3595. }
  3596. _this.isDraggingExternal = false;
  3597. _this.externalDragListener = null;
  3598. }
  3599. });
  3600. dragListener.startDrag(ev); // start listening immediately
  3601. },
  3602. // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  3603. // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
  3604. // Returning a null value signals an invalid drop hit.
  3605. // DOES NOT consider overlap/constraint.
  3606. computeExternalDrop: function(span, meta) {
  3607. var calendar = this.view.calendar;
  3608. var dropLocation = {
  3609. start: calendar.applyTimezone(span.start), // simulate a zoned event start date
  3610. end: null
  3611. };
  3612. // if dropped on an all-day span, and element's metadata specified a time, set it
  3613. if (meta.startTime && !dropLocation.start.hasTime()) {
  3614. dropLocation.start.time(meta.startTime);
  3615. }
  3616. if (meta.duration) {
  3617. dropLocation.end = dropLocation.start.clone().add(meta.duration);
  3618. }
  3619. return dropLocation;
  3620. },
  3621. /* Drag Rendering (for both events and an external elements)
  3622. ------------------------------------------------------------------------------------------------------------------*/
  3623. // Renders a visual indication of an event or external element being dragged.
  3624. // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
  3625. // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
  3626. // A truthy returned value indicates this method has rendered a helper element.
  3627. // Must return elements used for any mock events.
  3628. renderDrag: function(dropLocation, seg) {
  3629. // subclasses must implement
  3630. },
  3631. // Unrenders a visual indication of an event or external element being dragged
  3632. unrenderDrag: function() {
  3633. // subclasses must implement
  3634. },
  3635. /* Resizing
  3636. ------------------------------------------------------------------------------------------------------------------*/
  3637. // Creates a listener that tracks the user as they resize an event segment.
  3638. // Generic enough to work with any type of Grid.
  3639. buildSegResizeListener: function(seg, isStart) {
  3640. var _this = this;
  3641. var view = this.view;
  3642. var calendar = view.calendar;
  3643. var el = seg.el;
  3644. var event = seg.event;
  3645. var eventEnd = calendar.getEventEnd(event);
  3646. var isDragging;
  3647. var resizeLocation; // zoned event date properties. falsy if invalid resize
  3648. // Tracks mouse movement over the *grid's* coordinate map
  3649. var dragListener = this.segResizeListener = new HitDragListener(this, {
  3650. scroll: view.opt('dragScroll'),
  3651. subjectEl: el,
  3652. interactionStart: function() {
  3653. isDragging = false;
  3654. },
  3655. dragStart: function(ev) {
  3656. isDragging = true;
  3657. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3658. _this.segResizeStart(seg, ev);
  3659. },
  3660. hitOver: function(hit, isOrig, origHit) {
  3661. var origHitSpan = _this.getHitSpan(origHit);
  3662. var hitSpan = _this.getHitSpan(hit);
  3663. resizeLocation = isStart ?
  3664. _this.computeEventStartResize(origHitSpan, hitSpan, event) :
  3665. _this.computeEventEndResize(origHitSpan, hitSpan, event);
  3666. if (resizeLocation) {
  3667. if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
  3668. disableCursor();
  3669. resizeLocation = null;
  3670. }
  3671. // no change? (FYI, event dates might have zones)
  3672. else if (
  3673. resizeLocation.start.isSame(event.start.clone().stripZone()) &&
  3674. resizeLocation.end.isSame(eventEnd.clone().stripZone())
  3675. ) {
  3676. resizeLocation = null;
  3677. }
  3678. }
  3679. if (resizeLocation) {
  3680. view.hideEvent(event);
  3681. _this.renderEventResize(resizeLocation, seg);
  3682. }
  3683. },
  3684. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  3685. resizeLocation = null;
  3686. view.showEvent(event); // for when out-of-bounds. show original
  3687. },
  3688. hitDone: function() { // resets the rendering to show the original event
  3689. _this.unrenderEventResize();
  3690. enableCursor();
  3691. },
  3692. interactionEnd: function(ev) {
  3693. if (isDragging) {
  3694. _this.segResizeStop(seg, ev);
  3695. }
  3696. if (resizeLocation) { // valid date to resize to?
  3697. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  3698. view.reportEventResize(event, resizeLocation, _this.largeUnit, el, ev);
  3699. }
  3700. else {
  3701. view.showEvent(event);
  3702. }
  3703. _this.segResizeListener = null;
  3704. }
  3705. });
  3706. return dragListener;
  3707. },
  3708. // Called before event segment resizing starts
  3709. segResizeStart: function(seg, ev) {
  3710. this.isResizingSeg = true;
  3711. this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3712. },
  3713. // Called after event segment resizing stops
  3714. segResizeStop: function(seg, ev) {
  3715. this.isResizingSeg = false;
  3716. this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3717. },
  3718. // Returns new date-information for an event segment being resized from its start
  3719. computeEventStartResize: function(startSpan, endSpan, event) {
  3720. return this.computeEventResize('start', startSpan, endSpan, event);
  3721. },
  3722. // Returns new date-information for an event segment being resized from its end
  3723. computeEventEndResize: function(startSpan, endSpan, event) {
  3724. return this.computeEventResize('end', startSpan, endSpan, event);
  3725. },
  3726. // Returns new zoned date information for an event segment being resized from its start OR end
  3727. // `type` is either 'start' or 'end'.
  3728. // DOES NOT consider overlap/constraint.
  3729. computeEventResize: function(type, startSpan, endSpan, event) {
  3730. var calendar = this.view.calendar;
  3731. var delta = this.diffDates(endSpan[type], startSpan[type]);
  3732. var resizeLocation; // zoned event date properties
  3733. var defaultDuration;
  3734. // build original values to work from, guaranteeing a start and end
  3735. resizeLocation = {
  3736. start: event.start.clone(),
  3737. end: calendar.getEventEnd(event),
  3738. allDay: event.allDay
  3739. };
  3740. // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
  3741. if (resizeLocation.allDay && durationHasTime(delta)) {
  3742. resizeLocation.allDay = false;
  3743. calendar.normalizeEventTimes(resizeLocation);
  3744. }
  3745. resizeLocation[type].add(delta); // apply delta to start or end
  3746. // if the event was compressed too small, find a new reasonable duration for it
  3747. if (!resizeLocation.start.isBefore(resizeLocation.end)) {
  3748. defaultDuration =
  3749. this.minResizeDuration || // TODO: hack
  3750. (event.allDay ?
  3751. calendar.defaultAllDayEventDuration :
  3752. calendar.defaultTimedEventDuration);
  3753. if (type == 'start') { // resizing the start?
  3754. resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration);
  3755. }
  3756. else { // resizing the end?
  3757. resizeLocation.end = resizeLocation.start.clone().add(defaultDuration);
  3758. }
  3759. }
  3760. return resizeLocation;
  3761. },
  3762. // Renders a visual indication of an event being resized.
  3763. // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
  3764. // Must return elements used for any mock events.
  3765. renderEventResize: function(range, seg) {
  3766. // subclasses must implement
  3767. },
  3768. // Unrenders a visual indication of an event being resized.
  3769. unrenderEventResize: function() {
  3770. // subclasses must implement
  3771. },
  3772. /* Rendering Utils
  3773. ------------------------------------------------------------------------------------------------------------------*/
  3774. // Compute the text that should be displayed on an event's element.
  3775. // `range` can be the Event object itself, or something range-like, with at least a `start`.
  3776. // If event times are disabled, or the event has no time, will return a blank string.
  3777. // If not specified, formatStr will default to the eventTimeFormat setting,
  3778. // and displayEnd will default to the displayEventEnd setting.
  3779. getEventTimeText: function(range, formatStr, displayEnd) {
  3780. if (formatStr == null) {
  3781. formatStr = this.eventTimeFormat;
  3782. }
  3783. if (displayEnd == null) {
  3784. displayEnd = this.displayEventEnd;
  3785. }
  3786. if (this.displayEventTime && range.start.hasTime()) {
  3787. if (displayEnd && range.end) {
  3788. return this.view.formatRange(range, formatStr);
  3789. }
  3790. else {
  3791. return range.start.format(formatStr);
  3792. }
  3793. }
  3794. return '';
  3795. },
  3796. // Generic utility for generating the HTML classNames for an event segment's element
  3797. getSegClasses: function(seg, isDraggable, isResizable) {
  3798. var view = this.view;
  3799. var classes = [
  3800. 'fc-event',
  3801. seg.isStart ? 'fc-start' : 'fc-not-start',
  3802. seg.isEnd ? 'fc-end' : 'fc-not-end'
  3803. ].concat(this.getSegCustomClasses(seg));
  3804. if (isDraggable) {
  3805. classes.push('fc-draggable');
  3806. }
  3807. if (isResizable) {
  3808. classes.push('fc-resizable');
  3809. }
  3810. // event is currently selected? attach a className.
  3811. if (view.isEventSelected(seg.event)) {
  3812. classes.push('fc-selected');
  3813. }
  3814. return classes;
  3815. },
  3816. // List of classes that were defined by the caller of the API in some way
  3817. getSegCustomClasses: function(seg) {
  3818. var event = seg.event;
  3819. return [].concat(
  3820. event.className, // guaranteed to be an array
  3821. event.source ? event.source.className : []
  3822. );
  3823. },
  3824. // Utility for generating event skin-related CSS properties
  3825. getSegSkinCss: function(seg) {
  3826. return {
  3827. 'background-color': this.getSegBackgroundColor(seg),
  3828. 'border-color': this.getSegBorderColor(seg),
  3829. color: this.getSegTextColor(seg)
  3830. };
  3831. },
  3832. // Queries for caller-specified color, then falls back to default
  3833. getSegBackgroundColor: function(seg) {
  3834. return seg.event.backgroundColor ||
  3835. seg.event.color ||
  3836. this.getSegDefaultBackgroundColor(seg);
  3837. },
  3838. getSegDefaultBackgroundColor: function(seg) {
  3839. var source = seg.event.source || {};
  3840. return source.backgroundColor ||
  3841. source.color ||
  3842. this.view.opt('eventBackgroundColor') ||
  3843. this.view.opt('eventColor');
  3844. },
  3845. // Queries for caller-specified color, then falls back to default
  3846. getSegBorderColor: function(seg) {
  3847. return seg.event.borderColor ||
  3848. seg.event.color ||
  3849. this.getSegDefaultBorderColor(seg);
  3850. },
  3851. getSegDefaultBorderColor: function(seg) {
  3852. var source = seg.event.source || {};
  3853. return source.borderColor ||
  3854. source.color ||
  3855. this.view.opt('eventBorderColor') ||
  3856. this.view.opt('eventColor');
  3857. },
  3858. // Queries for caller-specified color, then falls back to default
  3859. getSegTextColor: function(seg) {
  3860. return seg.event.textColor ||
  3861. this.getSegDefaultTextColor(seg);
  3862. },
  3863. getSegDefaultTextColor: function(seg) {
  3864. var source = seg.event.source || {};
  3865. return source.textColor ||
  3866. this.view.opt('eventTextColor');
  3867. },
  3868. /* Converting events -> eventRange -> eventSpan -> eventSegs
  3869. ------------------------------------------------------------------------------------------------------------------*/
  3870. // Generates an array of segments for the given single event
  3871. // Can accept an event "location" as well (which only has start/end and no allDay)
  3872. eventToSegs: function(event) {
  3873. return this.eventsToSegs([ event ]);
  3874. },
  3875. eventToSpan: function(event) {
  3876. return this.eventToSpans(event)[0];
  3877. },
  3878. // Generates spans (always unzoned) for the given event.
  3879. // Does not do any inverting for inverse-background events.
  3880. // Can accept an event "location" as well (which only has start/end and no allDay)
  3881. eventToSpans: function(event) {
  3882. var range = this.eventToRange(event);
  3883. return this.eventRangeToSpans(range, event);
  3884. },
  3885. // Converts an array of event objects into an array of event segment objects.
  3886. // A custom `segSliceFunc` may be given for arbitrarily slicing up events.
  3887. // Doesn't guarantee an order for the resulting array.
  3888. eventsToSegs: function(allEvents, segSliceFunc) {
  3889. var _this = this;
  3890. var eventsById = groupEventsById(allEvents);
  3891. var segs = [];
  3892. $.each(eventsById, function(id, events) {
  3893. var ranges = [];
  3894. var i;
  3895. for (i = 0; i < events.length; i++) {
  3896. ranges.push(_this.eventToRange(events[i]));
  3897. }
  3898. // inverse-background events (utilize only the first event in calculations)
  3899. if (isInverseBgEvent(events[0])) {
  3900. ranges = _this.invertRanges(ranges);
  3901. for (i = 0; i < ranges.length; i++) {
  3902. segs.push.apply(segs, // append to
  3903. _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc));
  3904. }
  3905. }
  3906. // normal event ranges
  3907. else {
  3908. for (i = 0; i < ranges.length; i++) {
  3909. segs.push.apply(segs, // append to
  3910. _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc));
  3911. }
  3912. }
  3913. });
  3914. return segs;
  3915. },
  3916. // Generates the unzoned start/end dates an event appears to occupy
  3917. // Can accept an event "location" as well (which only has start/end and no allDay)
  3918. eventToRange: function(event) {
  3919. var calendar = this.view.calendar;
  3920. var start = event.start.clone().stripZone();
  3921. var end = (
  3922. event.end ?
  3923. event.end.clone() :
  3924. // derive the end from the start and allDay. compute allDay if necessary
  3925. calendar.getDefaultEventEnd(
  3926. event.allDay != null ?
  3927. event.allDay :
  3928. !event.start.hasTime(),
  3929. event.start
  3930. )
  3931. ).stripZone();
  3932. // hack: dynamic locale change forgets to upate stored event localed
  3933. calendar.localizeMoment(start);
  3934. calendar.localizeMoment(end);
  3935. return { start: start, end: end };
  3936. },
  3937. // Given an event's range (unzoned start/end), and the event itself,
  3938. // slice into segments (using the segSliceFunc function if specified)
  3939. eventRangeToSegs: function(range, event, segSliceFunc) {
  3940. var spans = this.eventRangeToSpans(range, event);
  3941. var segs = [];
  3942. var i;
  3943. for (i = 0; i < spans.length; i++) {
  3944. segs.push.apply(segs, // append to
  3945. this.eventSpanToSegs(spans[i], event, segSliceFunc));
  3946. }
  3947. return segs;
  3948. },
  3949. // Given an event's unzoned date range, return an array of "span" objects.
  3950. // Subclasses can override.
  3951. eventRangeToSpans: function(range, event) {
  3952. return [ $.extend({}, range) ]; // copy into a single-item array
  3953. },
  3954. // Given an event's span (unzoned start/end and other misc data), and the event itself,
  3955. // slices into segments and attaches event-derived properties to them.
  3956. eventSpanToSegs: function(span, event, segSliceFunc) {
  3957. var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span);
  3958. var i, seg;
  3959. for (i = 0; i < segs.length; i++) {
  3960. seg = segs[i];
  3961. seg.event = event;
  3962. seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned
  3963. seg.eventDurationMS = span.end - span.start;
  3964. }
  3965. return segs;
  3966. },
  3967. // Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
  3968. // SIDE EFFECT: will mutate the given array and will use its date references.
  3969. invertRanges: function(ranges) {
  3970. var view = this.view;
  3971. var viewStart = view.start.clone(); // need a copy
  3972. var viewEnd = view.end.clone(); // need a copy
  3973. var inverseRanges = [];
  3974. var start = viewStart; // the end of the previous range. the start of the new range
  3975. var i, range;
  3976. // ranges need to be in order. required for our date-walking algorithm
  3977. ranges.sort(compareRanges);
  3978. for (i = 0; i < ranges.length; i++) {
  3979. range = ranges[i];
  3980. // add the span of time before the event (if there is any)
  3981. if (range.start > start) { // compare millisecond time (skip any ambig logic)
  3982. inverseRanges.push({
  3983. start: start,
  3984. end: range.start
  3985. });
  3986. }
  3987. start = range.end;
  3988. }
  3989. // add the span of time after the last event (if there is any)
  3990. if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  3991. inverseRanges.push({
  3992. start: start,
  3993. end: viewEnd
  3994. });
  3995. }
  3996. return inverseRanges;
  3997. },
  3998. sortEventSegs: function(segs) {
  3999. segs.sort(proxy(this, 'compareEventSegs'));
  4000. },
  4001. // A cmp function for determining which segments should take visual priority
  4002. compareEventSegs: function(seg1, seg2) {
  4003. return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  4004. seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  4005. seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  4006. compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
  4007. }
  4008. });
  4009. /* Utilities
  4010. ----------------------------------------------------------------------------------------------------------------------*/
  4011. function pluckEventDateProps(event) {
  4012. return {
  4013. start: event.start.clone(),
  4014. end: event.end ? event.end.clone() : null,
  4015. allDay: event.allDay // keep it the same
  4016. };
  4017. }
  4018. FC.pluckEventDateProps = pluckEventDateProps;
  4019. function isBgEvent(event) { // returns true if background OR inverse-background
  4020. var rendering = getEventRendering(event);
  4021. return rendering === 'background' || rendering === 'inverse-background';
  4022. }
  4023. FC.isBgEvent = isBgEvent; // export
  4024. function isInverseBgEvent(event) {
  4025. return getEventRendering(event) === 'inverse-background';
  4026. }
  4027. function getEventRendering(event) {
  4028. return firstDefined((event.source || {}).rendering, event.rendering);
  4029. }
  4030. function groupEventsById(events) {
  4031. var eventsById = {};
  4032. var i, event;
  4033. for (i = 0; i < events.length; i++) {
  4034. event = events[i];
  4035. (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  4036. }
  4037. return eventsById;
  4038. }
  4039. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  4040. function compareRanges(range1, range2) {
  4041. return range1.start - range2.start; // earlier ranges go first
  4042. }
  4043. /* External-Dragging-Element Data
  4044. ----------------------------------------------------------------------------------------------------------------------*/
  4045. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  4046. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  4047. FC.dataAttrPrefix = '';
  4048. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  4049. // to be used for Event Object creation.
  4050. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  4051. function getDraggedElMeta(el) {
  4052. var prefix = FC.dataAttrPrefix;
  4053. var eventProps; // properties for creating the event, not related to date/time
  4054. var startTime; // a Duration
  4055. var duration;
  4056. var stick;
  4057. if (prefix) { prefix += '-'; }
  4058. eventProps = el.data(prefix + 'event') || null;
  4059. if (eventProps) {
  4060. if (typeof eventProps === 'object') {
  4061. eventProps = $.extend({}, eventProps); // make a copy
  4062. }
  4063. else { // something like 1 or true. still signal event creation
  4064. eventProps = {};
  4065. }
  4066. // pluck special-cased date/time properties
  4067. startTime = eventProps.start;
  4068. if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  4069. duration = eventProps.duration;
  4070. stick = eventProps.stick;
  4071. delete eventProps.start;
  4072. delete eventProps.time;
  4073. delete eventProps.duration;
  4074. delete eventProps.stick;
  4075. }
  4076. // fallback to standalone attribute values for each of the date/time properties
  4077. if (startTime == null) { startTime = el.data(prefix + 'start'); }
  4078. if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  4079. if (duration == null) { duration = el.data(prefix + 'duration'); }
  4080. if (stick == null) { stick = el.data(prefix + 'stick'); }
  4081. // massage into correct data types
  4082. startTime = startTime != null ? moment.duration(startTime) : null;
  4083. duration = duration != null ? moment.duration(duration) : null;
  4084. stick = Boolean(stick);
  4085. return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  4086. }
  4087. ;;
  4088. /*
  4089. A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
  4090. Prerequisite: the object being mixed into needs to be a *Grid*
  4091. */
  4092. var DayTableMixin = FC.DayTableMixin = {
  4093. breakOnWeeks: false, // should create a new row for each week?
  4094. dayDates: null, // whole-day dates for each column. left to right
  4095. dayIndices: null, // for each day from start, the offset
  4096. daysPerRow: null,
  4097. rowCnt: null,
  4098. colCnt: null,
  4099. colHeadFormat: null,
  4100. // Populates internal variables used for date calculation and rendering
  4101. updateDayTable: function() {
  4102. var view = this.view;
  4103. var date = this.start.clone();
  4104. var dayIndex = -1;
  4105. var dayIndices = [];
  4106. var dayDates = [];
  4107. var daysPerRow;
  4108. var firstDay;
  4109. var rowCnt;
  4110. while (date.isBefore(this.end)) { // loop each day from start to end
  4111. if (view.isHiddenDay(date)) {
  4112. dayIndices.push(dayIndex + 0.5); // mark that it's between indices
  4113. }
  4114. else {
  4115. dayIndex++;
  4116. dayIndices.push(dayIndex);
  4117. dayDates.push(date.clone());
  4118. }
  4119. date.add(1, 'days');
  4120. }
  4121. if (this.breakOnWeeks) {
  4122. // count columns until the day-of-week repeats
  4123. firstDay = dayDates[0].day();
  4124. for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
  4125. if (dayDates[daysPerRow].day() == firstDay) {
  4126. break;
  4127. }
  4128. }
  4129. rowCnt = Math.ceil(dayDates.length / daysPerRow);
  4130. }
  4131. else {
  4132. rowCnt = 1;
  4133. daysPerRow = dayDates.length;
  4134. }
  4135. this.dayDates = dayDates;
  4136. this.dayIndices = dayIndices;
  4137. this.daysPerRow = daysPerRow;
  4138. this.rowCnt = rowCnt;
  4139. this.updateDayTableCols();
  4140. },
  4141. // Computes and assigned the colCnt property and updates any options that may be computed from it
  4142. updateDayTableCols: function() {
  4143. this.colCnt = this.computeColCnt();
  4144. this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat();
  4145. },
  4146. // Determines how many columns there should be in the table
  4147. computeColCnt: function() {
  4148. return this.daysPerRow;
  4149. },
  4150. // Computes the ambiguously-timed moment for the given cell
  4151. getCellDate: function(row, col) {
  4152. return this.dayDates[
  4153. this.getCellDayIndex(row, col)
  4154. ].clone();
  4155. },
  4156. // Computes the ambiguously-timed date range for the given cell
  4157. getCellRange: function(row, col) {
  4158. var start = this.getCellDate(row, col);
  4159. var end = start.clone().add(1, 'days');
  4160. return { start: start, end: end };
  4161. },
  4162. // Returns the number of day cells, chronologically, from the first of the grid (0-based)
  4163. getCellDayIndex: function(row, col) {
  4164. return row * this.daysPerRow + this.getColDayIndex(col);
  4165. },
  4166. // Returns the numner of day cells, chronologically, from the first cell in *any given row*
  4167. getColDayIndex: function(col) {
  4168. if (this.isRTL) {
  4169. return this.colCnt - 1 - col;
  4170. }
  4171. else {
  4172. return col;
  4173. }
  4174. },
  4175. // Given a date, returns its chronolocial cell-index from the first cell of the grid.
  4176. // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
  4177. // If before the first offset, returns a negative number.
  4178. // If after the last offset, returns an offset past the last cell offset.
  4179. // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
  4180. getDateDayIndex: function(date) {
  4181. var dayIndices = this.dayIndices;
  4182. var dayOffset = date.diff(this.start, 'days');
  4183. if (dayOffset < 0) {
  4184. return dayIndices[0] - 1;
  4185. }
  4186. else if (dayOffset >= dayIndices.length) {
  4187. return dayIndices[dayIndices.length - 1] + 1;
  4188. }
  4189. else {
  4190. return dayIndices[dayOffset];
  4191. }
  4192. },
  4193. /* Options
  4194. ------------------------------------------------------------------------------------------------------------------*/
  4195. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  4196. computeColHeadFormat: function() {
  4197. // if more than one week row, or if there are a lot of columns with not much space,
  4198. // put just the day numbers will be in each cell
  4199. if (this.rowCnt > 1 || this.colCnt > 10) {
  4200. return 'ddd'; // "Sat"
  4201. }
  4202. // multiple days, so full single date string WON'T be in title text
  4203. else if (this.colCnt > 1) {
  4204. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  4205. }
  4206. // single day, so full single date string will probably be in title text
  4207. else {
  4208. return 'dddd'; // "Saturday"
  4209. }
  4210. },
  4211. /* Slicing
  4212. ------------------------------------------------------------------------------------------------------------------*/
  4213. // Slices up a date range into a segment for every week-row it intersects with
  4214. sliceRangeByRow: function(range) {
  4215. var daysPerRow = this.daysPerRow;
  4216. var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  4217. var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
  4218. var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
  4219. var segs = [];
  4220. var row;
  4221. var rowFirst, rowLast; // inclusive day-index range for current row
  4222. var segFirst, segLast; // inclusive day-index range for segment
  4223. for (row = 0; row < this.rowCnt; row++) {
  4224. rowFirst = row * daysPerRow;
  4225. rowLast = rowFirst + daysPerRow - 1;
  4226. // intersect segment's offset range with the row's
  4227. segFirst = Math.max(rangeFirst, rowFirst);
  4228. segLast = Math.min(rangeLast, rowLast);
  4229. // deal with in-between indices
  4230. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  4231. segLast = Math.floor(segLast); // in-between ends round to prev cell
  4232. if (segFirst <= segLast) { // was there any intersection with the current row?
  4233. segs.push({
  4234. row: row,
  4235. // normalize to start of row
  4236. firstRowDayIndex: segFirst - rowFirst,
  4237. lastRowDayIndex: segLast - rowFirst,
  4238. // must be matching integers to be the segment's start/end
  4239. isStart: segFirst === rangeFirst,
  4240. isEnd: segLast === rangeLast
  4241. });
  4242. }
  4243. }
  4244. return segs;
  4245. },
  4246. // Slices up a date range into a segment for every day-cell it intersects with.
  4247. // TODO: make more DRY with sliceRangeByRow somehow.
  4248. sliceRangeByDay: function(range) {
  4249. var daysPerRow = this.daysPerRow;
  4250. var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  4251. var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
  4252. var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
  4253. var segs = [];
  4254. var row;
  4255. var rowFirst, rowLast; // inclusive day-index range for current row
  4256. var i;
  4257. var segFirst, segLast; // inclusive day-index range for segment
  4258. for (row = 0; row < this.rowCnt; row++) {
  4259. rowFirst = row * daysPerRow;
  4260. rowLast = rowFirst + daysPerRow - 1;
  4261. for (i = rowFirst; i <= rowLast; i++) {
  4262. // intersect segment's offset range with the row's
  4263. segFirst = Math.max(rangeFirst, i);
  4264. segLast = Math.min(rangeLast, i);
  4265. // deal with in-between indices
  4266. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  4267. segLast = Math.floor(segLast); // in-between ends round to prev cell
  4268. if (segFirst <= segLast) { // was there any intersection with the current row?
  4269. segs.push({
  4270. row: row,
  4271. // normalize to start of row
  4272. firstRowDayIndex: segFirst - rowFirst,
  4273. lastRowDayIndex: segLast - rowFirst,
  4274. // must be matching integers to be the segment's start/end
  4275. isStart: segFirst === rangeFirst,
  4276. isEnd: segLast === rangeLast
  4277. });
  4278. }
  4279. }
  4280. }
  4281. return segs;
  4282. },
  4283. /* Header Rendering
  4284. ------------------------------------------------------------------------------------------------------------------*/
  4285. renderHeadHtml: function() {
  4286. var view = this.view;
  4287. return '' +
  4288. '<div class="fc-row ' + view.widgetHeaderClass + '">' +
  4289. '<table>' +
  4290. '<thead>' +
  4291. this.renderHeadTrHtml() +
  4292. '</thead>' +
  4293. '</table>' +
  4294. '</div>';
  4295. },
  4296. renderHeadIntroHtml: function() {
  4297. return this.renderIntroHtml(); // fall back to generic
  4298. },
  4299. renderHeadTrHtml: function() {
  4300. return '' +
  4301. '<tr>' +
  4302. (this.isRTL ? '' : this.renderHeadIntroHtml()) +
  4303. this.renderHeadDateCellsHtml() +
  4304. (this.isRTL ? this.renderHeadIntroHtml() : '') +
  4305. '</tr>';
  4306. },
  4307. renderHeadDateCellsHtml: function() {
  4308. var htmls = [];
  4309. var col, date;
  4310. for (col = 0; col < this.colCnt; col++) {
  4311. date = this.getCellDate(0, col);
  4312. htmls.push(this.renderHeadDateCellHtml(date));
  4313. }
  4314. return htmls.join('');
  4315. },
  4316. // TODO: when internalApiVersion, accept an object for HTML attributes
  4317. // (colspan should be no different)
  4318. renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
  4319. var view = this.view;
  4320. var classNames = [
  4321. 'fc-day-header',
  4322. view.widgetHeaderClass
  4323. ];
  4324. // if only one row of days, the classNames on the header can represent the specific days beneath
  4325. if (this.rowCnt === 1) {
  4326. classNames = classNames.concat(
  4327. // includes the day-of-week class
  4328. // noThemeHighlight=true (don't highlight the header)
  4329. this.getDayClasses(date, true)
  4330. );
  4331. }
  4332. else {
  4333. classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class
  4334. }
  4335. return '' +
  4336. '<th class="' + classNames.join(' ') + '"' +
  4337. (this.rowCnt === 1 ?
  4338. ' data-date="' + date.format('YYYY-MM-DD') + '"' :
  4339. '') +
  4340. (colspan > 1 ?
  4341. ' colspan="' + colspan + '"' :
  4342. '') +
  4343. (otherAttrs ?
  4344. ' ' + otherAttrs :
  4345. '') +
  4346. '>' +
  4347. // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
  4348. view.buildGotoAnchorHtml(
  4349. { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
  4350. htmlEscape(date.format(this.colHeadFormat)) // inner HTML
  4351. ) +
  4352. '</th>';
  4353. },
  4354. /* Background Rendering
  4355. ------------------------------------------------------------------------------------------------------------------*/
  4356. renderBgTrHtml: function(row) {
  4357. return '' +
  4358. '<tr>' +
  4359. (this.isRTL ? '' : this.renderBgIntroHtml(row)) +
  4360. this.renderBgCellsHtml(row) +
  4361. (this.isRTL ? this.renderBgIntroHtml(row) : '') +
  4362. '</tr>';
  4363. },
  4364. renderBgIntroHtml: function(row) {
  4365. return this.renderIntroHtml(); // fall back to generic
  4366. },
  4367. renderBgCellsHtml: function(row) {
  4368. var htmls = [];
  4369. var col, date;
  4370. for (col = 0; col < this.colCnt; col++) {
  4371. date = this.getCellDate(row, col);
  4372. htmls.push(this.renderBgCellHtml(date));
  4373. }
  4374. return htmls.join('');
  4375. },
  4376. renderBgCellHtml: function(date, otherAttrs) {
  4377. var view = this.view;
  4378. var classes = this.getDayClasses(date);
  4379. classes.unshift('fc-day', view.widgetContentClass);
  4380. return '<td class="' + classes.join(' ') + '"' +
  4381. ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  4382. (otherAttrs ?
  4383. ' ' + otherAttrs :
  4384. '') +
  4385. '></td>';
  4386. },
  4387. /* Generic
  4388. ------------------------------------------------------------------------------------------------------------------*/
  4389. // Generates the default HTML intro for any row. User classes should override
  4390. renderIntroHtml: function() {
  4391. },
  4392. // TODO: a generic method for dealing with <tr>, RTL, intro
  4393. // when increment internalApiVersion
  4394. // wrapTr (scheduler)
  4395. /* Utils
  4396. ------------------------------------------------------------------------------------------------------------------*/
  4397. // Applies the generic "intro" and "outro" HTML to the given cells.
  4398. // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  4399. bookendCells: function(trEl) {
  4400. var introHtml = this.renderIntroHtml();
  4401. if (introHtml) {
  4402. if (this.isRTL) {
  4403. trEl.append(introHtml);
  4404. }
  4405. else {
  4406. trEl.prepend(introHtml);
  4407. }
  4408. }
  4409. }
  4410. };
  4411. ;;
  4412. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  4413. ----------------------------------------------------------------------------------------------------------------------*/
  4414. var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
  4415. numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  4416. bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  4417. rowEls: null, // set of fake row elements
  4418. cellEls: null, // set of whole-day elements comprising the row's background
  4419. helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  4420. rowCoordCache: null,
  4421. colCoordCache: null,
  4422. // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  4423. // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  4424. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  4425. renderDates: function(isRigid) {
  4426. var view = this.view;
  4427. var rowCnt = this.rowCnt;
  4428. var colCnt = this.colCnt;
  4429. var html = '';
  4430. var row;
  4431. var col;
  4432. for (row = 0; row < rowCnt; row++) {
  4433. html += this.renderDayRowHtml(row, isRigid);
  4434. }
  4435. this.el.html(html);
  4436. this.rowEls = this.el.find('.fc-row');
  4437. this.cellEls = this.el.find('.fc-day');
  4438. this.rowCoordCache = new CoordCache({
  4439. els: this.rowEls,
  4440. isVertical: true
  4441. });
  4442. this.colCoordCache = new CoordCache({
  4443. els: this.cellEls.slice(0, this.colCnt), // only the first row
  4444. isHorizontal: true
  4445. });
  4446. // trigger dayRender with each cell's element
  4447. for (row = 0; row < rowCnt; row++) {
  4448. for (col = 0; col < colCnt; col++) {
  4449. view.publiclyTrigger(
  4450. 'dayRender',
  4451. null,
  4452. this.getCellDate(row, col),
  4453. this.getCellEl(row, col)
  4454. );
  4455. }
  4456. }
  4457. },
  4458. unrenderDates: function() {
  4459. this.removeSegPopover();
  4460. },
  4461. renderBusinessHours: function() {
  4462. var segs = this.buildBusinessHourSegs(true); // wholeDay=true
  4463. this.renderFill('businessHours', segs, 'bgevent');
  4464. },
  4465. unrenderBusinessHours: function() {
  4466. this.unrenderFill('businessHours');
  4467. },
  4468. // Generates the HTML for a single row, which is a div that wraps a table.
  4469. // `row` is the row number.
  4470. renderDayRowHtml: function(row, isRigid) {
  4471. var view = this.view;
  4472. var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  4473. if (isRigid) {
  4474. classes.push('fc-rigid');
  4475. }
  4476. return '' +
  4477. '<div class="' + classes.join(' ') + '">' +
  4478. '<div class="fc-bg">' +
  4479. '<table>' +
  4480. this.renderBgTrHtml(row) +
  4481. '</table>' +
  4482. '</div>' +
  4483. '<div class="fc-content-skeleton">' +
  4484. '<table>' +
  4485. (this.numbersVisible ?
  4486. '<thead>' +
  4487. this.renderNumberTrHtml(row) +
  4488. '</thead>' :
  4489. ''
  4490. ) +
  4491. '</table>' +
  4492. '</div>' +
  4493. '</div>';
  4494. },
  4495. /* Grid Number Rendering
  4496. ------------------------------------------------------------------------------------------------------------------*/
  4497. renderNumberTrHtml: function(row) {
  4498. return '' +
  4499. '<tr>' +
  4500. (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
  4501. this.renderNumberCellsHtml(row) +
  4502. (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
  4503. '</tr>';
  4504. },
  4505. renderNumberIntroHtml: function(row) {
  4506. return this.renderIntroHtml();
  4507. },
  4508. renderNumberCellsHtml: function(row) {
  4509. var htmls = [];
  4510. var col, date;
  4511. for (col = 0; col < this.colCnt; col++) {
  4512. date = this.getCellDate(row, col);
  4513. htmls.push(this.renderNumberCellHtml(date));
  4514. }
  4515. return htmls.join('');
  4516. },
  4517. // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  4518. // The number row will only exist if either day numbers or week numbers are turned on.
  4519. renderNumberCellHtml: function(date) {
  4520. var html = '';
  4521. var classes;
  4522. var weekCalcFirstDoW;
  4523. if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) {
  4524. // no numbers in day cell (week number must be along the side)
  4525. return '<td/>'; // will create an empty space above events :(
  4526. }
  4527. classes = this.getDayClasses(date);
  4528. classes.unshift('fc-day-top');
  4529. if (this.view.cellWeekNumbersVisible) {
  4530. // To determine the day of week number change under ISO, we cannot
  4531. // rely on moment.js methods such as firstDayOfWeek() or weekday(),
  4532. // because they rely on the locale's dow (possibly overridden by
  4533. // our firstDay option), which may not be Monday. We cannot change
  4534. // dow, because that would affect the calendar start day as well.
  4535. if (date._locale._fullCalendar_weekCalc === 'ISO') {
  4536. weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
  4537. }
  4538. else {
  4539. weekCalcFirstDoW = date._locale.firstDayOfWeek();
  4540. }
  4541. }
  4542. html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">';
  4543. if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
  4544. html += this.view.buildGotoAnchorHtml(
  4545. { date: date, type: 'week' },
  4546. { 'class': 'fc-week-number' },
  4547. date.format('w') // inner HTML
  4548. );
  4549. }
  4550. if (this.view.dayNumbersVisible) {
  4551. html += this.view.buildGotoAnchorHtml(
  4552. date,
  4553. { 'class': 'fc-day-number' },
  4554. date.date() // inner HTML
  4555. );
  4556. }
  4557. html += '</td>';
  4558. return html;
  4559. },
  4560. /* Options
  4561. ------------------------------------------------------------------------------------------------------------------*/
  4562. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  4563. computeEventTimeFormat: function() {
  4564. return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
  4565. },
  4566. // Computes a default `displayEventEnd` value if one is not expliclty defined
  4567. computeDisplayEventEnd: function() {
  4568. return this.colCnt == 1; // we'll likely have space if there's only one day
  4569. },
  4570. /* Dates
  4571. ------------------------------------------------------------------------------------------------------------------*/
  4572. rangeUpdated: function() {
  4573. this.updateDayTable();
  4574. },
  4575. // Slices up the given span (unzoned start/end with other misc data) into an array of segments
  4576. spanToSegs: function(span) {
  4577. var segs = this.sliceRangeByRow(span);
  4578. var i, seg;
  4579. for (i = 0; i < segs.length; i++) {
  4580. seg = segs[i];
  4581. if (this.isRTL) {
  4582. seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
  4583. seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
  4584. }
  4585. else {
  4586. seg.leftCol = seg.firstRowDayIndex;
  4587. seg.rightCol = seg.lastRowDayIndex;
  4588. }
  4589. }
  4590. return segs;
  4591. },
  4592. /* Hit System
  4593. ------------------------------------------------------------------------------------------------------------------*/
  4594. prepareHits: function() {
  4595. this.colCoordCache.build();
  4596. this.rowCoordCache.build();
  4597. this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
  4598. },
  4599. releaseHits: function() {
  4600. this.colCoordCache.clear();
  4601. this.rowCoordCache.clear();
  4602. },
  4603. queryHit: function(leftOffset, topOffset) {
  4604. if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
  4605. var col = this.colCoordCache.getHorizontalIndex(leftOffset);
  4606. var row = this.rowCoordCache.getVerticalIndex(topOffset);
  4607. if (row != null && col != null) {
  4608. return this.getCellHit(row, col);
  4609. }
  4610. }
  4611. },
  4612. getHitSpan: function(hit) {
  4613. return this.getCellRange(hit.row, hit.col);
  4614. },
  4615. getHitEl: function(hit) {
  4616. return this.getCellEl(hit.row, hit.col);
  4617. },
  4618. /* Cell System
  4619. ------------------------------------------------------------------------------------------------------------------*/
  4620. // FYI: the first column is the leftmost column, regardless of date
  4621. getCellHit: function(row, col) {
  4622. return {
  4623. row: row,
  4624. col: col,
  4625. component: this, // needed unfortunately :(
  4626. left: this.colCoordCache.getLeftOffset(col),
  4627. right: this.colCoordCache.getRightOffset(col),
  4628. top: this.rowCoordCache.getTopOffset(row),
  4629. bottom: this.rowCoordCache.getBottomOffset(row)
  4630. };
  4631. },
  4632. getCellEl: function(row, col) {
  4633. return this.cellEls.eq(row * this.colCnt + col);
  4634. },
  4635. /* Event Drag Visualization
  4636. ------------------------------------------------------------------------------------------------------------------*/
  4637. // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
  4638. // Renders a visual indication of an event or external element being dragged.
  4639. // `eventLocation` has zoned start and end (optional)
  4640. renderDrag: function(eventLocation, seg) {
  4641. // always render a highlight underneath
  4642. this.renderHighlight(this.eventToSpan(eventLocation));
  4643. // if a segment from the same calendar but another component is being dragged, render a helper event
  4644. if (seg && seg.component !== this) {
  4645. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  4646. }
  4647. },
  4648. // Unrenders any visual indication of a hovering event
  4649. unrenderDrag: function() {
  4650. this.unrenderHighlight();
  4651. this.unrenderHelper();
  4652. },
  4653. /* Event Resize Visualization
  4654. ------------------------------------------------------------------------------------------------------------------*/
  4655. // Renders a visual indication of an event being resized
  4656. renderEventResize: function(eventLocation, seg) {
  4657. this.renderHighlight(this.eventToSpan(eventLocation));
  4658. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  4659. },
  4660. // Unrenders a visual indication of an event being resized
  4661. unrenderEventResize: function() {
  4662. this.unrenderHighlight();
  4663. this.unrenderHelper();
  4664. },
  4665. /* Event Helper
  4666. ------------------------------------------------------------------------------------------------------------------*/
  4667. // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  4668. renderHelper: function(event, sourceSeg) {
  4669. var helperNodes = [];
  4670. var segs = this.eventToSegs(event);
  4671. var rowStructs;
  4672. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  4673. rowStructs = this.renderSegRows(segs);
  4674. // inject each new event skeleton into each associated row
  4675. this.rowEls.each(function(row, rowNode) {
  4676. var rowEl = $(rowNode); // the .fc-row
  4677. var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  4678. var skeletonTop;
  4679. // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  4680. if (sourceSeg && sourceSeg.row === row) {
  4681. skeletonTop = sourceSeg.el.position().top;
  4682. }
  4683. else {
  4684. skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  4685. }
  4686. skeletonEl.css('top', skeletonTop)
  4687. .find('table')
  4688. .append(rowStructs[row].tbodyEl);
  4689. rowEl.append(skeletonEl);
  4690. helperNodes.push(skeletonEl[0]);
  4691. });
  4692. return ( // must return the elements rendered
  4693. this.helperEls = $(helperNodes) // array -> jQuery set
  4694. );
  4695. },
  4696. // Unrenders any visual indication of a mock helper event
  4697. unrenderHelper: function() {
  4698. if (this.helperEls) {
  4699. this.helperEls.remove();
  4700. this.helperEls = null;
  4701. }
  4702. },
  4703. /* Fill System (highlight, background events, business hours)
  4704. ------------------------------------------------------------------------------------------------------------------*/
  4705. fillSegTag: 'td', // override the default tag name
  4706. // Renders a set of rectangles over the given segments of days.
  4707. // Only returns segments that successfully rendered.
  4708. renderFill: function(type, segs, className) {
  4709. var nodes = [];
  4710. var i, seg;
  4711. var skeletonEl;
  4712. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  4713. for (i = 0; i < segs.length; i++) {
  4714. seg = segs[i];
  4715. skeletonEl = this.renderFillRow(type, seg, className);
  4716. this.rowEls.eq(seg.row).append(skeletonEl);
  4717. nodes.push(skeletonEl[0]);
  4718. }
  4719. this.elsByFill[type] = $(nodes);
  4720. return segs;
  4721. },
  4722. // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  4723. renderFillRow: function(type, seg, className) {
  4724. var colCnt = this.colCnt;
  4725. var startCol = seg.leftCol;
  4726. var endCol = seg.rightCol + 1;
  4727. var skeletonEl;
  4728. var trEl;
  4729. className = className || type.toLowerCase();
  4730. skeletonEl = $(
  4731. '<div class="fc-' + className + '-skeleton">' +
  4732. '<table><tr/></table>' +
  4733. '</div>'
  4734. );
  4735. trEl = skeletonEl.find('tr');
  4736. if (startCol > 0) {
  4737. trEl.append('<td colspan="' + startCol + '"/>');
  4738. }
  4739. trEl.append(
  4740. seg.el.attr('colspan', endCol - startCol)
  4741. );
  4742. if (endCol < colCnt) {
  4743. trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  4744. }
  4745. this.bookendCells(trEl);
  4746. return skeletonEl;
  4747. }
  4748. });
  4749. ;;
  4750. /* Event-rendering methods for the DayGrid class
  4751. ----------------------------------------------------------------------------------------------------------------------*/
  4752. DayGrid.mixin({
  4753. rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  4754. // Unrenders all events currently rendered on the grid
  4755. unrenderEvents: function() {
  4756. this.removeSegPopover(); // removes the "more.." events popover
  4757. Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
  4758. },
  4759. // Retrieves all rendered segment objects currently rendered on the grid
  4760. getEventSegs: function() {
  4761. return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
  4762. .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  4763. },
  4764. // Renders the given background event segments onto the grid
  4765. renderBgSegs: function(segs) {
  4766. // don't render timed background events
  4767. var allDaySegs = $.grep(segs, function(seg) {
  4768. return seg.event.allDay;
  4769. });
  4770. return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  4771. },
  4772. // Renders the given foreground event segments onto the grid
  4773. renderFgSegs: function(segs) {
  4774. var rowStructs;
  4775. // render an `.el` on each seg
  4776. // returns a subset of the segs. segs that were actually rendered
  4777. segs = this.renderFgSegEls(segs);
  4778. rowStructs = this.rowStructs = this.renderSegRows(segs);
  4779. // append to each row's content skeleton
  4780. this.rowEls.each(function(i, rowNode) {
  4781. $(rowNode).find('.fc-content-skeleton > table').append(
  4782. rowStructs[i].tbodyEl
  4783. );
  4784. });
  4785. return segs; // return only the segs that were actually rendered
  4786. },
  4787. // Unrenders all currently rendered foreground event segments
  4788. unrenderFgSegs: function() {
  4789. var rowStructs = this.rowStructs || [];
  4790. var rowStruct;
  4791. while ((rowStruct = rowStructs.pop())) {
  4792. rowStruct.tbodyEl.remove();
  4793. }
  4794. this.rowStructs = null;
  4795. },
  4796. // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  4797. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  4798. // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  4799. renderSegRows: function(segs) {
  4800. var rowStructs = [];
  4801. var segRows;
  4802. var row;
  4803. segRows = this.groupSegRows(segs); // group into nested arrays
  4804. // iterate each row of segment groupings
  4805. for (row = 0; row < segRows.length; row++) {
  4806. rowStructs.push(
  4807. this.renderSegRow(row, segRows[row])
  4808. );
  4809. }
  4810. return rowStructs;
  4811. },
  4812. // Builds the HTML to be used for the default element for an individual segment
  4813. fgSegHtml: function(seg, disableResizing) {
  4814. var view = this.view;
  4815. var event = seg.event;
  4816. var isDraggable = view.isEventDraggable(event);
  4817. var isResizableFromStart = !disableResizing && event.allDay &&
  4818. seg.isStart && view.isEventResizableFromStart(event);
  4819. var isResizableFromEnd = !disableResizing && event.allDay &&
  4820. seg.isEnd && view.isEventResizableFromEnd(event);
  4821. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  4822. var skinCss = cssToStr(this.getSegSkinCss(seg));
  4823. var timeHtml = '';
  4824. var timeText;
  4825. var titleHtml;
  4826. classes.unshift('fc-day-grid-event', 'fc-h-event');
  4827. // Only display a timed events time if it is the starting segment
  4828. if (seg.isStart) {
  4829. timeText = this.getEventTimeText(event);
  4830. if (timeText) {
  4831. timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
  4832. }
  4833. }
  4834. titleHtml =
  4835. '<span class="fc-title">' +
  4836. (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  4837. '</span>';
  4838. return '<a class="' + classes.join(' ') + '"' +
  4839. (event.url ?
  4840. ' href="' + htmlEscape(event.url) + '"' :
  4841. ''
  4842. ) +
  4843. (skinCss ?
  4844. ' style="' + skinCss + '"' :
  4845. ''
  4846. ) +
  4847. '>' +
  4848. '<div class="fc-content">' +
  4849. (this.isRTL ?
  4850. titleHtml + ' ' + timeHtml : // put a natural space in between
  4851. timeHtml + ' ' + titleHtml //
  4852. ) +
  4853. '</div>' +
  4854. (isResizableFromStart ?
  4855. '<div class="fc-resizer fc-start-resizer" />' :
  4856. ''
  4857. ) +
  4858. (isResizableFromEnd ?
  4859. '<div class="fc-resizer fc-end-resizer" />' :
  4860. ''
  4861. ) +
  4862. '</a>';
  4863. },
  4864. // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  4865. // the segments. Returns object with a bunch of internal data about how the render was calculated.
  4866. // NOTE: modifies rowSegs
  4867. renderSegRow: function(row, rowSegs) {
  4868. var colCnt = this.colCnt;
  4869. var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  4870. var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  4871. var tbody = $('<tbody/>');
  4872. var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  4873. var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  4874. var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  4875. var i, levelSegs;
  4876. var col;
  4877. var tr;
  4878. var j, seg;
  4879. var td;
  4880. // populates empty cells from the current column (`col`) to `endCol`
  4881. function emptyCellsUntil(endCol) {
  4882. while (col < endCol) {
  4883. // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  4884. td = (loneCellMatrix[i - 1] || [])[col];
  4885. if (td) {
  4886. td.attr(
  4887. 'rowspan',
  4888. parseInt(td.attr('rowspan') || 1, 10) + 1
  4889. );
  4890. }
  4891. else {
  4892. td = $('<td/>');
  4893. tr.append(td);
  4894. }
  4895. cellMatrix[i][col] = td;
  4896. loneCellMatrix[i][col] = td;
  4897. col++;
  4898. }
  4899. }
  4900. for (i = 0; i < levelCnt; i++) { // iterate through all levels
  4901. levelSegs = segLevels[i];
  4902. col = 0;
  4903. tr = $('<tr/>');
  4904. segMatrix.push([]);
  4905. cellMatrix.push([]);
  4906. loneCellMatrix.push([]);
  4907. // levelCnt might be 1 even though there are no actual levels. protect against this.
  4908. // this single empty row is useful for styling.
  4909. if (levelSegs) {
  4910. for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  4911. seg = levelSegs[j];
  4912. emptyCellsUntil(seg.leftCol);
  4913. // create a container that occupies or more columns. append the event element.
  4914. td = $('<td class="fc-event-container"/>').append(seg.el);
  4915. if (seg.leftCol != seg.rightCol) {
  4916. td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  4917. }
  4918. else { // a single-column segment
  4919. loneCellMatrix[i][col] = td;
  4920. }
  4921. while (col <= seg.rightCol) {
  4922. cellMatrix[i][col] = td;
  4923. segMatrix[i][col] = seg;
  4924. col++;
  4925. }
  4926. tr.append(td);
  4927. }
  4928. }
  4929. emptyCellsUntil(colCnt); // finish off the row
  4930. this.bookendCells(tr);
  4931. tbody.append(tr);
  4932. }
  4933. return { // a "rowStruct"
  4934. row: row, // the row number
  4935. tbodyEl: tbody,
  4936. cellMatrix: cellMatrix,
  4937. segMatrix: segMatrix,
  4938. segLevels: segLevels,
  4939. segs: rowSegs
  4940. };
  4941. },
  4942. // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  4943. // NOTE: modifies segs
  4944. buildSegLevels: function(segs) {
  4945. var levels = [];
  4946. var i, seg;
  4947. var j;
  4948. // Give preference to elements with certain criteria, so they have
  4949. // a chance to be closer to the top.
  4950. this.sortEventSegs(segs);
  4951. for (i = 0; i < segs.length; i++) {
  4952. seg = segs[i];
  4953. // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  4954. for (j = 0; j < levels.length; j++) {
  4955. if (!isDaySegCollision(seg, levels[j])) {
  4956. break;
  4957. }
  4958. }
  4959. // `j` now holds the desired subrow index
  4960. seg.level = j;
  4961. // create new level array if needed and append segment
  4962. (levels[j] || (levels[j] = [])).push(seg);
  4963. }
  4964. // order segments left-to-right. very important if calendar is RTL
  4965. for (j = 0; j < levels.length; j++) {
  4966. levels[j].sort(compareDaySegCols);
  4967. }
  4968. return levels;
  4969. },
  4970. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  4971. groupSegRows: function(segs) {
  4972. var segRows = [];
  4973. var i;
  4974. for (i = 0; i < this.rowCnt; i++) {
  4975. segRows.push([]);
  4976. }
  4977. for (i = 0; i < segs.length; i++) {
  4978. segRows[segs[i].row].push(segs[i]);
  4979. }
  4980. return segRows;
  4981. }
  4982. });
  4983. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  4984. function isDaySegCollision(seg, otherSegs) {
  4985. var i, otherSeg;
  4986. for (i = 0; i < otherSegs.length; i++) {
  4987. otherSeg = otherSegs[i];
  4988. if (
  4989. otherSeg.leftCol <= seg.rightCol &&
  4990. otherSeg.rightCol >= seg.leftCol
  4991. ) {
  4992. return true;
  4993. }
  4994. }
  4995. return false;
  4996. }
  4997. // A cmp function for determining the leftmost event
  4998. function compareDaySegCols(a, b) {
  4999. return a.leftCol - b.leftCol;
  5000. }
  5001. ;;
  5002. /* Methods relate to limiting the number events for a given day on a DayGrid
  5003. ----------------------------------------------------------------------------------------------------------------------*/
  5004. // NOTE: all the segs being passed around in here are foreground segs
  5005. DayGrid.mixin({
  5006. segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  5007. popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  5008. removeSegPopover: function() {
  5009. if (this.segPopover) {
  5010. this.segPopover.hide(); // in handler, will call segPopover's removeElement
  5011. }
  5012. },
  5013. // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  5014. // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  5015. limitRows: function(levelLimit) {
  5016. var rowStructs = this.rowStructs || [];
  5017. var row; // row #
  5018. var rowLevelLimit;
  5019. for (row = 0; row < rowStructs.length; row++) {
  5020. this.unlimitRow(row);
  5021. if (!levelLimit) {
  5022. rowLevelLimit = false;
  5023. }
  5024. else if (typeof levelLimit === 'number') {
  5025. rowLevelLimit = levelLimit;
  5026. }
  5027. else {
  5028. rowLevelLimit = this.computeRowLevelLimit(row);
  5029. }
  5030. if (rowLevelLimit !== false) {
  5031. this.limitRow(row, rowLevelLimit);
  5032. }
  5033. }
  5034. },
  5035. // Computes the number of levels a row will accomodate without going outside its bounds.
  5036. // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  5037. // `row` is the row number.
  5038. computeRowLevelLimit: function(row) {
  5039. var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  5040. var rowHeight = rowEl.height(); // TODO: cache somehow?
  5041. var trEls = this.rowStructs[row].tbodyEl.children();
  5042. var i, trEl;
  5043. var trHeight;
  5044. function iterInnerHeights(i, childNode) {
  5045. trHeight = Math.max(trHeight, $(childNode).outerHeight());
  5046. }
  5047. // Reveal one level <tr> at a time and stop when we find one out of bounds
  5048. for (i = 0; i < trEls.length; i++) {
  5049. trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
  5050. // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
  5051. // so instead, find the tallest inner content element.
  5052. trHeight = 0;
  5053. trEl.find('> td > :first-child').each(iterInnerHeights);
  5054. if (trEl.position().top + trHeight > rowHeight) {
  5055. return i;
  5056. }
  5057. }
  5058. return false; // should not limit at all
  5059. },
  5060. // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  5061. // `row` is the row number.
  5062. // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  5063. limitRow: function(row, levelLimit) {
  5064. var _this = this;
  5065. var rowStruct = this.rowStructs[row];
  5066. var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  5067. var col = 0; // col #, left-to-right (not chronologically)
  5068. var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  5069. var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  5070. var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  5071. var i, seg;
  5072. var segsBelow; // array of segment objects below `seg` in the current `col`
  5073. var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  5074. var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  5075. var td, rowspan;
  5076. var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  5077. var j;
  5078. var moreTd, moreWrap, moreLink;
  5079. // Iterates through empty level cells and places "more" links inside if need be
  5080. function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  5081. while (col < endCol) {
  5082. segsBelow = _this.getCellSegs(row, col, levelLimit);
  5083. if (segsBelow.length) {
  5084. td = cellMatrix[levelLimit - 1][col];
  5085. moreLink = _this.renderMoreLink(row, col, segsBelow);
  5086. moreWrap = $('<div/>').append(moreLink);
  5087. td.append(moreWrap);
  5088. moreNodes.push(moreWrap[0]);
  5089. }
  5090. col++;
  5091. }
  5092. }
  5093. if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  5094. levelSegs = rowStruct.segLevels[levelLimit - 1];
  5095. cellMatrix = rowStruct.cellMatrix;
  5096. limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  5097. .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  5098. // iterate though segments in the last allowable level
  5099. for (i = 0; i < levelSegs.length; i++) {
  5100. seg = levelSegs[i];
  5101. emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  5102. // determine *all* segments below `seg` that occupy the same columns
  5103. colSegsBelow = [];
  5104. totalSegsBelow = 0;
  5105. while (col <= seg.rightCol) {
  5106. segsBelow = this.getCellSegs(row, col, levelLimit);
  5107. colSegsBelow.push(segsBelow);
  5108. totalSegsBelow += segsBelow.length;
  5109. col++;
  5110. }
  5111. if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  5112. td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  5113. rowspan = td.attr('rowspan') || 1;
  5114. segMoreNodes = [];
  5115. // make a replacement <td> for each column the segment occupies. will be one for each colspan
  5116. for (j = 0; j < colSegsBelow.length; j++) {
  5117. moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  5118. segsBelow = colSegsBelow[j];
  5119. moreLink = this.renderMoreLink(
  5120. row,
  5121. seg.leftCol + j,
  5122. [ seg ].concat(segsBelow) // count seg as hidden too
  5123. );
  5124. moreWrap = $('<div/>').append(moreLink);
  5125. moreTd.append(moreWrap);
  5126. segMoreNodes.push(moreTd[0]);
  5127. moreNodes.push(moreTd[0]);
  5128. }
  5129. td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  5130. limitedNodes.push(td[0]);
  5131. }
  5132. }
  5133. emptyCellsUntil(this.colCnt); // finish off the level
  5134. rowStruct.moreEls = $(moreNodes); // for easy undoing later
  5135. rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  5136. }
  5137. },
  5138. // Reveals all levels and removes all "more"-related elements for a grid's row.
  5139. // `row` is a row number.
  5140. unlimitRow: function(row) {
  5141. var rowStruct = this.rowStructs[row];
  5142. if (rowStruct.moreEls) {
  5143. rowStruct.moreEls.remove();
  5144. rowStruct.moreEls = null;
  5145. }
  5146. if (rowStruct.limitedEls) {
  5147. rowStruct.limitedEls.removeClass('fc-limited');
  5148. rowStruct.limitedEls = null;
  5149. }
  5150. },
  5151. // Renders an <a> element that represents hidden event element for a cell.
  5152. // Responsible for attaching click handler as well.
  5153. renderMoreLink: function(row, col, hiddenSegs) {
  5154. var _this = this;
  5155. var view = this.view;
  5156. return $('<a class="fc-more"/>')
  5157. .text(
  5158. this.getMoreLinkText(hiddenSegs.length)
  5159. )
  5160. .on('click', function(ev) {
  5161. var clickOption = view.opt('eventLimitClick');
  5162. var date = _this.getCellDate(row, col);
  5163. var moreEl = $(this);
  5164. var dayEl = _this.getCellEl(row, col);
  5165. var allSegs = _this.getCellSegs(row, col);
  5166. // rescope the segments to be within the cell's date
  5167. var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  5168. var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  5169. if (typeof clickOption === 'function') {
  5170. // the returned value can be an atomic option
  5171. clickOption = view.publiclyTrigger('eventLimitClick', null, {
  5172. date: date,
  5173. dayEl: dayEl,
  5174. moreEl: moreEl,
  5175. segs: reslicedAllSegs,
  5176. hiddenSegs: reslicedHiddenSegs
  5177. }, ev);
  5178. }
  5179. if (clickOption === 'popover') {
  5180. _this.showSegPopover(row, col, moreEl, reslicedAllSegs);
  5181. }
  5182. else if (typeof clickOption === 'string') { // a view name
  5183. view.calendar.zoomTo(date, clickOption);
  5184. }
  5185. });
  5186. },
  5187. // Reveals the popover that displays all events within a cell
  5188. showSegPopover: function(row, col, moreLink, segs) {
  5189. var _this = this;
  5190. var view = this.view;
  5191. var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  5192. var topEl; // the element we want to match the top coordinate of
  5193. var options;
  5194. if (this.rowCnt == 1) {
  5195. topEl = view.el; // will cause the popover to cover any sort of header
  5196. }
  5197. else {
  5198. topEl = this.rowEls.eq(row); // will align with top of row
  5199. }
  5200. options = {
  5201. className: 'fc-more-popover',
  5202. content: this.renderSegPopoverContent(row, col, segs),
  5203. parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
  5204. top: topEl.offset().top,
  5205. autoHide: true, // when the user clicks elsewhere, hide the popover
  5206. viewportConstrain: view.opt('popoverViewportConstrain'),
  5207. hide: function() {
  5208. // kill everything when the popover is hidden
  5209. // notify events to be removed
  5210. if (_this.popoverSegs) {
  5211. var seg;
  5212. for (var i = 0; i < _this.popoverSegs.length; ++i) {
  5213. seg = _this.popoverSegs[i];
  5214. view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
  5215. }
  5216. }
  5217. _this.segPopover.removeElement();
  5218. _this.segPopover = null;
  5219. _this.popoverSegs = null;
  5220. }
  5221. };
  5222. // Determine horizontal coordinate.
  5223. // We use the moreWrap instead of the <td> to avoid border confusion.
  5224. if (this.isRTL) {
  5225. options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  5226. }
  5227. else {
  5228. options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  5229. }
  5230. this.segPopover = new Popover(options);
  5231. this.segPopover.show();
  5232. // the popover doesn't live within the grid's container element, and thus won't get the event
  5233. // delegated-handlers for free. attach event-related handlers to the popover.
  5234. this.bindSegHandlersToEl(this.segPopover.el);
  5235. },
  5236. // Builds the inner DOM contents of the segment popover
  5237. renderSegPopoverContent: function(row, col, segs) {
  5238. var view = this.view;
  5239. var isTheme = view.opt('theme');
  5240. var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
  5241. var content = $(
  5242. '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  5243. '<span class="fc-close ' +
  5244. (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  5245. '"></span>' +
  5246. '<span class="fc-title">' +
  5247. htmlEscape(title) +
  5248. '</span>' +
  5249. '<div class="fc-clear"/>' +
  5250. '</div>' +
  5251. '<div class="fc-body ' + view.widgetContentClass + '">' +
  5252. '<div class="fc-event-container"></div>' +
  5253. '</div>'
  5254. );
  5255. var segContainer = content.find('.fc-event-container');
  5256. var i;
  5257. // render each seg's `el` and only return the visible segs
  5258. segs = this.renderFgSegEls(segs, true); // disableResizing=true
  5259. this.popoverSegs = segs;
  5260. for (i = 0; i < segs.length; i++) {
  5261. // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  5262. // grids that want to do drag-n-drop about which cell it came from
  5263. this.prepareHits();
  5264. segs[i].hit = this.getCellHit(row, col);
  5265. this.releaseHits();
  5266. segContainer.append(segs[i].el);
  5267. }
  5268. return content;
  5269. },
  5270. // Given the events within an array of segment objects, reslice them to be in a single day
  5271. resliceDaySegs: function(segs, dayDate) {
  5272. // build an array of the original events
  5273. var events = $.map(segs, function(seg) {
  5274. return seg.event;
  5275. });
  5276. var dayStart = dayDate.clone();
  5277. var dayEnd = dayStart.clone().add(1, 'days');
  5278. var dayRange = { start: dayStart, end: dayEnd };
  5279. // slice the events with a custom slicing function
  5280. segs = this.eventsToSegs(
  5281. events,
  5282. function(range) {
  5283. var seg = intersectRanges(range, dayRange); // undefind if no intersection
  5284. return seg ? [ seg ] : []; // must return an array of segments
  5285. }
  5286. );
  5287. // force an order because eventsToSegs doesn't guarantee one
  5288. this.sortEventSegs(segs);
  5289. return segs;
  5290. },
  5291. // Generates the text that should be inside a "more" link, given the number of events it represents
  5292. getMoreLinkText: function(num) {
  5293. var opt = this.view.opt('eventLimitText');
  5294. if (typeof opt === 'function') {
  5295. return opt(num);
  5296. }
  5297. else {
  5298. return '+' + num + ' ' + opt;
  5299. }
  5300. },
  5301. // Returns segments within a given cell.
  5302. // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  5303. getCellSegs: function(row, col, startLevel) {
  5304. var segMatrix = this.rowStructs[row].segMatrix;
  5305. var level = startLevel || 0;
  5306. var segs = [];
  5307. var seg;
  5308. while (level < segMatrix.length) {
  5309. seg = segMatrix[level][col];
  5310. if (seg) {
  5311. segs.push(seg);
  5312. }
  5313. level++;
  5314. }
  5315. return segs;
  5316. }
  5317. });
  5318. ;;
  5319. /* A component that renders one or more columns of vertical time slots
  5320. ----------------------------------------------------------------------------------------------------------------------*/
  5321. // We mixin DayTable, even though there is only a single row of days
  5322. var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
  5323. slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  5324. snapDuration: null, // granularity of time for dragging and selecting
  5325. snapsPerSlot: null,
  5326. minTime: null, // Duration object that denotes the first visible time of any given day
  5327. maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  5328. labelFormat: null, // formatting string for times running along vertical axis
  5329. labelInterval: null, // duration of how often a label should be displayed for a slot
  5330. colEls: null, // cells elements in the day-row background
  5331. slatContainerEl: null, // div that wraps all the slat rows
  5332. slatEls: null, // elements running horizontally across all columns
  5333. nowIndicatorEls: null,
  5334. colCoordCache: null,
  5335. slatCoordCache: null,
  5336. constructor: function() {
  5337. Grid.apply(this, arguments); // call the super-constructor
  5338. this.processOptions();
  5339. },
  5340. // Renders the time grid into `this.el`, which should already be assigned.
  5341. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  5342. renderDates: function() {
  5343. this.el.html(this.renderHtml());
  5344. this.colEls = this.el.find('.fc-day');
  5345. this.slatContainerEl = this.el.find('.fc-slats');
  5346. this.slatEls = this.slatContainerEl.find('tr');
  5347. this.colCoordCache = new CoordCache({
  5348. els: this.colEls,
  5349. isHorizontal: true
  5350. });
  5351. this.slatCoordCache = new CoordCache({
  5352. els: this.slatEls,
  5353. isVertical: true
  5354. });
  5355. this.renderContentSkeleton();
  5356. },
  5357. // Renders the basic HTML skeleton for the grid
  5358. renderHtml: function() {
  5359. return '' +
  5360. '<div class="fc-bg">' +
  5361. '<table>' +
  5362. this.renderBgTrHtml(0) + // row=0
  5363. '</table>' +
  5364. '</div>' +
  5365. '<div class="fc-slats">' +
  5366. '<table>' +
  5367. this.renderSlatRowHtml() +
  5368. '</table>' +
  5369. '</div>';
  5370. },
  5371. // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  5372. renderSlatRowHtml: function() {
  5373. var view = this.view;
  5374. var isRTL = this.isRTL;
  5375. var html = '';
  5376. var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  5377. var slotDate; // will be on the view's first day, but we only care about its time
  5378. var isLabeled;
  5379. var axisHtml;
  5380. // Calculate the time for each slot
  5381. while (slotTime < this.maxTime) {
  5382. slotDate = this.start.clone().time(slotTime);
  5383. isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
  5384. axisHtml =
  5385. '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  5386. (isLabeled ?
  5387. '<span>' + // for matchCellWidths
  5388. htmlEscape(slotDate.format(this.labelFormat)) +
  5389. '</span>' :
  5390. ''
  5391. ) +
  5392. '</td>';
  5393. html +=
  5394. '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
  5395. (isLabeled ? '' : ' class="fc-minor"') +
  5396. '>' +
  5397. (!isRTL ? axisHtml : '') +
  5398. '<td class="' + view.widgetContentClass + '"/>' +
  5399. (isRTL ? axisHtml : '') +
  5400. "</tr>";
  5401. slotTime.add(this.slotDuration);
  5402. }
  5403. return html;
  5404. },
  5405. /* Options
  5406. ------------------------------------------------------------------------------------------------------------------*/
  5407. // Parses various options into properties of this object
  5408. processOptions: function() {
  5409. var view = this.view;
  5410. var slotDuration = view.opt('slotDuration');
  5411. var snapDuration = view.opt('snapDuration');
  5412. var input;
  5413. slotDuration = moment.duration(slotDuration);
  5414. snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  5415. this.slotDuration = slotDuration;
  5416. this.snapDuration = snapDuration;
  5417. this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
  5418. this.minResizeDuration = snapDuration; // hack
  5419. this.minTime = moment.duration(view.opt('minTime'));
  5420. this.maxTime = moment.duration(view.opt('maxTime'));
  5421. // might be an array value (for TimelineView).
  5422. // if so, getting the most granular entry (the last one probably).
  5423. input = view.opt('slotLabelFormat');
  5424. if ($.isArray(input)) {
  5425. input = input[input.length - 1];
  5426. }
  5427. this.labelFormat =
  5428. input ||
  5429. view.opt('smallTimeFormat'); // the computed default
  5430. input = view.opt('slotLabelInterval');
  5431. this.labelInterval = input ?
  5432. moment.duration(input) :
  5433. this.computeLabelInterval(slotDuration);
  5434. },
  5435. // Computes an automatic value for slotLabelInterval
  5436. computeLabelInterval: function(slotDuration) {
  5437. var i;
  5438. var labelInterval;
  5439. var slotsPerLabel;
  5440. // find the smallest stock label interval that results in more than one slots-per-label
  5441. for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
  5442. labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
  5443. slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration);
  5444. if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
  5445. return labelInterval;
  5446. }
  5447. }
  5448. return moment.duration(slotDuration); // fall back. clone
  5449. },
  5450. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  5451. computeEventTimeFormat: function() {
  5452. return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
  5453. },
  5454. // Computes a default `displayEventEnd` value if one is not expliclty defined
  5455. computeDisplayEventEnd: function() {
  5456. return true;
  5457. },
  5458. /* Hit System
  5459. ------------------------------------------------------------------------------------------------------------------*/
  5460. prepareHits: function() {
  5461. this.colCoordCache.build();
  5462. this.slatCoordCache.build();
  5463. },
  5464. releaseHits: function() {
  5465. this.colCoordCache.clear();
  5466. // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
  5467. },
  5468. queryHit: function(leftOffset, topOffset) {
  5469. var snapsPerSlot = this.snapsPerSlot;
  5470. var colCoordCache = this.colCoordCache;
  5471. var slatCoordCache = this.slatCoordCache;
  5472. if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
  5473. var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
  5474. var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
  5475. if (colIndex != null && slatIndex != null) {
  5476. var slatTop = slatCoordCache.getTopOffset(slatIndex);
  5477. var slatHeight = slatCoordCache.getHeight(slatIndex);
  5478. var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
  5479. var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
  5480. var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
  5481. var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
  5482. var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
  5483. return {
  5484. col: colIndex,
  5485. snap: snapIndex,
  5486. component: this, // needed unfortunately :(
  5487. left: colCoordCache.getLeftOffset(colIndex),
  5488. right: colCoordCache.getRightOffset(colIndex),
  5489. top: snapTop,
  5490. bottom: snapBottom
  5491. };
  5492. }
  5493. }
  5494. },
  5495. getHitSpan: function(hit) {
  5496. var start = this.getCellDate(0, hit.col); // row=0
  5497. var time = this.computeSnapTime(hit.snap); // pass in the snap-index
  5498. var end;
  5499. start.time(time);
  5500. end = start.clone().add(this.snapDuration);
  5501. return { start: start, end: end };
  5502. },
  5503. getHitEl: function(hit) {
  5504. return this.colEls.eq(hit.col);
  5505. },
  5506. /* Dates
  5507. ------------------------------------------------------------------------------------------------------------------*/
  5508. rangeUpdated: function() {
  5509. this.updateDayTable();
  5510. },
  5511. // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
  5512. computeSnapTime: function(snapIndex) {
  5513. return moment.duration(this.minTime + this.snapDuration * snapIndex);
  5514. },
  5515. // Slices up the given span (unzoned start/end with other misc data) into an array of segments
  5516. spanToSegs: function(span) {
  5517. var segs = this.sliceRangeByTimes(span);
  5518. var i;
  5519. for (i = 0; i < segs.length; i++) {
  5520. if (this.isRTL) {
  5521. segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
  5522. }
  5523. else {
  5524. segs[i].col = segs[i].dayIndex;
  5525. }
  5526. }
  5527. return segs;
  5528. },
  5529. sliceRangeByTimes: function(range) {
  5530. var segs = [];
  5531. var seg;
  5532. var dayIndex;
  5533. var dayDate;
  5534. var dayRange;
  5535. for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
  5536. dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this?
  5537. dayRange = {
  5538. start: dayDate.clone().time(this.minTime),
  5539. end: dayDate.clone().time(this.maxTime)
  5540. };
  5541. seg = intersectRanges(range, dayRange); // both will be ambig timezone
  5542. if (seg) {
  5543. seg.dayIndex = dayIndex;
  5544. segs.push(seg);
  5545. }
  5546. }
  5547. return segs;
  5548. },
  5549. /* Coordinates
  5550. ------------------------------------------------------------------------------------------------------------------*/
  5551. updateSize: function(isResize) { // NOT a standard Grid method
  5552. this.slatCoordCache.build();
  5553. if (isResize) {
  5554. this.updateSegVerticals(
  5555. [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || [])
  5556. );
  5557. }
  5558. },
  5559. getTotalSlatHeight: function() {
  5560. return this.slatContainerEl.outerHeight();
  5561. },
  5562. // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  5563. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  5564. computeDateTop: function(date, startOfDayDate) {
  5565. return this.computeTimeTop(
  5566. moment.duration(
  5567. date - startOfDayDate.clone().stripTime()
  5568. )
  5569. );
  5570. },
  5571. // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  5572. computeTimeTop: function(time) {
  5573. var len = this.slatEls.length;
  5574. var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  5575. var slatIndex;
  5576. var slatRemainder;
  5577. // compute a floating-point number for how many slats should be progressed through.
  5578. // from 0 to number of slats (inclusive)
  5579. // constrained because minTime/maxTime might be customized.
  5580. slatCoverage = Math.max(0, slatCoverage);
  5581. slatCoverage = Math.min(len, slatCoverage);
  5582. // an integer index of the furthest whole slat
  5583. // from 0 to number slats (*exclusive*, so len-1)
  5584. slatIndex = Math.floor(slatCoverage);
  5585. slatIndex = Math.min(slatIndex, len - 1);
  5586. // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
  5587. // could be 1.0 if slatCoverage is covering *all* the slots
  5588. slatRemainder = slatCoverage - slatIndex;
  5589. return this.slatCoordCache.getTopPosition(slatIndex) +
  5590. this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
  5591. },
  5592. /* Event Drag Visualization
  5593. ------------------------------------------------------------------------------------------------------------------*/
  5594. // Renders a visual indication of an event being dragged over the specified date(s).
  5595. // A returned value of `true` signals that a mock "helper" event has been rendered.
  5596. renderDrag: function(eventLocation, seg) {
  5597. if (seg) { // if there is event information for this drag, render a helper event
  5598. // returns mock event elements
  5599. // signal that a helper has been rendered
  5600. return this.renderEventLocationHelper(eventLocation, seg);
  5601. }
  5602. else {
  5603. // otherwise, just render a highlight
  5604. this.renderHighlight(this.eventToSpan(eventLocation));
  5605. }
  5606. },
  5607. // Unrenders any visual indication of an event being dragged
  5608. unrenderDrag: function() {
  5609. this.unrenderHelper();
  5610. this.unrenderHighlight();
  5611. },
  5612. /* Event Resize Visualization
  5613. ------------------------------------------------------------------------------------------------------------------*/
  5614. // Renders a visual indication of an event being resized
  5615. renderEventResize: function(eventLocation, seg) {
  5616. return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
  5617. },
  5618. // Unrenders any visual indication of an event being resized
  5619. unrenderEventResize: function() {
  5620. this.unrenderHelper();
  5621. },
  5622. /* Event Helper
  5623. ------------------------------------------------------------------------------------------------------------------*/
  5624. // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  5625. renderHelper: function(event, sourceSeg) {
  5626. return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
  5627. },
  5628. // Unrenders any mock helper event
  5629. unrenderHelper: function() {
  5630. this.unrenderHelperSegs();
  5631. },
  5632. /* Business Hours
  5633. ------------------------------------------------------------------------------------------------------------------*/
  5634. renderBusinessHours: function() {
  5635. this.renderBusinessSegs(
  5636. this.buildBusinessHourSegs()
  5637. );
  5638. },
  5639. unrenderBusinessHours: function() {
  5640. this.unrenderBusinessSegs();
  5641. },
  5642. /* Now Indicator
  5643. ------------------------------------------------------------------------------------------------------------------*/
  5644. getNowIndicatorUnit: function() {
  5645. return 'minute'; // will refresh on the minute
  5646. },
  5647. renderNowIndicator: function(date) {
  5648. // seg system might be overkill, but it handles scenario where line needs to be rendered
  5649. // more than once because of columns with the same date (resources columns for example)
  5650. var segs = this.spanToSegs({ start: date, end: date });
  5651. var top = this.computeDateTop(date, date);
  5652. var nodes = [];
  5653. var i;
  5654. // render lines within the columns
  5655. for (i = 0; i < segs.length; i++) {
  5656. nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
  5657. .css('top', top)
  5658. .appendTo(this.colContainerEls.eq(segs[i].col))[0]);
  5659. }
  5660. // render an arrow over the axis
  5661. if (segs.length > 0) { // is the current time in view?
  5662. nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
  5663. .css('top', top)
  5664. .appendTo(this.el.find('.fc-content-skeleton'))[0]);
  5665. }
  5666. this.nowIndicatorEls = $(nodes);
  5667. },
  5668. unrenderNowIndicator: function() {
  5669. if (this.nowIndicatorEls) {
  5670. this.nowIndicatorEls.remove();
  5671. this.nowIndicatorEls = null;
  5672. }
  5673. },
  5674. /* Selection
  5675. ------------------------------------------------------------------------------------------------------------------*/
  5676. // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  5677. renderSelection: function(span) {
  5678. if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  5679. // normally acceps an eventLocation, span has a start/end, which is good enough
  5680. this.renderEventLocationHelper(span);
  5681. }
  5682. else {
  5683. this.renderHighlight(span);
  5684. }
  5685. },
  5686. // Unrenders any visual indication of a selection
  5687. unrenderSelection: function() {
  5688. this.unrenderHelper();
  5689. this.unrenderHighlight();
  5690. },
  5691. /* Highlight
  5692. ------------------------------------------------------------------------------------------------------------------*/
  5693. renderHighlight: function(span) {
  5694. this.renderHighlightSegs(this.spanToSegs(span));
  5695. },
  5696. unrenderHighlight: function() {
  5697. this.unrenderHighlightSegs();
  5698. }
  5699. });
  5700. ;;
  5701. /* Methods for rendering SEGMENTS, pieces of content that live on the view
  5702. ( this file is no longer just for events )
  5703. ----------------------------------------------------------------------------------------------------------------------*/
  5704. TimeGrid.mixin({
  5705. colContainerEls: null, // containers for each column
  5706. // inner-containers for each column where different types of segs live
  5707. fgContainerEls: null,
  5708. bgContainerEls: null,
  5709. helperContainerEls: null,
  5710. highlightContainerEls: null,
  5711. businessContainerEls: null,
  5712. // arrays of different types of displayed segments
  5713. fgSegs: null,
  5714. bgSegs: null,
  5715. helperSegs: null,
  5716. highlightSegs: null,
  5717. businessSegs: null,
  5718. // Renders the DOM that the view's content will live in
  5719. renderContentSkeleton: function() {
  5720. var cellHtml = '';
  5721. var i;
  5722. var skeletonEl;
  5723. for (i = 0; i < this.colCnt; i++) {
  5724. cellHtml +=
  5725. '<td>' +
  5726. '<div class="fc-content-col">' +
  5727. '<div class="fc-event-container fc-helper-container"></div>' +
  5728. '<div class="fc-event-container"></div>' +
  5729. '<div class="fc-highlight-container"></div>' +
  5730. '<div class="fc-bgevent-container"></div>' +
  5731. '<div class="fc-business-container"></div>' +
  5732. '</div>' +
  5733. '</td>';
  5734. }
  5735. skeletonEl = $(
  5736. '<div class="fc-content-skeleton">' +
  5737. '<table>' +
  5738. '<tr>' + cellHtml + '</tr>' +
  5739. '</table>' +
  5740. '</div>'
  5741. );
  5742. this.colContainerEls = skeletonEl.find('.fc-content-col');
  5743. this.helperContainerEls = skeletonEl.find('.fc-helper-container');
  5744. this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
  5745. this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
  5746. this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
  5747. this.businessContainerEls = skeletonEl.find('.fc-business-container');
  5748. this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
  5749. this.el.append(skeletonEl);
  5750. },
  5751. /* Foreground Events
  5752. ------------------------------------------------------------------------------------------------------------------*/
  5753. renderFgSegs: function(segs) {
  5754. segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls);
  5755. this.fgSegs = segs;
  5756. return segs; // needed for Grid::renderEvents
  5757. },
  5758. unrenderFgSegs: function() {
  5759. this.unrenderNamedSegs('fgSegs');
  5760. },
  5761. /* Foreground Helper Events
  5762. ------------------------------------------------------------------------------------------------------------------*/
  5763. renderHelperSegs: function(segs, sourceSeg) {
  5764. var helperEls = [];
  5765. var i, seg;
  5766. var sourceEl;
  5767. segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls);
  5768. // Try to make the segment that is in the same row as sourceSeg look the same
  5769. for (i = 0; i < segs.length; i++) {
  5770. seg = segs[i];
  5771. if (sourceSeg && sourceSeg.col === seg.col) {
  5772. sourceEl = sourceSeg.el;
  5773. seg.el.css({
  5774. left: sourceEl.css('left'),
  5775. right: sourceEl.css('right'),
  5776. 'margin-left': sourceEl.css('margin-left'),
  5777. 'margin-right': sourceEl.css('margin-right')
  5778. });
  5779. }
  5780. helperEls.push(seg.el[0]);
  5781. }
  5782. this.helperSegs = segs;
  5783. return $(helperEls); // must return rendered helpers
  5784. },
  5785. unrenderHelperSegs: function() {
  5786. this.unrenderNamedSegs('helperSegs');
  5787. },
  5788. /* Background Events
  5789. ------------------------------------------------------------------------------------------------------------------*/
  5790. renderBgSegs: function(segs) {
  5791. segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system
  5792. this.updateSegVerticals(segs);
  5793. this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
  5794. this.bgSegs = segs;
  5795. return segs; // needed for Grid::renderEvents
  5796. },
  5797. unrenderBgSegs: function() {
  5798. this.unrenderNamedSegs('bgSegs');
  5799. },
  5800. /* Highlight
  5801. ------------------------------------------------------------------------------------------------------------------*/
  5802. renderHighlightSegs: function(segs) {
  5803. segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system
  5804. this.updateSegVerticals(segs);
  5805. this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls);
  5806. this.highlightSegs = segs;
  5807. },
  5808. unrenderHighlightSegs: function() {
  5809. this.unrenderNamedSegs('highlightSegs');
  5810. },
  5811. /* Business Hours
  5812. ------------------------------------------------------------------------------------------------------------------*/
  5813. renderBusinessSegs: function(segs) {
  5814. segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system
  5815. this.updateSegVerticals(segs);
  5816. this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls);
  5817. this.businessSegs = segs;
  5818. },
  5819. unrenderBusinessSegs: function() {
  5820. this.unrenderNamedSegs('businessSegs');
  5821. },
  5822. /* Seg Rendering Utils
  5823. ------------------------------------------------------------------------------------------------------------------*/
  5824. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  5825. groupSegsByCol: function(segs) {
  5826. var segsByCol = [];
  5827. var i;
  5828. for (i = 0; i < this.colCnt; i++) {
  5829. segsByCol.push([]);
  5830. }
  5831. for (i = 0; i < segs.length; i++) {
  5832. segsByCol[segs[i].col].push(segs[i]);
  5833. }
  5834. return segsByCol;
  5835. },
  5836. // Given segments grouped by column, insert the segments' elements into a parallel array of container
  5837. // elements, each living within a column.
  5838. attachSegsByCol: function(segsByCol, containerEls) {
  5839. var col;
  5840. var segs;
  5841. var i;
  5842. for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
  5843. segs = segsByCol[col];
  5844. for (i = 0; i < segs.length; i++) {
  5845. containerEls.eq(col).append(segs[i].el);
  5846. }
  5847. }
  5848. },
  5849. // Given the name of a property of `this` object, assumed to be an array of segments,
  5850. // loops through each segment and removes from DOM. Will null-out the property afterwards.
  5851. unrenderNamedSegs: function(propName) {
  5852. var segs = this[propName];
  5853. var i;
  5854. if (segs) {
  5855. for (i = 0; i < segs.length; i++) {
  5856. segs[i].el.remove();
  5857. }
  5858. this[propName] = null;
  5859. }
  5860. },
  5861. /* Foreground Event Rendering Utils
  5862. ------------------------------------------------------------------------------------------------------------------*/
  5863. // Given an array of foreground segments, render a DOM element for each, computes position,
  5864. // and attaches to the column inner-container elements.
  5865. renderFgSegsIntoContainers: function(segs, containerEls) {
  5866. var segsByCol;
  5867. var col;
  5868. segs = this.renderFgSegEls(segs); // will call fgSegHtml
  5869. segsByCol = this.groupSegsByCol(segs);
  5870. for (col = 0; col < this.colCnt; col++) {
  5871. this.updateFgSegCoords(segsByCol[col]);
  5872. }
  5873. this.attachSegsByCol(segsByCol, containerEls);
  5874. return segs;
  5875. },
  5876. // Renders the HTML for a single event segment's default rendering
  5877. fgSegHtml: function(seg, disableResizing) {
  5878. var view = this.view;
  5879. var event = seg.event;
  5880. var isDraggable = view.isEventDraggable(event);
  5881. var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
  5882. var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
  5883. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  5884. var skinCss = cssToStr(this.getSegSkinCss(seg));
  5885. var timeText;
  5886. var fullTimeText; // more verbose time text. for the print stylesheet
  5887. var startTimeText; // just the start time text
  5888. classes.unshift('fc-time-grid-event', 'fc-v-event');
  5889. if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  5890. // Don't display time text on segments that run entirely through a day.
  5891. // That would appear as midnight-midnight and would look dumb.
  5892. // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  5893. if (seg.isStart || seg.isEnd) {
  5894. timeText = this.getEventTimeText(seg);
  5895. fullTimeText = this.getEventTimeText(seg, 'LT');
  5896. startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
  5897. }
  5898. } else {
  5899. // Display the normal time text for the *event's* times
  5900. timeText = this.getEventTimeText(event);
  5901. fullTimeText = this.getEventTimeText(event, 'LT');
  5902. startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
  5903. }
  5904. return '<a class="' + classes.join(' ') + '"' +
  5905. (event.url ?
  5906. ' href="' + htmlEscape(event.url) + '"' :
  5907. ''
  5908. ) +
  5909. (skinCss ?
  5910. ' style="' + skinCss + '"' :
  5911. ''
  5912. ) +
  5913. '>' +
  5914. '<div class="fc-content">' +
  5915. (timeText ?
  5916. '<div class="fc-time"' +
  5917. ' data-start="' + htmlEscape(startTimeText) + '"' +
  5918. ' data-full="' + htmlEscape(fullTimeText) + '"' +
  5919. '>' +
  5920. '<span>' + htmlEscape(timeText) + '</span>' +
  5921. '</div>' :
  5922. ''
  5923. ) +
  5924. (event.title ?
  5925. '<div class="fc-title">' +
  5926. htmlEscape(event.title) +
  5927. '</div>' :
  5928. ''
  5929. ) +
  5930. '</div>' +
  5931. '<div class="fc-bg"/>' +
  5932. /* TODO: write CSS for this
  5933. (isResizableFromStart ?
  5934. '<div class="fc-resizer fc-start-resizer" />' :
  5935. ''
  5936. ) +
  5937. */
  5938. (isResizableFromEnd ?
  5939. '<div class="fc-resizer fc-end-resizer" />' :
  5940. ''
  5941. ) +
  5942. '</a>';
  5943. },
  5944. /* Seg Position Utils
  5945. ------------------------------------------------------------------------------------------------------------------*/
  5946. // Refreshes the CSS top/bottom coordinates for each segment element.
  5947. // Works when called after initial render, after a window resize/zoom for example.
  5948. updateSegVerticals: function(segs) {
  5949. this.computeSegVerticals(segs);
  5950. this.assignSegVerticals(segs);
  5951. },
  5952. // For each segment in an array, computes and assigns its top and bottom properties
  5953. computeSegVerticals: function(segs) {
  5954. var i, seg;
  5955. for (i = 0; i < segs.length; i++) {
  5956. seg = segs[i];
  5957. seg.top = this.computeDateTop(seg.start, seg.start);
  5958. seg.bottom = this.computeDateTop(seg.end, seg.start);
  5959. }
  5960. },
  5961. // Given segments that already have their top/bottom properties computed, applies those values to
  5962. // the segments' elements.
  5963. assignSegVerticals: function(segs) {
  5964. var i, seg;
  5965. for (i = 0; i < segs.length; i++) {
  5966. seg = segs[i];
  5967. seg.el.css(this.generateSegVerticalCss(seg));
  5968. }
  5969. },
  5970. // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  5971. generateSegVerticalCss: function(seg) {
  5972. return {
  5973. top: seg.top,
  5974. bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  5975. };
  5976. },
  5977. /* Foreground Event Positioning Utils
  5978. ------------------------------------------------------------------------------------------------------------------*/
  5979. // Given segments that are assumed to all live in the *same column*,
  5980. // compute their verical/horizontal coordinates and assign to their elements.
  5981. updateFgSegCoords: function(segs) {
  5982. this.computeSegVerticals(segs); // horizontals relies on this
  5983. this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
  5984. this.assignSegVerticals(segs);
  5985. this.assignFgSegHorizontals(segs);
  5986. },
  5987. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  5988. // NOTE: Also reorders the given array by date!
  5989. computeFgSegHorizontals: function(segs) {
  5990. var levels;
  5991. var level0;
  5992. var i;
  5993. this.sortEventSegs(segs); // order by certain criteria
  5994. levels = buildSlotSegLevels(segs);
  5995. computeForwardSlotSegs(levels);
  5996. if ((level0 = levels[0])) {
  5997. for (i = 0; i < level0.length; i++) {
  5998. computeSlotSegPressures(level0[i]);
  5999. }
  6000. for (i = 0; i < level0.length; i++) {
  6001. this.computeFgSegForwardBack(level0[i], 0, 0);
  6002. }
  6003. }
  6004. },
  6005. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  6006. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  6007. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  6008. //
  6009. // The segment might be part of a "series", which means consecutive segments with the same pressure
  6010. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  6011. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  6012. // coordinate of the first segment in the series.
  6013. computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
  6014. var forwardSegs = seg.forwardSegs;
  6015. var i;
  6016. if (seg.forwardCoord === undefined) { // not already computed
  6017. if (!forwardSegs.length) {
  6018. // if there are no forward segments, this segment should butt up against the edge
  6019. seg.forwardCoord = 1;
  6020. }
  6021. else {
  6022. // sort highest pressure first
  6023. this.sortForwardSegs(forwardSegs);
  6024. // this segment's forwardCoord will be calculated from the backwardCoord of the
  6025. // highest-pressure forward segment.
  6026. this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  6027. seg.forwardCoord = forwardSegs[0].backwardCoord;
  6028. }
  6029. // calculate the backwardCoord from the forwardCoord. consider the series
  6030. seg.backwardCoord = seg.forwardCoord -
  6031. (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  6032. (seriesBackwardPressure + 1); // # of segments in the series
  6033. // use this segment's coordinates to computed the coordinates of the less-pressurized
  6034. // forward segments
  6035. for (i=0; i<forwardSegs.length; i++) {
  6036. this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
  6037. }
  6038. }
  6039. },
  6040. sortForwardSegs: function(forwardSegs) {
  6041. forwardSegs.sort(proxy(this, 'compareForwardSegs'));
  6042. },
  6043. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  6044. compareForwardSegs: function(seg1, seg2) {
  6045. // put higher-pressure first
  6046. return seg2.forwardPressure - seg1.forwardPressure ||
  6047. // put segments that are closer to initial edge first (and favor ones with no coords yet)
  6048. (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  6049. // do normal sorting...
  6050. this.compareEventSegs(seg1, seg2);
  6051. },
  6052. // Given foreground event segments that have already had their position coordinates computed,
  6053. // assigns position-related CSS values to their elements.
  6054. assignFgSegHorizontals: function(segs) {
  6055. var i, seg;
  6056. for (i = 0; i < segs.length; i++) {
  6057. seg = segs[i];
  6058. seg.el.css(this.generateFgSegHorizontalCss(seg));
  6059. // if the height is short, add a className for alternate styling
  6060. if (seg.bottom - seg.top < 30) {
  6061. seg.el.addClass('fc-short');
  6062. }
  6063. }
  6064. },
  6065. // Generates an object with CSS properties/values that should be applied to an event segment element.
  6066. // Contains important positioning-related properties that should be applied to any event element, customized or not.
  6067. generateFgSegHorizontalCss: function(seg) {
  6068. var shouldOverlap = this.view.opt('slotEventOverlap');
  6069. var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  6070. var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  6071. var props = this.generateSegVerticalCss(seg); // get top/bottom first
  6072. var left; // amount of space from left edge, a fraction of the total width
  6073. var right; // amount of space from right edge, a fraction of the total width
  6074. if (shouldOverlap) {
  6075. // double the width, but don't go beyond the maximum forward coordinate (1.0)
  6076. forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  6077. }
  6078. if (this.isRTL) {
  6079. left = 1 - forwardCoord;
  6080. right = backwardCoord;
  6081. }
  6082. else {
  6083. left = backwardCoord;
  6084. right = 1 - forwardCoord;
  6085. }
  6086. props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  6087. props.left = left * 100 + '%';
  6088. props.right = right * 100 + '%';
  6089. if (shouldOverlap && seg.forwardPressure) {
  6090. // add padding to the edge so that forward stacked events don't cover the resizer's icon
  6091. props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  6092. }
  6093. return props;
  6094. }
  6095. });
  6096. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  6097. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  6098. function buildSlotSegLevels(segs) {
  6099. var levels = [];
  6100. var i, seg;
  6101. var j;
  6102. for (i=0; i<segs.length; i++) {
  6103. seg = segs[i];
  6104. // go through all the levels and stop on the first level where there are no collisions
  6105. for (j=0; j<levels.length; j++) {
  6106. if (!computeSlotSegCollisions(seg, levels[j]).length) {
  6107. break;
  6108. }
  6109. }
  6110. seg.level = j;
  6111. (levels[j] || (levels[j] = [])).push(seg);
  6112. }
  6113. return levels;
  6114. }
  6115. // For every segment, figure out the other segments that are in subsequent
  6116. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  6117. function computeForwardSlotSegs(levels) {
  6118. var i, level;
  6119. var j, seg;
  6120. var k;
  6121. for (i=0; i<levels.length; i++) {
  6122. level = levels[i];
  6123. for (j=0; j<level.length; j++) {
  6124. seg = level[j];
  6125. seg.forwardSegs = [];
  6126. for (k=i+1; k<levels.length; k++) {
  6127. computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  6128. }
  6129. }
  6130. }
  6131. }
  6132. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  6133. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  6134. function computeSlotSegPressures(seg) {
  6135. var forwardSegs = seg.forwardSegs;
  6136. var forwardPressure = 0;
  6137. var i, forwardSeg;
  6138. if (seg.forwardPressure === undefined) { // not already computed
  6139. for (i=0; i<forwardSegs.length; i++) {
  6140. forwardSeg = forwardSegs[i];
  6141. // figure out the child's maximum forward path
  6142. computeSlotSegPressures(forwardSeg);
  6143. // either use the existing maximum, or use the child's forward pressure
  6144. // plus one (for the forwardSeg itself)
  6145. forwardPressure = Math.max(
  6146. forwardPressure,
  6147. 1 + forwardSeg.forwardPressure
  6148. );
  6149. }
  6150. seg.forwardPressure = forwardPressure;
  6151. }
  6152. }
  6153. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  6154. // Append into an optionally-supplied `results` array and return.
  6155. function computeSlotSegCollisions(seg, otherSegs, results) {
  6156. results = results || [];
  6157. for (var i=0; i<otherSegs.length; i++) {
  6158. if (isSlotSegCollision(seg, otherSegs[i])) {
  6159. results.push(otherSegs[i]);
  6160. }
  6161. }
  6162. return results;
  6163. }
  6164. // Do these segments occupy the same vertical space?
  6165. function isSlotSegCollision(seg1, seg2) {
  6166. return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  6167. }
  6168. ;;
  6169. /* An abstract class from which other views inherit from
  6170. ----------------------------------------------------------------------------------------------------------------------*/
  6171. var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
  6172. type: null, // subclass' view name (string)
  6173. name: null, // deprecated. use `type` instead
  6174. title: null, // the text that will be displayed in the header's title
  6175. calendar: null, // owner Calendar object
  6176. options: null, // hash containing all options. already merged with view-specific-options
  6177. el: null, // the view's containing element. set by Calendar
  6178. isDateSet: false,
  6179. isDateRendered: false,
  6180. dateRenderQueue: null,
  6181. isEventsBound: false,
  6182. isEventsSet: false,
  6183. isEventsRendered: false,
  6184. eventRenderQueue: null,
  6185. // range the view is actually displaying (moments)
  6186. start: null,
  6187. end: null, // exclusive
  6188. // range the view is formally responsible for (moments)
  6189. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  6190. intervalStart: null,
  6191. intervalEnd: null, // exclusive
  6192. intervalDuration: null,
  6193. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  6194. isRTL: false,
  6195. isSelected: false, // boolean whether a range of time is user-selected or not
  6196. selectedEvent: null,
  6197. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  6198. // classNames styled by jqui themes
  6199. widgetHeaderClass: null,
  6200. widgetContentClass: null,
  6201. highlightStateClass: null,
  6202. // for date utils, computed from options
  6203. nextDayThreshold: null,
  6204. isHiddenDayHash: null,
  6205. // now indicator
  6206. isNowIndicatorRendered: null,
  6207. initialNowDate: null, // result first getNow call
  6208. initialNowQueriedMs: null, // ms time the getNow was called
  6209. nowIndicatorTimeoutID: null, // for refresh timing of now indicator
  6210. nowIndicatorIntervalID: null, // "
  6211. constructor: function(calendar, type, options, intervalDuration) {
  6212. this.calendar = calendar;
  6213. this.type = this.name = type; // .name is deprecated
  6214. this.options = options;
  6215. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  6216. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  6217. this.initThemingProps();
  6218. this.initHiddenDays();
  6219. this.isRTL = this.opt('isRTL');
  6220. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  6221. this.dateRenderQueue = new TaskQueue();
  6222. this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
  6223. this.initialize();
  6224. },
  6225. // A good place for subclasses to initialize member variables
  6226. initialize: function() {
  6227. // subclasses can implement
  6228. },
  6229. // Retrieves an option with the given name
  6230. opt: function(name) {
  6231. return this.options[name];
  6232. },
  6233. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  6234. publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  6235. var calendar = this.calendar;
  6236. return calendar.publiclyTrigger.apply(
  6237. calendar,
  6238. [name, thisObj || this].concat(
  6239. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  6240. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  6241. )
  6242. );
  6243. },
  6244. // Returns a proxy of the given promise that will be rejected if the given event fires
  6245. // before the promise resolves.
  6246. rejectOn: function(eventName, promise) {
  6247. var _this = this;
  6248. return new Promise(function(resolve, reject) {
  6249. _this.one(eventName, reject);
  6250. function cleanup() {
  6251. _this.off(eventName, reject);
  6252. }
  6253. promise.then(function(res) { // success
  6254. cleanup();
  6255. resolve(res);
  6256. }, function() { // failure
  6257. cleanup();
  6258. reject();
  6259. });
  6260. });
  6261. },
  6262. /* Date Computation
  6263. ------------------------------------------------------------------------------------------------------------------*/
  6264. // Updates all internal dates for displaying the given unzoned range.
  6265. setRange: function(range) {
  6266. $.extend(this, range); // assigns every property to this object's member variables
  6267. this.updateTitle();
  6268. },
  6269. // Given a single current unzoned date, produce information about what range to display.
  6270. // Subclasses can override. Must return all properties.
  6271. computeRange: function(date) {
  6272. var intervalUnit = computeIntervalUnit(this.intervalDuration);
  6273. var intervalStart = date.clone().startOf(intervalUnit);
  6274. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  6275. var start, end;
  6276. // normalize the range's time-ambiguity
  6277. if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  6278. intervalStart.stripTime();
  6279. intervalEnd.stripTime();
  6280. }
  6281. else { // needs to have a time?
  6282. if (!intervalStart.hasTime()) {
  6283. intervalStart = this.calendar.time(0); // give 00:00 time
  6284. }
  6285. if (!intervalEnd.hasTime()) {
  6286. intervalEnd = this.calendar.time(0); // give 00:00 time
  6287. }
  6288. }
  6289. start = intervalStart.clone();
  6290. start = this.skipHiddenDays(start);
  6291. end = intervalEnd.clone();
  6292. end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  6293. return {
  6294. intervalUnit: intervalUnit,
  6295. intervalStart: intervalStart,
  6296. intervalEnd: intervalEnd,
  6297. start: start,
  6298. end: end
  6299. };
  6300. },
  6301. // Computes the new date when the user hits the prev button, given the current date
  6302. computePrevDate: function(date) {
  6303. return this.massageCurrentDate(
  6304. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  6305. );
  6306. },
  6307. // Computes the new date when the user hits the next button, given the current date
  6308. computeNextDate: function(date) {
  6309. return this.massageCurrentDate(
  6310. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  6311. );
  6312. },
  6313. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  6314. // visible. `direction` is optional and indicates which direction the current date was being
  6315. // incremented or decremented (1 or -1).
  6316. massageCurrentDate: function(date, direction) {
  6317. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  6318. if (this.isHiddenDay(date)) {
  6319. date = this.skipHiddenDays(date, direction);
  6320. date.startOf('day');
  6321. }
  6322. }
  6323. return date;
  6324. },
  6325. /* Title and Date Formatting
  6326. ------------------------------------------------------------------------------------------------------------------*/
  6327. // Sets the view's title property to the most updated computed value
  6328. updateTitle: function() {
  6329. this.title = this.computeTitle();
  6330. this.calendar.setToolbarsTitle(this.title);
  6331. },
  6332. // Computes what the title at the top of the calendar should be for this view
  6333. computeTitle: function() {
  6334. return this.formatRange(
  6335. {
  6336. // in case intervalStart/End has a time, make sure timezone is correct
  6337. start: this.calendar.applyTimezone(this.intervalStart),
  6338. end: this.calendar.applyTimezone(this.intervalEnd)
  6339. },
  6340. this.opt('titleFormat') || this.computeTitleFormat(),
  6341. this.opt('titleRangeSeparator')
  6342. );
  6343. },
  6344. // Generates the format string that should be used to generate the title for the current date range.
  6345. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  6346. computeTitleFormat: function() {
  6347. if (this.intervalUnit == 'year') {
  6348. return 'YYYY';
  6349. }
  6350. else if (this.intervalUnit == 'month') {
  6351. return this.opt('monthYearFormat'); // like "September 2014"
  6352. }
  6353. else if (this.intervalDuration.as('days') > 1) {
  6354. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  6355. }
  6356. else {
  6357. return 'LL'; // one day. longer, like "September 9 2014"
  6358. }
  6359. },
  6360. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  6361. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  6362. // The timezones of the dates within `range` will be respected.
  6363. formatRange: function(range, formatStr, separator) {
  6364. var end = range.end;
  6365. if (!end.hasTime()) { // all-day?
  6366. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  6367. }
  6368. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  6369. },
  6370. getAllDayHtml: function() {
  6371. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
  6372. },
  6373. /* Navigation
  6374. ------------------------------------------------------------------------------------------------------------------*/
  6375. // Generates HTML for an anchor to another view into the calendar.
  6376. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  6377. // `gotoOptions` can either be a moment input, or an object with the form:
  6378. // { date, type, forceOff }
  6379. // `type` is a view-type like "day" or "week". default value is "day".
  6380. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  6381. buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
  6382. var date, type, forceOff;
  6383. var finalOptions;
  6384. if ($.isPlainObject(gotoOptions)) {
  6385. date = gotoOptions.date;
  6386. type = gotoOptions.type;
  6387. forceOff = gotoOptions.forceOff;
  6388. }
  6389. else {
  6390. date = gotoOptions; // a single moment input
  6391. }
  6392. date = FC.moment(date); // if a string, parse it
  6393. finalOptions = { // for serialization into the link
  6394. date: date.format('YYYY-MM-DD'),
  6395. type: type || 'day'
  6396. };
  6397. if (typeof attrs === 'string') {
  6398. innerHtml = attrs;
  6399. attrs = null;
  6400. }
  6401. attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
  6402. innerHtml = innerHtml || '';
  6403. if (!forceOff && this.opt('navLinks')) {
  6404. return '<a' + attrs +
  6405. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  6406. innerHtml +
  6407. '</a>';
  6408. }
  6409. else {
  6410. return '<span' + attrs + '>' +
  6411. innerHtml +
  6412. '</span>';
  6413. }
  6414. },
  6415. // Rendering Non-date-related Content
  6416. // -----------------------------------------------------------------------------------------------------------------
  6417. // Sets the container element that the view should render inside of, does global DOM-related initializations,
  6418. // and renders all the non-date-related content inside.
  6419. setElement: function(el) {
  6420. this.el = el;
  6421. this.bindGlobalHandlers();
  6422. this.renderSkeleton();
  6423. },
  6424. // Removes the view's container element from the DOM, clearing any content beforehand.
  6425. // Undoes any other DOM-related attachments.
  6426. removeElement: function() {
  6427. this.unsetDate();
  6428. this.unrenderSkeleton();
  6429. this.unbindGlobalHandlers();
  6430. this.el.remove();
  6431. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  6432. // We don't null-out the View's other jQuery element references upon destroy,
  6433. // so we shouldn't kill this.el either.
  6434. },
  6435. // Renders the basic structure of the view before any content is rendered
  6436. renderSkeleton: function() {
  6437. // subclasses should implement
  6438. },
  6439. // Unrenders the basic structure of the view
  6440. unrenderSkeleton: function() {
  6441. // subclasses should implement
  6442. },
  6443. // Date Setting/Unsetting
  6444. // -----------------------------------------------------------------------------------------------------------------
  6445. setDate: function(date) {
  6446. var isReset = this.isDateSet;
  6447. this.isDateSet = true;
  6448. this.handleDate(date, isReset);
  6449. this.trigger(isReset ? 'dateReset' : 'dateSet', date);
  6450. },
  6451. unsetDate: function() {
  6452. if (this.isDateSet) {
  6453. this.isDateSet = false;
  6454. this.handleDateUnset();
  6455. this.trigger('dateUnset');
  6456. }
  6457. },
  6458. // Date Handling
  6459. // -----------------------------------------------------------------------------------------------------------------
  6460. handleDate: function(date, isReset) {
  6461. var _this = this;
  6462. this.unbindEvents(); // will do nothing if not already bound
  6463. this.requestDateRender(date).then(function() {
  6464. // wish we could start earlier, but setRange/computeRange needs to execute first
  6465. _this.bindEvents(); // will request events
  6466. });
  6467. },
  6468. handleDateUnset: function() {
  6469. this.unbindEvents();
  6470. this.requestDateUnrender();
  6471. },
  6472. // Date Render Queuing
  6473. // -----------------------------------------------------------------------------------------------------------------
  6474. // if date not specified, uses current
  6475. requestDateRender: function(date) {
  6476. var _this = this;
  6477. return this.dateRenderQueue.add(function() {
  6478. return _this.executeDateRender(date);
  6479. });
  6480. },
  6481. requestDateUnrender: function() {
  6482. var _this = this;
  6483. return this.dateRenderQueue.add(function() {
  6484. return _this.executeDateUnrender();
  6485. });
  6486. },
  6487. // Date High-level Rendering
  6488. // -----------------------------------------------------------------------------------------------------------------
  6489. // if date not specified, uses current
  6490. executeDateRender: function(date) {
  6491. var _this = this;
  6492. // if rendering a new date, reset scroll to initial state (scrollTime)
  6493. if (date) {
  6494. this.captureInitialScroll();
  6495. }
  6496. else {
  6497. this.captureScroll(); // a rerender of the current date
  6498. }
  6499. this.freezeHeight();
  6500. return this.executeDateUnrender().then(function() {
  6501. if (date) {
  6502. _this.setRange(_this.computeRange(date));
  6503. }
  6504. if (_this.render) {
  6505. _this.render(); // TODO: deprecate
  6506. }
  6507. _this.renderDates();
  6508. _this.updateSize();
  6509. _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  6510. _this.startNowIndicator();
  6511. _this.thawHeight();
  6512. _this.releaseScroll();
  6513. _this.isDateRendered = true;
  6514. _this.onDateRender();
  6515. _this.trigger('dateRender');
  6516. });
  6517. },
  6518. executeDateUnrender: function() {
  6519. var _this = this;
  6520. if (_this.isDateRendered) {
  6521. return this.requestEventsUnrender().then(function() {
  6522. _this.unselect();
  6523. _this.stopNowIndicator();
  6524. _this.triggerUnrender();
  6525. _this.unrenderBusinessHours();
  6526. _this.unrenderDates();
  6527. if (_this.destroy) {
  6528. _this.destroy(); // TODO: deprecate
  6529. }
  6530. _this.isDateRendered = false;
  6531. _this.trigger('dateUnrender');
  6532. });
  6533. }
  6534. else {
  6535. return Promise.resolve();
  6536. }
  6537. },
  6538. // Date Rendering Triggers
  6539. // -----------------------------------------------------------------------------------------------------------------
  6540. onDateRender: function() {
  6541. this.triggerRender();
  6542. },
  6543. // Date Low-level Rendering
  6544. // -----------------------------------------------------------------------------------------------------------------
  6545. // date-cell content only
  6546. renderDates: function() {
  6547. // subclasses should implement
  6548. },
  6549. // date-cell content only
  6550. unrenderDates: function() {
  6551. // subclasses should override
  6552. },
  6553. // Misc view rendering utils
  6554. // -------------------------
  6555. // Signals that the view's content has been rendered
  6556. triggerRender: function() {
  6557. this.publiclyTrigger('viewRender', this, this, this.el);
  6558. },
  6559. // Signals that the view's content is about to be unrendered
  6560. triggerUnrender: function() {
  6561. this.publiclyTrigger('viewDestroy', this, this, this.el);
  6562. },
  6563. // Binds DOM handlers to elements that reside outside the view container, such as the document
  6564. bindGlobalHandlers: function() {
  6565. this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
  6566. this.listenTo($(document), 'touchstart', this.processUnselect);
  6567. },
  6568. // Unbinds DOM handlers from elements that reside outside the view container
  6569. unbindGlobalHandlers: function() {
  6570. this.stopListeningTo($(document));
  6571. },
  6572. // Initializes internal variables related to theming
  6573. initThemingProps: function() {
  6574. var tm = this.opt('theme') ? 'ui' : 'fc';
  6575. this.widgetHeaderClass = tm + '-widget-header';
  6576. this.widgetContentClass = tm + '-widget-content';
  6577. this.highlightStateClass = tm + '-state-highlight';
  6578. },
  6579. /* Business Hours
  6580. ------------------------------------------------------------------------------------------------------------------*/
  6581. // Renders business-hours onto the view. Assumes updateSize has already been called.
  6582. renderBusinessHours: function() {
  6583. // subclasses should implement
  6584. },
  6585. // Unrenders previously-rendered business-hours
  6586. unrenderBusinessHours: function() {
  6587. // subclasses should implement
  6588. },
  6589. /* Now Indicator
  6590. ------------------------------------------------------------------------------------------------------------------*/
  6591. // Immediately render the current time indicator and begins re-rendering it at an interval,
  6592. // which is defined by this.getNowIndicatorUnit().
  6593. // TODO: somehow do this for the current whole day's background too
  6594. startNowIndicator: function() {
  6595. var _this = this;
  6596. var unit;
  6597. var update;
  6598. var delay; // ms wait value
  6599. if (this.opt('nowIndicator')) {
  6600. unit = this.getNowIndicatorUnit();
  6601. if (unit) {
  6602. update = proxy(this, 'updateNowIndicator'); // bind to `this`
  6603. this.initialNowDate = this.calendar.getNow();
  6604. this.initialNowQueriedMs = +new Date();
  6605. this.renderNowIndicator(this.initialNowDate);
  6606. this.isNowIndicatorRendered = true;
  6607. // wait until the beginning of the next interval
  6608. delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
  6609. this.nowIndicatorTimeoutID = setTimeout(function() {
  6610. _this.nowIndicatorTimeoutID = null;
  6611. update();
  6612. delay = +moment.duration(1, unit);
  6613. delay = Math.max(100, delay); // prevent too frequent
  6614. _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
  6615. }, delay);
  6616. }
  6617. }
  6618. },
  6619. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  6620. // since the initial getNow call.
  6621. updateNowIndicator: function() {
  6622. if (this.isNowIndicatorRendered) {
  6623. this.unrenderNowIndicator();
  6624. this.renderNowIndicator(
  6625. this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
  6626. );
  6627. }
  6628. },
  6629. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  6630. // Won't cause side effects if indicator isn't rendered.
  6631. stopNowIndicator: function() {
  6632. if (this.isNowIndicatorRendered) {
  6633. if (this.nowIndicatorTimeoutID) {
  6634. clearTimeout(this.nowIndicatorTimeoutID);
  6635. this.nowIndicatorTimeoutID = null;
  6636. }
  6637. if (this.nowIndicatorIntervalID) {
  6638. clearTimeout(this.nowIndicatorIntervalID);
  6639. this.nowIndicatorIntervalID = null;
  6640. }
  6641. this.unrenderNowIndicator();
  6642. this.isNowIndicatorRendered = false;
  6643. }
  6644. },
  6645. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  6646. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  6647. getNowIndicatorUnit: function() {
  6648. // subclasses should implement
  6649. },
  6650. // Renders a current time indicator at the given datetime
  6651. renderNowIndicator: function(date) {
  6652. // subclasses should implement
  6653. },
  6654. // Undoes the rendering actions from renderNowIndicator
  6655. unrenderNowIndicator: function() {
  6656. // subclasses should implement
  6657. },
  6658. /* Dimensions
  6659. ------------------------------------------------------------------------------------------------------------------*/
  6660. // Refreshes anything dependant upon sizing of the container element of the grid
  6661. updateSize: function(isResize) {
  6662. if (isResize) {
  6663. this.captureScroll();
  6664. }
  6665. this.updateHeight(isResize);
  6666. this.updateWidth(isResize);
  6667. this.updateNowIndicator();
  6668. if (isResize) {
  6669. this.releaseScroll();
  6670. }
  6671. },
  6672. // Refreshes the horizontal dimensions of the calendar
  6673. updateWidth: function(isResize) {
  6674. // subclasses should implement
  6675. },
  6676. // Refreshes the vertical dimensions of the calendar
  6677. updateHeight: function(isResize) {
  6678. var calendar = this.calendar; // we poll the calendar for height information
  6679. this.setHeight(
  6680. calendar.getSuggestedViewHeight(),
  6681. calendar.isHeightAuto()
  6682. );
  6683. },
  6684. // Updates the vertical dimensions of the calendar to the specified height.
  6685. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  6686. setHeight: function(height, isAuto) {
  6687. // subclasses should implement
  6688. },
  6689. /* Scroller
  6690. ------------------------------------------------------------------------------------------------------------------*/
  6691. capturedScroll: null,
  6692. capturedScrollDepth: 0,
  6693. captureScroll: function() {
  6694. if (!(this.capturedScrollDepth++)) {
  6695. this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
  6696. return true; // root?
  6697. }
  6698. return false;
  6699. },
  6700. captureInitialScroll: function(forcedScroll) {
  6701. if (this.captureScroll()) { // root?
  6702. this.capturedScroll.isInitial = true;
  6703. if (forcedScroll) {
  6704. $.extend(this.capturedScroll, forcedScroll);
  6705. }
  6706. else {
  6707. this.capturedScroll.isComputed = true;
  6708. }
  6709. }
  6710. },
  6711. releaseScroll: function() {
  6712. var scroll = this.capturedScroll;
  6713. var isRoot = this.discardScroll();
  6714. if (scroll.isComputed) {
  6715. if (isRoot) {
  6716. // only compute initial scroll if it will actually be used (is the root capture)
  6717. $.extend(scroll, this.computeInitialScroll());
  6718. }
  6719. else {
  6720. scroll = null; // scroll couldn't be computed. don't apply it to the DOM
  6721. }
  6722. }
  6723. if (scroll) {
  6724. // we act immediately on a releaseScroll operation, as opposed to captureScroll.
  6725. // if capture/release wraps a render operation that screws up the scroll,
  6726. // we still want to restore it a good state after, regardless of depth.
  6727. if (scroll.isInitial) {
  6728. this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
  6729. }
  6730. else {
  6731. this.setScroll(scroll);
  6732. }
  6733. }
  6734. },
  6735. discardScroll: function() {
  6736. if (!(--this.capturedScrollDepth)) {
  6737. this.capturedScroll = null;
  6738. return true; // root?
  6739. }
  6740. return false;
  6741. },
  6742. computeInitialScroll: function() {
  6743. return {};
  6744. },
  6745. queryScroll: function() {
  6746. return {};
  6747. },
  6748. hardSetScroll: function(scroll) {
  6749. var _this = this;
  6750. var exec = function() { _this.setScroll(scroll); };
  6751. exec();
  6752. setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
  6753. },
  6754. setScroll: function(scroll) {
  6755. },
  6756. /* Height Freezing
  6757. ------------------------------------------------------------------------------------------------------------------*/
  6758. freezeHeight: function() {
  6759. this.calendar.freezeContentHeight();
  6760. },
  6761. thawHeight: function() {
  6762. this.calendar.thawContentHeight();
  6763. },
  6764. // Event Binding/Unbinding
  6765. // -----------------------------------------------------------------------------------------------------------------
  6766. bindEvents: function() {
  6767. var _this = this;
  6768. if (!this.isEventsBound) {
  6769. this.isEventsBound = true;
  6770. this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
  6771. _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
  6772. _this.setEvents(events);
  6773. });
  6774. }
  6775. },
  6776. unbindEvents: function() {
  6777. if (this.isEventsBound) {
  6778. this.isEventsBound = false;
  6779. this.stopListeningTo(this.calendar, 'eventsReset');
  6780. this.unsetEvents();
  6781. this.trigger('eventsUnbind');
  6782. }
  6783. },
  6784. // Event Setting/Unsetting
  6785. // -----------------------------------------------------------------------------------------------------------------
  6786. setEvents: function(events) {
  6787. var isReset = this.isEventSet;
  6788. this.isEventsSet = true;
  6789. this.handleEvents(events, isReset);
  6790. this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
  6791. },
  6792. unsetEvents: function() {
  6793. if (this.isEventsSet) {
  6794. this.isEventsSet = false;
  6795. this.handleEventsUnset();
  6796. this.trigger('eventsUnset');
  6797. }
  6798. },
  6799. whenEventsSet: function() {
  6800. var _this = this;
  6801. if (this.isEventsSet) {
  6802. return Promise.resolve(this.getCurrentEvents());
  6803. }
  6804. else {
  6805. return new Promise(function(resolve) {
  6806. _this.one('eventsSet', resolve);
  6807. });
  6808. }
  6809. },
  6810. // Event Handling
  6811. // -----------------------------------------------------------------------------------------------------------------
  6812. handleEvents: function(events, isReset) {
  6813. this.requestEventsRender(events);
  6814. },
  6815. handleEventsUnset: function() {
  6816. this.requestEventsUnrender();
  6817. },
  6818. // Event Render Queuing
  6819. // -----------------------------------------------------------------------------------------------------------------
  6820. // assumes any previous event renders have been cleared already
  6821. requestEventsRender: function(events) {
  6822. var _this = this;
  6823. return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
  6824. return _this.executeEventsRender(events);
  6825. });
  6826. },
  6827. requestEventsUnrender: function() {
  6828. var _this = this;
  6829. if (this.isEventsRendered) {
  6830. return this.eventRenderQueue.addQuickly(function() {
  6831. return _this.executeEventsUnrender();
  6832. });
  6833. }
  6834. else {
  6835. return Promise.resolve();
  6836. }
  6837. },
  6838. requestCurrentEventsRender: function() {
  6839. if (this.isEventsSet) {
  6840. this.requestEventsRender(this.getCurrentEvents());
  6841. }
  6842. else {
  6843. return Promise.reject();
  6844. }
  6845. },
  6846. // Event High-level Rendering
  6847. // -----------------------------------------------------------------------------------------------------------------
  6848. executeEventsRender: function(events) {
  6849. var _this = this;
  6850. this.captureScroll();
  6851. this.freezeHeight();
  6852. return this.executeEventsUnrender().then(function() {
  6853. _this.renderEvents(events);
  6854. _this.thawHeight();
  6855. _this.releaseScroll();
  6856. _this.isEventsRendered = true;
  6857. _this.onEventsRender();
  6858. _this.trigger('eventsRender');
  6859. });
  6860. },
  6861. executeEventsUnrender: function() {
  6862. if (this.isEventsRendered) {
  6863. this.onBeforeEventsUnrender();
  6864. this.captureScroll();
  6865. this.freezeHeight();
  6866. if (this.destroyEvents) {
  6867. this.destroyEvents(); // TODO: deprecate
  6868. }
  6869. this.unrenderEvents();
  6870. this.thawHeight();
  6871. this.releaseScroll();
  6872. this.isEventsRendered = false;
  6873. this.trigger('eventsUnrender');
  6874. }
  6875. return Promise.resolve(); // always synchronous
  6876. },
  6877. // Event Rendering Triggers
  6878. // -----------------------------------------------------------------------------------------------------------------
  6879. // Signals that all events have been rendered
  6880. onEventsRender: function() {
  6881. this.renderedEventSegEach(function(seg) {
  6882. this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
  6883. });
  6884. this.publiclyTrigger('eventAfterAllRender');
  6885. },
  6886. // Signals that all event elements are about to be removed
  6887. onBeforeEventsUnrender: function() {
  6888. this.renderedEventSegEach(function(seg) {
  6889. this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
  6890. });
  6891. },
  6892. // Event Low-level Rendering
  6893. // -----------------------------------------------------------------------------------------------------------------
  6894. // Renders the events onto the view.
  6895. renderEvents: function(events) {
  6896. // subclasses should implement
  6897. },
  6898. // Removes event elements from the view.
  6899. unrenderEvents: function() {
  6900. // subclasses should implement
  6901. },
  6902. // Event Data Access
  6903. // -----------------------------------------------------------------------------------------------------------------
  6904. requestEvents: function() {
  6905. return this.calendar.requestEvents(this.start, this.end);
  6906. },
  6907. getCurrentEvents: function() {
  6908. return this.calendar.getPrunedEventCache();
  6909. },
  6910. // Event Rendering Utils
  6911. // -----------------------------------------------------------------------------------------------------------------
  6912. // Given an event and the default element used for rendering, returns the element that should actually be used.
  6913. // Basically runs events and elements through the eventRender hook.
  6914. resolveEventEl: function(event, el) {
  6915. var custom = this.publiclyTrigger('eventRender', event, event, el);
  6916. if (custom === false) { // means don't render at all
  6917. el = null;
  6918. }
  6919. else if (custom && custom !== true) {
  6920. el = $(custom);
  6921. }
  6922. return el;
  6923. },
  6924. // Hides all rendered event segments linked to the given event
  6925. showEvent: function(event) {
  6926. this.renderedEventSegEach(function(seg) {
  6927. seg.el.css('visibility', '');
  6928. }, event);
  6929. },
  6930. // Shows all rendered event segments linked to the given event
  6931. hideEvent: function(event) {
  6932. this.renderedEventSegEach(function(seg) {
  6933. seg.el.css('visibility', 'hidden');
  6934. }, event);
  6935. },
  6936. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  6937. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  6938. // The `this` value of the callback function will be the view.
  6939. renderedEventSegEach: function(func, event) {
  6940. var segs = this.getEventSegs();
  6941. var i;
  6942. for (i = 0; i < segs.length; i++) {
  6943. if (!event || segs[i].event._id === event._id) {
  6944. if (segs[i].el) {
  6945. func.call(this, segs[i]);
  6946. }
  6947. }
  6948. }
  6949. },
  6950. // Retrieves all the rendered segment objects for the view
  6951. getEventSegs: function() {
  6952. // subclasses must implement
  6953. return [];
  6954. },
  6955. /* Event Drag-n-Drop
  6956. ------------------------------------------------------------------------------------------------------------------*/
  6957. // Computes if the given event is allowed to be dragged by the user
  6958. isEventDraggable: function(event) {
  6959. return this.isEventStartEditable(event);
  6960. },
  6961. isEventStartEditable: function(event) {
  6962. return firstDefined(
  6963. event.startEditable,
  6964. (event.source || {}).startEditable,
  6965. this.opt('eventStartEditable'),
  6966. this.isEventGenerallyEditable(event)
  6967. );
  6968. },
  6969. isEventGenerallyEditable: function(event) {
  6970. return firstDefined(
  6971. event.editable,
  6972. (event.source || {}).editable,
  6973. this.opt('editable')
  6974. );
  6975. },
  6976. // Must be called when an event in the view is dropped onto new location.
  6977. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  6978. reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  6979. var calendar = this.calendar;
  6980. var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  6981. var undoFunc = function() {
  6982. mutateResult.undo();
  6983. calendar.reportEventChange();
  6984. };
  6985. this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  6986. calendar.reportEventChange(); // will rerender events
  6987. },
  6988. // Triggers event-drop handlers that have subscribed via the API
  6989. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  6990. this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  6991. },
  6992. /* External Element Drag-n-Drop
  6993. ------------------------------------------------------------------------------------------------------------------*/
  6994. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  6995. // `meta` is the parsed data that has been embedded into the dragging event.
  6996. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  6997. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  6998. var eventProps = meta.eventProps;
  6999. var eventInput;
  7000. var event;
  7001. // Try to build an event object and render it. TODO: decouple the two
  7002. if (eventProps) {
  7003. eventInput = $.extend({}, eventProps, dropLocation);
  7004. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  7005. }
  7006. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  7007. },
  7008. // Triggers external-drop handlers that have subscribed via the API
  7009. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  7010. // trigger 'drop' regardless of whether element represents an event
  7011. this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
  7012. if (event) {
  7013. this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
  7014. }
  7015. },
  7016. /* Drag-n-Drop Rendering (for both events and external elements)
  7017. ------------------------------------------------------------------------------------------------------------------*/
  7018. // Renders a visual indication of a event or external-element drag over the given drop zone.
  7019. // If an external-element, seg will be `null`.
  7020. // Must return elements used for any mock events.
  7021. renderDrag: function(dropLocation, seg) {
  7022. // subclasses must implement
  7023. },
  7024. // Unrenders a visual indication of an event or external-element being dragged.
  7025. unrenderDrag: function() {
  7026. // subclasses must implement
  7027. },
  7028. /* Event Resizing
  7029. ------------------------------------------------------------------------------------------------------------------*/
  7030. // Computes if the given event is allowed to be resized from its starting edge
  7031. isEventResizableFromStart: function(event) {
  7032. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  7033. },
  7034. // Computes if the given event is allowed to be resized from its ending edge
  7035. isEventResizableFromEnd: function(event) {
  7036. return this.isEventResizable(event);
  7037. },
  7038. // Computes if the given event is allowed to be resized by the user at all
  7039. isEventResizable: function(event) {
  7040. var source = event.source || {};
  7041. return firstDefined(
  7042. event.durationEditable,
  7043. source.durationEditable,
  7044. this.opt('eventDurationEditable'),
  7045. event.editable,
  7046. source.editable,
  7047. this.opt('editable')
  7048. );
  7049. },
  7050. // Must be called when an event in the view has been resized to a new length
  7051. reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  7052. var calendar = this.calendar;
  7053. var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  7054. var undoFunc = function() {
  7055. mutateResult.undo();
  7056. calendar.reportEventChange();
  7057. };
  7058. this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  7059. calendar.reportEventChange(); // will rerender events
  7060. },
  7061. // Triggers event-resize handlers that have subscribed via the API
  7062. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  7063. this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  7064. },
  7065. /* Selection (time range)
  7066. ------------------------------------------------------------------------------------------------------------------*/
  7067. // Selects a date span on the view. `start` and `end` are both Moments.
  7068. // `ev` is the native mouse event that begin the interaction.
  7069. select: function(span, ev) {
  7070. this.unselect(ev);
  7071. this.renderSelection(span);
  7072. this.reportSelection(span, ev);
  7073. },
  7074. // Renders a visual indication of the selection
  7075. renderSelection: function(span) {
  7076. // subclasses should implement
  7077. },
  7078. // Called when a new selection is made. Updates internal state and triggers handlers.
  7079. reportSelection: function(span, ev) {
  7080. this.isSelected = true;
  7081. this.triggerSelect(span, ev);
  7082. },
  7083. // Triggers handlers to 'select'
  7084. triggerSelect: function(span, ev) {
  7085. this.publiclyTrigger(
  7086. 'select',
  7087. null,
  7088. this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
  7089. this.calendar.applyTimezone(span.end), // "
  7090. ev
  7091. );
  7092. },
  7093. // Undoes a selection. updates in the internal state and triggers handlers.
  7094. // `ev` is the native mouse event that began the interaction.
  7095. unselect: function(ev) {
  7096. if (this.isSelected) {
  7097. this.isSelected = false;
  7098. if (this.destroySelection) {
  7099. this.destroySelection(); // TODO: deprecate
  7100. }
  7101. this.unrenderSelection();
  7102. this.publiclyTrigger('unselect', null, ev);
  7103. }
  7104. },
  7105. // Unrenders a visual indication of selection
  7106. unrenderSelection: function() {
  7107. // subclasses should implement
  7108. },
  7109. /* Event Selection
  7110. ------------------------------------------------------------------------------------------------------------------*/
  7111. selectEvent: function(event) {
  7112. if (!this.selectedEvent || this.selectedEvent !== event) {
  7113. this.unselectEvent();
  7114. this.renderedEventSegEach(function(seg) {
  7115. seg.el.addClass('fc-selected');
  7116. }, event);
  7117. this.selectedEvent = event;
  7118. }
  7119. },
  7120. unselectEvent: function() {
  7121. if (this.selectedEvent) {
  7122. this.renderedEventSegEach(function(seg) {
  7123. seg.el.removeClass('fc-selected');
  7124. }, this.selectedEvent);
  7125. this.selectedEvent = null;
  7126. }
  7127. },
  7128. isEventSelected: function(event) {
  7129. // event references might change on refetchEvents(), while selectedEvent doesn't,
  7130. // so compare IDs
  7131. return this.selectedEvent && this.selectedEvent._id === event._id;
  7132. },
  7133. /* Mouse / Touch Unselecting (time range & event unselection)
  7134. ------------------------------------------------------------------------------------------------------------------*/
  7135. // TODO: move consistently to down/start or up/end?
  7136. // TODO: don't kill previous selection if touch scrolling
  7137. handleDocumentMousedown: function(ev) {
  7138. if (isPrimaryMouseButton(ev)) {
  7139. this.processUnselect(ev);
  7140. }
  7141. },
  7142. processUnselect: function(ev) {
  7143. this.processRangeUnselect(ev);
  7144. this.processEventUnselect(ev);
  7145. },
  7146. processRangeUnselect: function(ev) {
  7147. var ignore;
  7148. // is there a time-range selection?
  7149. if (this.isSelected && this.opt('unselectAuto')) {
  7150. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  7151. ignore = this.opt('unselectCancel');
  7152. if (!ignore || !$(ev.target).closest(ignore).length) {
  7153. this.unselect(ev);
  7154. }
  7155. }
  7156. },
  7157. processEventUnselect: function(ev) {
  7158. if (this.selectedEvent) {
  7159. if (!$(ev.target).closest('.fc-selected').length) {
  7160. this.unselectEvent();
  7161. }
  7162. }
  7163. },
  7164. /* Day Click
  7165. ------------------------------------------------------------------------------------------------------------------*/
  7166. // Triggers handlers to 'dayClick'
  7167. // Span has start/end of the clicked area. Only the start is useful.
  7168. triggerDayClick: function(span, dayEl, ev) {
  7169. this.publiclyTrigger(
  7170. 'dayClick',
  7171. dayEl,
  7172. this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
  7173. ev
  7174. );
  7175. },
  7176. /* Date Utils
  7177. ------------------------------------------------------------------------------------------------------------------*/
  7178. // Initializes internal variables related to calculating hidden days-of-week
  7179. initHiddenDays: function() {
  7180. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  7181. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  7182. var dayCnt = 0;
  7183. var i;
  7184. if (this.opt('weekends') === false) {
  7185. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  7186. }
  7187. for (i = 0; i < 7; i++) {
  7188. if (
  7189. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  7190. ) {
  7191. dayCnt++;
  7192. }
  7193. }
  7194. if (!dayCnt) {
  7195. throw 'invalid hiddenDays'; // all days were hidden? bad.
  7196. }
  7197. this.isHiddenDayHash = isHiddenDayHash;
  7198. },
  7199. // Is the current day hidden?
  7200. // `day` is a day-of-week index (0-6), or a Moment
  7201. isHiddenDay: function(day) {
  7202. if (moment.isMoment(day)) {
  7203. day = day.day();
  7204. }
  7205. return this.isHiddenDayHash[day];
  7206. },
  7207. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  7208. // If the initial value of `date` is not a hidden day, don't do anything.
  7209. // Pass `isExclusive` as `true` if you are dealing with an end date.
  7210. // `inc` defaults to `1` (increment one day forward each time)
  7211. skipHiddenDays: function(date, inc, isExclusive) {
  7212. var out = date.clone();
  7213. inc = inc || 1;
  7214. while (
  7215. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  7216. ) {
  7217. out.add(inc, 'days');
  7218. }
  7219. return out;
  7220. },
  7221. // Returns the date range of the full days the given range visually appears to occupy.
  7222. // Returns a new range object.
  7223. computeDayRange: function(range) {
  7224. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  7225. var end = range.end;
  7226. var endDay = null;
  7227. var endTimeMS;
  7228. if (end) {
  7229. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  7230. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  7231. // If the end time is actually inclusively part of the next day and is equal to or
  7232. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  7233. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  7234. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  7235. endDay.add(1, 'days');
  7236. }
  7237. }
  7238. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  7239. // assign the default duration of one day.
  7240. if (!end || endDay <= startDay) {
  7241. endDay = startDay.clone().add(1, 'days');
  7242. }
  7243. return { start: startDay, end: endDay };
  7244. },
  7245. // Does the given event visually appear to occupy more than one day?
  7246. isMultiDayEvent: function(event) {
  7247. var range = this.computeDayRange(event); // event is range-ish
  7248. return range.end.diff(range.start, 'days') > 1;
  7249. }
  7250. });
  7251. ;;
  7252. /*
  7253. Embodies a div that has potential scrollbars
  7254. */
  7255. var Scroller = FC.Scroller = Class.extend({
  7256. el: null, // the guaranteed outer element
  7257. scrollEl: null, // the element with the scrollbars
  7258. overflowX: null,
  7259. overflowY: null,
  7260. constructor: function(options) {
  7261. options = options || {};
  7262. this.overflowX = options.overflowX || options.overflow || 'auto';
  7263. this.overflowY = options.overflowY || options.overflow || 'auto';
  7264. },
  7265. render: function() {
  7266. this.el = this.renderEl();
  7267. this.applyOverflow();
  7268. },
  7269. renderEl: function() {
  7270. return (this.scrollEl = $('<div class="fc-scroller"></div>'));
  7271. },
  7272. // sets to natural height, unlocks overflow
  7273. clear: function() {
  7274. this.setHeight('auto');
  7275. this.applyOverflow();
  7276. },
  7277. destroy: function() {
  7278. this.el.remove();
  7279. },
  7280. // Overflow
  7281. // -----------------------------------------------------------------------------------------------------------------
  7282. applyOverflow: function() {
  7283. this.scrollEl.css({
  7284. 'overflow-x': this.overflowX,
  7285. 'overflow-y': this.overflowY
  7286. });
  7287. },
  7288. // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
  7289. // Useful for preserving scrollbar widths regardless of future resizes.
  7290. // Can pass in scrollbarWidths for optimization.
  7291. lockOverflow: function(scrollbarWidths) {
  7292. var overflowX = this.overflowX;
  7293. var overflowY = this.overflowY;
  7294. scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
  7295. if (overflowX === 'auto') {
  7296. overflowX = (
  7297. scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
  7298. // OR scrolling pane with massless scrollbars?
  7299. this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
  7300. // subtract 1 because of IE off-by-one issue
  7301. ) ? 'scroll' : 'hidden';
  7302. }
  7303. if (overflowY === 'auto') {
  7304. overflowY = (
  7305. scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
  7306. // OR scrolling pane with massless scrollbars?
  7307. this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
  7308. // subtract 1 because of IE off-by-one issue
  7309. ) ? 'scroll' : 'hidden';
  7310. }
  7311. this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
  7312. },
  7313. // Getters / Setters
  7314. // -----------------------------------------------------------------------------------------------------------------
  7315. setHeight: function(height) {
  7316. this.scrollEl.height(height);
  7317. },
  7318. getScrollTop: function() {
  7319. return this.scrollEl.scrollTop();
  7320. },
  7321. setScrollTop: function(top) {
  7322. this.scrollEl.scrollTop(top);
  7323. },
  7324. getClientWidth: function() {
  7325. return this.scrollEl[0].clientWidth;
  7326. },
  7327. getClientHeight: function() {
  7328. return this.scrollEl[0].clientHeight;
  7329. },
  7330. getScrollbarWidths: function() {
  7331. return getScrollbarWidths(this.scrollEl);
  7332. }
  7333. });
  7334. ;;
  7335. function Iterator(items) {
  7336. this.items = items || [];
  7337. }
  7338. /* Calls a method on every item passing the arguments through */
  7339. Iterator.prototype.proxyCall = function(methodName) {
  7340. var args = Array.prototype.slice.call(arguments, 1);
  7341. var results = [];
  7342. this.items.forEach(function(item) {
  7343. results.push(item[methodName].apply(item, args));
  7344. });
  7345. return results;
  7346. };
  7347. ;;
  7348. /* Toolbar with buttons and title
  7349. ----------------------------------------------------------------------------------------------------------------------*/
  7350. function Toolbar(calendar, toolbarOptions) {
  7351. var t = this;
  7352. // exports
  7353. t.setToolbarOptions = setToolbarOptions;
  7354. t.render = render;
  7355. t.removeElement = removeElement;
  7356. t.updateTitle = updateTitle;
  7357. t.activateButton = activateButton;
  7358. t.deactivateButton = deactivateButton;
  7359. t.disableButton = disableButton;
  7360. t.enableButton = enableButton;
  7361. t.getViewsWithButtons = getViewsWithButtons;
  7362. t.el = null; // mirrors local `el`
  7363. // locals
  7364. var el;
  7365. var viewsWithButtons = [];
  7366. var tm;
  7367. // method to update toolbar-specific options, not calendar-wide options
  7368. function setToolbarOptions(newToolbarOptions) {
  7369. toolbarOptions = newToolbarOptions;
  7370. }
  7371. // can be called repeatedly and will rerender
  7372. function render() {
  7373. var sections = toolbarOptions.layout;
  7374. tm = calendar.options.theme ? 'ui' : 'fc';
  7375. if (sections) {
  7376. if (!el) {
  7377. el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
  7378. }
  7379. else {
  7380. el.empty();
  7381. }
  7382. el.append(renderSection('left'))
  7383. .append(renderSection('right'))
  7384. .append(renderSection('center'))
  7385. .append('<div class="fc-clear"/>');
  7386. }
  7387. else {
  7388. removeElement();
  7389. }
  7390. }
  7391. function removeElement() {
  7392. if (el) {
  7393. el.remove();
  7394. el = t.el = null;
  7395. }
  7396. }
  7397. function renderSection(position) {
  7398. var sectionEl = $('<div class="fc-' + position + '"/>');
  7399. var buttonStr = toolbarOptions.layout[position];
  7400. if (buttonStr) {
  7401. $.each(buttonStr.split(' '), function(i) {
  7402. var groupChildren = $();
  7403. var isOnlyButtons = true;
  7404. var groupEl;
  7405. $.each(this.split(','), function(j, buttonName) {
  7406. var customButtonProps;
  7407. var viewSpec;
  7408. var buttonClick;
  7409. var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
  7410. var defaultText;
  7411. var themeIcon;
  7412. var normalIcon;
  7413. var innerHtml;
  7414. var classes;
  7415. var button; // the element
  7416. if (buttonName == 'title') {
  7417. groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  7418. isOnlyButtons = false;
  7419. }
  7420. else {
  7421. if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
  7422. buttonClick = function(ev) {
  7423. if (customButtonProps.click) {
  7424. customButtonProps.click.call(button[0], ev);
  7425. }
  7426. };
  7427. overrideText = ''; // icons will override text
  7428. defaultText = customButtonProps.text;
  7429. }
  7430. else if ((viewSpec = calendar.getViewSpec(buttonName))) {
  7431. buttonClick = function() {
  7432. calendar.changeView(buttonName);
  7433. };
  7434. viewsWithButtons.push(buttonName);
  7435. overrideText = viewSpec.buttonTextOverride;
  7436. defaultText = viewSpec.buttonTextDefault;
  7437. }
  7438. else if (calendar[buttonName]) { // a calendar method
  7439. buttonClick = function() {
  7440. calendar[buttonName]();
  7441. };
  7442. overrideText = (calendar.overrides.buttonText || {})[buttonName];
  7443. defaultText = calendar.options.buttonText[buttonName]; // everything else is considered default
  7444. }
  7445. if (buttonClick) {
  7446. themeIcon =
  7447. customButtonProps ?
  7448. customButtonProps.themeIcon :
  7449. calendar.options.themeButtonIcons[buttonName];
  7450. normalIcon =
  7451. customButtonProps ?
  7452. customButtonProps.icon :
  7453. calendar.options.buttonIcons[buttonName];
  7454. if (overrideText) {
  7455. innerHtml = htmlEscape(overrideText);
  7456. }
  7457. else if (themeIcon && calendar.options.theme) {
  7458. innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  7459. }
  7460. else if (normalIcon && !calendar.options.theme) {
  7461. innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  7462. }
  7463. else {
  7464. innerHtml = htmlEscape(defaultText);
  7465. }
  7466. classes = [
  7467. 'fc-' + buttonName + '-button',
  7468. tm + '-button',
  7469. tm + '-state-default'
  7470. ];
  7471. button = $( // type="button" so that it doesn't submit a form
  7472. '<button type="button" class="' + classes.join(' ') + '">' +
  7473. innerHtml +
  7474. '</button>'
  7475. )
  7476. .click(function(ev) {
  7477. // don't process clicks for disabled buttons
  7478. if (!button.hasClass(tm + '-state-disabled')) {
  7479. buttonClick(ev);
  7480. // after the click action, if the button becomes the "active" tab, or disabled,
  7481. // it should never have a hover class, so remove it now.
  7482. if (
  7483. button.hasClass(tm + '-state-active') ||
  7484. button.hasClass(tm + '-state-disabled')
  7485. ) {
  7486. button.removeClass(tm + '-state-hover');
  7487. }
  7488. }
  7489. })
  7490. .mousedown(function() {
  7491. // the *down* effect (mouse pressed in).
  7492. // only on buttons that are not the "active" tab, or disabled
  7493. button
  7494. .not('.' + tm + '-state-active')
  7495. .not('.' + tm + '-state-disabled')
  7496. .addClass(tm + '-state-down');
  7497. })
  7498. .mouseup(function() {
  7499. // undo the *down* effect
  7500. button.removeClass(tm + '-state-down');
  7501. })
  7502. .hover(
  7503. function() {
  7504. // the *hover* effect.
  7505. // only on buttons that are not the "active" tab, or disabled
  7506. button
  7507. .not('.' + tm + '-state-active')
  7508. .not('.' + tm + '-state-disabled')
  7509. .addClass(tm + '-state-hover');
  7510. },
  7511. function() {
  7512. // undo the *hover* effect
  7513. button
  7514. .removeClass(tm + '-state-hover')
  7515. .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  7516. }
  7517. );
  7518. groupChildren = groupChildren.add(button);
  7519. }
  7520. }
  7521. });
  7522. if (isOnlyButtons) {
  7523. groupChildren
  7524. .first().addClass(tm + '-corner-left').end()
  7525. .last().addClass(tm + '-corner-right').end();
  7526. }
  7527. if (groupChildren.length > 1) {
  7528. groupEl = $('<div/>');
  7529. if (isOnlyButtons) {
  7530. groupEl.addClass('fc-button-group');
  7531. }
  7532. groupEl.append(groupChildren);
  7533. sectionEl.append(groupEl);
  7534. }
  7535. else {
  7536. sectionEl.append(groupChildren); // 1 or 0 children
  7537. }
  7538. });
  7539. }
  7540. return sectionEl;
  7541. }
  7542. function updateTitle(text) {
  7543. if (el) {
  7544. el.find('h2').text(text);
  7545. }
  7546. }
  7547. function activateButton(buttonName) {
  7548. if (el) {
  7549. el.find('.fc-' + buttonName + '-button')
  7550. .addClass(tm + '-state-active');
  7551. }
  7552. }
  7553. function deactivateButton(buttonName) {
  7554. if (el) {
  7555. el.find('.fc-' + buttonName + '-button')
  7556. .removeClass(tm + '-state-active');
  7557. }
  7558. }
  7559. function disableButton(buttonName) {
  7560. if (el) {
  7561. el.find('.fc-' + buttonName + '-button')
  7562. .prop('disabled', true)
  7563. .addClass(tm + '-state-disabled');
  7564. }
  7565. }
  7566. function enableButton(buttonName) {
  7567. if (el) {
  7568. el.find('.fc-' + buttonName + '-button')
  7569. .prop('disabled', false)
  7570. .removeClass(tm + '-state-disabled');
  7571. }
  7572. }
  7573. function getViewsWithButtons() {
  7574. return viewsWithButtons;
  7575. }
  7576. }
  7577. ;;
  7578. var Calendar = FC.Calendar = Class.extend({
  7579. dirDefaults: null, // option defaults related to LTR or RTL
  7580. localeDefaults: null, // option defaults related to current locale
  7581. overrides: null, // option overrides given to the fullCalendar constructor
  7582. dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
  7583. options: null, // all defaults combined with overrides
  7584. viewSpecCache: null, // cache of view definitions
  7585. view: null, // current View object
  7586. header: null,
  7587. footer: null,
  7588. loadingLevel: 0, // number of simultaneous loading tasks
  7589. // a lot of this class' OOP logic is scoped within this constructor function,
  7590. // but in the future, write individual methods on the prototype.
  7591. constructor: Calendar_constructor,
  7592. // Subclasses can override this for initialization logic after the constructor has been called
  7593. initialize: function() {
  7594. },
  7595. // Computes the flattened options hash for the calendar and assigns to `this.options`.
  7596. // Assumes this.overrides and this.dynamicOverrides have already been initialized.
  7597. populateOptionsHash: function() {
  7598. var locale, localeDefaults;
  7599. var isRTL, dirDefaults;
  7600. locale = firstDefined( // explicit locale option given?
  7601. this.dynamicOverrides.locale,
  7602. this.overrides.locale
  7603. );
  7604. localeDefaults = localeOptionHash[locale];
  7605. if (!localeDefaults) { // explicit locale option not given or invalid?
  7606. locale = Calendar.defaults.locale;
  7607. localeDefaults = localeOptionHash[locale] || {};
  7608. }
  7609. isRTL = firstDefined( // based on options computed so far, is direction RTL?
  7610. this.dynamicOverrides.isRTL,
  7611. this.overrides.isRTL,
  7612. localeDefaults.isRTL,
  7613. Calendar.defaults.isRTL
  7614. );
  7615. dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  7616. this.dirDefaults = dirDefaults;
  7617. this.localeDefaults = localeDefaults;
  7618. this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  7619. Calendar.defaults, // global defaults
  7620. dirDefaults,
  7621. localeDefaults,
  7622. this.overrides,
  7623. this.dynamicOverrides
  7624. ]);
  7625. populateInstanceComputableOptions(this.options); // fill in gaps with computed options
  7626. },
  7627. // Gets information about how to create a view. Will use a cache.
  7628. getViewSpec: function(viewType) {
  7629. var cache = this.viewSpecCache;
  7630. return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  7631. },
  7632. // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  7633. // Preference is given to views that have corresponding buttons.
  7634. getUnitViewSpec: function(unit) {
  7635. var viewTypes;
  7636. var i;
  7637. var spec;
  7638. if ($.inArray(unit, intervalUnits) != -1) {
  7639. // put views that have buttons first. there will be duplicates, but oh well
  7640. viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
  7641. $.each(FC.views, function(viewType) { // all views
  7642. viewTypes.push(viewType);
  7643. });
  7644. for (i = 0; i < viewTypes.length; i++) {
  7645. spec = this.getViewSpec(viewTypes[i]);
  7646. if (spec) {
  7647. if (spec.singleUnit == unit) {
  7648. return spec;
  7649. }
  7650. }
  7651. }
  7652. }
  7653. },
  7654. // Builds an object with information on how to create a given view
  7655. buildViewSpec: function(requestedViewType) {
  7656. var viewOverrides = this.overrides.views || {};
  7657. var specChain = []; // for the view. lowest to highest priority
  7658. var defaultsChain = []; // for the view. lowest to highest priority
  7659. var overridesChain = []; // for the view. lowest to highest priority
  7660. var viewType = requestedViewType;
  7661. var spec; // for the view
  7662. var overrides; // for the view
  7663. var duration;
  7664. var unit;
  7665. // iterate from the specific view definition to a more general one until we hit an actual View class
  7666. while (viewType) {
  7667. spec = fcViews[viewType];
  7668. overrides = viewOverrides[viewType];
  7669. viewType = null; // clear. might repopulate for another iteration
  7670. if (typeof spec === 'function') { // TODO: deprecate
  7671. spec = { 'class': spec };
  7672. }
  7673. if (spec) {
  7674. specChain.unshift(spec);
  7675. defaultsChain.unshift(spec.defaults || {});
  7676. duration = duration || spec.duration;
  7677. viewType = viewType || spec.type;
  7678. }
  7679. if (overrides) {
  7680. overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  7681. duration = duration || overrides.duration;
  7682. viewType = viewType || overrides.type;
  7683. }
  7684. }
  7685. spec = mergeProps(specChain);
  7686. spec.type = requestedViewType;
  7687. if (!spec['class']) {
  7688. return false;
  7689. }
  7690. if (duration) {
  7691. duration = moment.duration(duration);
  7692. if (duration.valueOf()) { // valid?
  7693. spec.duration = duration;
  7694. unit = computeIntervalUnit(duration);
  7695. // view is a single-unit duration, like "week" or "day"
  7696. // incorporate options for this. lowest priority
  7697. if (duration.as(unit) === 1) {
  7698. spec.singleUnit = unit;
  7699. overridesChain.unshift(viewOverrides[unit] || {});
  7700. }
  7701. }
  7702. }
  7703. spec.defaults = mergeOptions(defaultsChain);
  7704. spec.overrides = mergeOptions(overridesChain);
  7705. this.buildViewSpecOptions(spec);
  7706. this.buildViewSpecButtonText(spec, requestedViewType);
  7707. return spec;
  7708. },
  7709. // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  7710. buildViewSpecOptions: function(spec) {
  7711. spec.options = mergeOptions([ // lowest to highest priority
  7712. Calendar.defaults, // global defaults
  7713. spec.defaults, // view's defaults (from ViewSubclass.defaults)
  7714. this.dirDefaults,
  7715. this.localeDefaults, // locale and dir take precedence over view's defaults!
  7716. this.overrides, // calendar's overrides (options given to constructor)
  7717. spec.overrides, // view's overrides (view-specific options)
  7718. this.dynamicOverrides // dynamically set via setter. highest precedence
  7719. ]);
  7720. populateInstanceComputableOptions(spec.options);
  7721. },
  7722. // Computes and assigns a view spec's buttonText-related options
  7723. buildViewSpecButtonText: function(spec, requestedViewType) {
  7724. // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  7725. // requested view, falling back to a generic unit entry like "week" or "day"
  7726. function queryButtonText(options) {
  7727. var buttonText = options.buttonText || {};
  7728. return buttonText[requestedViewType] ||
  7729. // view can decide to look up a certain key
  7730. (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
  7731. // a key like "month"
  7732. (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  7733. }
  7734. // highest to lowest priority
  7735. spec.buttonTextOverride =
  7736. queryButtonText(this.dynamicOverrides) ||
  7737. queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  7738. spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  7739. // highest to lowest priority. mirrors buildViewSpecOptions
  7740. spec.buttonTextDefault =
  7741. queryButtonText(this.localeDefaults) ||
  7742. queryButtonText(this.dirDefaults) ||
  7743. spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  7744. queryButtonText(Calendar.defaults) ||
  7745. (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  7746. requestedViewType; // fall back to given view name
  7747. },
  7748. // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  7749. instantiateView: function(viewType) {
  7750. var spec = this.getViewSpec(viewType);
  7751. return new spec['class'](this, viewType, spec.options, spec.duration);
  7752. },
  7753. // Returns a boolean about whether the view is okay to instantiate at some point
  7754. isValidViewType: function(viewType) {
  7755. return Boolean(this.getViewSpec(viewType));
  7756. },
  7757. // Should be called when any type of async data fetching begins
  7758. pushLoading: function() {
  7759. if (!(this.loadingLevel++)) {
  7760. this.publiclyTrigger('loading', null, true, this.view);
  7761. }
  7762. },
  7763. // Should be called when any type of async data fetching completes
  7764. popLoading: function() {
  7765. if (!(--this.loadingLevel)) {
  7766. this.publiclyTrigger('loading', null, false, this.view);
  7767. }
  7768. },
  7769. // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
  7770. buildSelectSpan: function(zonedStartInput, zonedEndInput) {
  7771. var start = this.moment(zonedStartInput).stripZone();
  7772. var end;
  7773. if (zonedEndInput) {
  7774. end = this.moment(zonedEndInput).stripZone();
  7775. }
  7776. else if (start.hasTime()) {
  7777. end = start.clone().add(this.defaultTimedEventDuration);
  7778. }
  7779. else {
  7780. end = start.clone().add(this.defaultAllDayEventDuration);
  7781. }
  7782. return { start: start, end: end };
  7783. }
  7784. });
  7785. Calendar.mixin(EmitterMixin);
  7786. function Calendar_constructor(element, overrides) {
  7787. var t = this;
  7788. // Exports
  7789. // -----------------------------------------------------------------------------------
  7790. t.render = render;
  7791. t.destroy = destroy;
  7792. t.rerenderEvents = rerenderEvents;
  7793. t.changeView = renderView; // `renderView` will switch to another view
  7794. t.select = select;
  7795. t.unselect = unselect;
  7796. t.prev = prev;
  7797. t.next = next;
  7798. t.prevYear = prevYear;
  7799. t.nextYear = nextYear;
  7800. t.today = today;
  7801. t.gotoDate = gotoDate;
  7802. t.incrementDate = incrementDate;
  7803. t.zoomTo = zoomTo;
  7804. t.getDate = getDate;
  7805. t.getCalendar = getCalendar;
  7806. t.getView = getView;
  7807. t.option = option; // getter/setter method
  7808. t.publiclyTrigger = publiclyTrigger;
  7809. // Options
  7810. // -----------------------------------------------------------------------------------
  7811. t.dynamicOverrides = {};
  7812. t.viewSpecCache = {};
  7813. t.optionHandlers = {}; // for Calendar.options.js
  7814. t.overrides = $.extend({}, overrides); // make a copy
  7815. t.populateOptionsHash(); // sets this.options
  7816. // Locale-data Internals
  7817. // -----------------------------------------------------------------------------------
  7818. // Apply overrides to the current locale's data
  7819. var localeData;
  7820. // Called immediately, and when any of the options change.
  7821. // Happens before any internal objects rebuild or rerender, because this is very core.
  7822. t.bindOptions([
  7823. 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
  7824. ], function(locale, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) {
  7825. // normalize
  7826. if (weekNumberCalculation === 'iso') {
  7827. weekNumberCalculation = 'ISO'; // normalize
  7828. }
  7829. localeData = createObject( // make a cheap copy
  7830. getMomentLocaleData(locale) // will fall back to en
  7831. );
  7832. if (monthNames) {
  7833. localeData._months = monthNames;
  7834. }
  7835. if (monthNamesShort) {
  7836. localeData._monthsShort = monthNamesShort;
  7837. }
  7838. if (dayNames) {
  7839. localeData._weekdays = dayNames;
  7840. }
  7841. if (dayNamesShort) {
  7842. localeData._weekdaysShort = dayNamesShort;
  7843. }
  7844. if (firstDay == null && weekNumberCalculation === 'ISO') {
  7845. firstDay = 1;
  7846. }
  7847. if (firstDay != null) {
  7848. var _week = createObject(localeData._week); // _week: { dow: # }
  7849. _week.dow = firstDay;
  7850. localeData._week = _week;
  7851. }
  7852. if ( // whitelist certain kinds of input
  7853. weekNumberCalculation === 'ISO' ||
  7854. weekNumberCalculation === 'local' ||
  7855. typeof weekNumberCalculation === 'function'
  7856. ) {
  7857. localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
  7858. }
  7859. // If the internal current date object already exists, move to new locale.
  7860. // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
  7861. if (date) {
  7862. localizeMoment(date); // sets to localeData
  7863. }
  7864. });
  7865. // Calendar-specific Date Utilities
  7866. // -----------------------------------------------------------------------------------
  7867. t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration);
  7868. t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration);
  7869. // Builds a moment using the settings of the current calendar: timezone and locale.
  7870. // Accepts anything the vanilla moment() constructor accepts.
  7871. t.moment = function() {
  7872. var mom;
  7873. if (t.options.timezone === 'local') {
  7874. mom = FC.moment.apply(null, arguments);
  7875. // Force the moment to be local, because FC.moment doesn't guarantee it.
  7876. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  7877. mom.local();
  7878. }
  7879. }
  7880. else if (t.options.timezone === 'UTC') {
  7881. mom = FC.moment.utc.apply(null, arguments); // process as UTC
  7882. }
  7883. else {
  7884. mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
  7885. }
  7886. localizeMoment(mom);
  7887. return mom;
  7888. };
  7889. // Updates the given moment's locale settings to the current calendar locale settings.
  7890. function localizeMoment(mom) {
  7891. mom._locale = localeData;
  7892. }
  7893. t.localizeMoment = localizeMoment;
  7894. // Returns a boolean about whether or not the calendar knows how to calculate
  7895. // the timezone offset of arbitrary dates in the current timezone.
  7896. t.getIsAmbigTimezone = function() {
  7897. return t.options.timezone !== 'local' && t.options.timezone !== 'UTC';
  7898. };
  7899. // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
  7900. t.applyTimezone = function(date) {
  7901. if (!date.hasTime()) {
  7902. return date.clone();
  7903. }
  7904. var zonedDate = t.moment(date.toArray());
  7905. var timeAdjust = date.time() - zonedDate.time();
  7906. var adjustedZonedDate;
  7907. // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
  7908. if (timeAdjust) { // is the time result different than expected?
  7909. adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
  7910. if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now?
  7911. zonedDate = adjustedZonedDate;
  7912. }
  7913. }
  7914. return zonedDate;
  7915. };
  7916. // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
  7917. // Will return an moment with an ambiguous timezone.
  7918. t.getNow = function() {
  7919. var now = t.options.now;
  7920. if (typeof now === 'function') {
  7921. now = now();
  7922. }
  7923. return t.moment(now).stripZone();
  7924. };
  7925. // Get an event's normalized end date. If not present, calculate it from the defaults.
  7926. t.getEventEnd = function(event) {
  7927. if (event.end) {
  7928. return event.end.clone();
  7929. }
  7930. else {
  7931. return t.getDefaultEventEnd(event.allDay, event.start);
  7932. }
  7933. };
  7934. // Given an event's allDay status and start date, return what its fallback end date should be.
  7935. // TODO: rename to computeDefaultEventEnd
  7936. t.getDefaultEventEnd = function(allDay, zonedStart) {
  7937. var end = zonedStart.clone();
  7938. if (allDay) {
  7939. end.stripTime().add(t.defaultAllDayEventDuration);
  7940. }
  7941. else {
  7942. end.add(t.defaultTimedEventDuration);
  7943. }
  7944. if (t.getIsAmbigTimezone()) {
  7945. end.stripZone(); // we don't know what the tzo should be
  7946. }
  7947. return end;
  7948. };
  7949. // Produces a human-readable string for the given duration.
  7950. // Side-effect: changes the locale of the given duration.
  7951. t.humanizeDuration = function(duration) {
  7952. return duration.locale(t.options.locale).humanize();
  7953. };
  7954. // Imports
  7955. // -----------------------------------------------------------------------------------
  7956. EventManager.call(t);
  7957. // Locals
  7958. // -----------------------------------------------------------------------------------
  7959. var _element = element[0];
  7960. var toolbarsManager;
  7961. var header;
  7962. var footer;
  7963. var content;
  7964. var tm; // for making theme classes
  7965. var currentView; // NOTE: keep this in sync with this.view
  7966. var viewsByType = {}; // holds all instantiated view instances, current or not
  7967. var suggestedViewHeight;
  7968. var windowResizeProxy; // wraps the windowResize function
  7969. var ignoreWindowResize = 0;
  7970. var date; // unzoned
  7971. // Main Rendering
  7972. // -----------------------------------------------------------------------------------
  7973. // compute the initial ambig-timezone date
  7974. if (t.options.defaultDate != null) {
  7975. date = t.moment(t.options.defaultDate).stripZone();
  7976. }
  7977. else {
  7978. date = t.getNow(); // getNow already returns unzoned
  7979. }
  7980. function render() {
  7981. if (!content) {
  7982. initialRender();
  7983. }
  7984. else if (elementVisible()) {
  7985. // mainly for the public API
  7986. calcSize();
  7987. renderView();
  7988. }
  7989. }
  7990. function initialRender() {
  7991. element.addClass('fc');
  7992. // event delegation for nav links
  7993. element.on('click.fc', 'a[data-goto]', function(ev) {
  7994. var anchorEl = $(this);
  7995. var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
  7996. var date = t.moment(gotoOptions.date);
  7997. var viewType = gotoOptions.type;
  7998. // property like "navLinkDayClick". might be a string or a function
  7999. var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
  8000. if (typeof customAction === 'function') {
  8001. customAction(date, ev);
  8002. }
  8003. else {
  8004. if (typeof customAction === 'string') {
  8005. viewType = customAction;
  8006. }
  8007. zoomTo(date, viewType);
  8008. }
  8009. });
  8010. // called immediately, and upon option change
  8011. t.bindOption('theme', function(theme) {
  8012. tm = theme ? 'ui' : 'fc'; // affects a larger scope
  8013. element.toggleClass('ui-widget', theme);
  8014. element.toggleClass('fc-unthemed', !theme);
  8015. });
  8016. // called immediately, and upon option change.
  8017. // HACK: locale often affects isRTL, so we explicitly listen to that too.
  8018. t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
  8019. element.toggleClass('fc-ltr', !isRTL);
  8020. element.toggleClass('fc-rtl', isRTL);
  8021. });
  8022. content = $("<div class='fc-view-container'/>").prependTo(element);
  8023. var toolbars = buildToolbars();
  8024. toolbarsManager = new Iterator(toolbars);
  8025. header = t.header = toolbars[0];
  8026. footer = t.footer = toolbars[1];
  8027. renderHeader();
  8028. renderFooter();
  8029. renderView(t.options.defaultView);
  8030. if (t.options.handleWindowResize) {
  8031. windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
  8032. $(window).resize(windowResizeProxy);
  8033. }
  8034. }
  8035. function destroy() {
  8036. if (currentView) {
  8037. currentView.removeElement();
  8038. // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  8039. // It is still the "current" view, just not rendered.
  8040. }
  8041. toolbarsManager.proxyCall('removeElement');
  8042. content.remove();
  8043. element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  8044. element.off('.fc'); // unbind nav link handlers
  8045. if (windowResizeProxy) {
  8046. $(window).unbind('resize', windowResizeProxy);
  8047. }
  8048. }
  8049. function elementVisible() {
  8050. return element.is(':visible');
  8051. }
  8052. // View Rendering
  8053. // -----------------------------------------------------------------------------------
  8054. // Renders a view because of a date change, view-type change, or for the first time.
  8055. // If not given a viewType, keep the current view but render different dates.
  8056. // Accepts an optional scroll state to restore to.
  8057. function renderView(viewType, forcedScroll) {
  8058. ignoreWindowResize++;
  8059. var needsClearView = currentView && viewType && currentView.type !== viewType;
  8060. // if viewType is changing, remove the old view's rendering
  8061. if (needsClearView) {
  8062. freezeContentHeight(); // prevent a scroll jump when view element is removed
  8063. clearView();
  8064. }
  8065. // if viewType changed, or the view was never created, create a fresh view
  8066. if (!currentView && viewType) {
  8067. currentView = t.view =
  8068. viewsByType[viewType] ||
  8069. (viewsByType[viewType] = t.instantiateView(viewType));
  8070. currentView.setElement(
  8071. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  8072. );
  8073. toolbarsManager.proxyCall('activateButton', viewType);
  8074. }
  8075. if (currentView) {
  8076. // in case the view should render a period of time that is completely hidden
  8077. date = currentView.massageCurrentDate(date);
  8078. // render or rerender the view
  8079. if (
  8080. !currentView.isDateSet ||
  8081. !( // NOT within interval range signals an implicit date window change
  8082. date >= currentView.intervalStart &&
  8083. date < currentView.intervalEnd
  8084. )
  8085. ) {
  8086. if (elementVisible()) {
  8087. if (forcedScroll) {
  8088. currentView.captureInitialScroll(forcedScroll);
  8089. }
  8090. currentView.setDate(date, forcedScroll);
  8091. if (forcedScroll) {
  8092. currentView.releaseScroll();
  8093. }
  8094. // need to do this after View::render, so dates are calculated
  8095. // NOTE: view updates title text proactively
  8096. updateToolbarsTodayButton();
  8097. }
  8098. }
  8099. }
  8100. if (needsClearView) {
  8101. thawContentHeight();
  8102. }
  8103. ignoreWindowResize--;
  8104. }
  8105. // Unrenders the current view and reflects this change in the Header.
  8106. // Unregsiters the `currentView`, but does not remove from viewByType hash.
  8107. function clearView() {
  8108. toolbarsManager.proxyCall('deactivateButton', currentView.type);
  8109. currentView.removeElement();
  8110. currentView = t.view = null;
  8111. }
  8112. // Destroys the view, including the view object. Then, re-instantiates it and renders it.
  8113. // Maintains the same scroll state.
  8114. // TODO: maintain any other user-manipulated state.
  8115. function reinitView() {
  8116. ignoreWindowResize++;
  8117. freezeContentHeight();
  8118. var viewType = currentView.type;
  8119. var scrollState = currentView.queryScroll();
  8120. clearView();
  8121. calcSize();
  8122. renderView(viewType, scrollState);
  8123. thawContentHeight();
  8124. ignoreWindowResize--;
  8125. }
  8126. // Resizing
  8127. // -----------------------------------------------------------------------------------
  8128. t.getSuggestedViewHeight = function() {
  8129. if (suggestedViewHeight === undefined) {
  8130. calcSize();
  8131. }
  8132. return suggestedViewHeight;
  8133. };
  8134. t.isHeightAuto = function() {
  8135. return t.options.contentHeight === 'auto' || t.options.height === 'auto';
  8136. };
  8137. function updateSize(shouldRecalc) {
  8138. if (elementVisible()) {
  8139. if (shouldRecalc) {
  8140. _calcSize();
  8141. }
  8142. ignoreWindowResize++;
  8143. currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  8144. ignoreWindowResize--;
  8145. return true; // signal success
  8146. }
  8147. }
  8148. function calcSize() {
  8149. if (elementVisible()) {
  8150. _calcSize();
  8151. }
  8152. }
  8153. function _calcSize() { // assumes elementVisible
  8154. var contentHeightInput = t.options.contentHeight;
  8155. var heightInput = t.options.height;
  8156. if (typeof contentHeightInput === 'number') { // exists and not 'auto'
  8157. suggestedViewHeight = contentHeightInput;
  8158. }
  8159. else if (typeof contentHeightInput === 'function') { // exists and is a function
  8160. suggestedViewHeight = contentHeightInput();
  8161. }
  8162. else if (typeof heightInput === 'number') { // exists and not 'auto'
  8163. suggestedViewHeight = heightInput - queryToolbarsHeight();
  8164. }
  8165. else if (typeof heightInput === 'function') { // exists and is a function
  8166. suggestedViewHeight = heightInput() - queryToolbarsHeight();
  8167. }
  8168. else if (heightInput === 'parent') { // set to height of parent element
  8169. suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
  8170. }
  8171. else {
  8172. suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
  8173. }
  8174. }
  8175. function queryToolbarsHeight() {
  8176. return toolbarsManager.items.reduce(function(accumulator, toolbar) {
  8177. var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
  8178. return accumulator + toolbarHeight;
  8179. }, 0);
  8180. }
  8181. function windowResize(ev) {
  8182. if (
  8183. !ignoreWindowResize &&
  8184. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  8185. currentView.start // view has already been rendered
  8186. ) {
  8187. if (updateSize(true)) {
  8188. currentView.publiclyTrigger('windowResize', _element);
  8189. }
  8190. }
  8191. }
  8192. /* Event Rendering
  8193. -----------------------------------------------------------------------------*/
  8194. function rerenderEvents() { // API method. destroys old events if previously rendered.
  8195. if (elementVisible()) {
  8196. t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
  8197. }
  8198. }
  8199. /* Toolbars
  8200. -----------------------------------------------------------------------------*/
  8201. function buildToolbars() {
  8202. return [
  8203. new Toolbar(t, computeHeaderOptions()),
  8204. new Toolbar(t, computeFooterOptions())
  8205. ];
  8206. }
  8207. function computeHeaderOptions() {
  8208. return {
  8209. extraClasses: 'fc-header-toolbar',
  8210. layout: t.options.header
  8211. };
  8212. }
  8213. function computeFooterOptions() {
  8214. return {
  8215. extraClasses: 'fc-footer-toolbar',
  8216. layout: t.options.footer
  8217. };
  8218. }
  8219. // can be called repeatedly and Header will rerender
  8220. function renderHeader() {
  8221. header.setToolbarOptions(computeHeaderOptions());
  8222. header.render();
  8223. if (header.el) {
  8224. element.prepend(header.el);
  8225. }
  8226. }
  8227. // can be called repeatedly and Footer will rerender
  8228. function renderFooter() {
  8229. footer.setToolbarOptions(computeFooterOptions());
  8230. footer.render();
  8231. if (footer.el) {
  8232. element.append(footer.el);
  8233. }
  8234. }
  8235. t.setToolbarsTitle = function(title) {
  8236. toolbarsManager.proxyCall('updateTitle', title);
  8237. };
  8238. function updateToolbarsTodayButton() {
  8239. var now = t.getNow();
  8240. if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
  8241. toolbarsManager.proxyCall('disableButton', 'today');
  8242. }
  8243. else {
  8244. toolbarsManager.proxyCall('enableButton', 'today');
  8245. }
  8246. }
  8247. /* Selection
  8248. -----------------------------------------------------------------------------*/
  8249. // this public method receives start/end dates in any format, with any timezone
  8250. function select(zonedStartInput, zonedEndInput) {
  8251. currentView.select(
  8252. t.buildSelectSpan.apply(t, arguments)
  8253. );
  8254. }
  8255. function unselect() { // safe to be called before renderView
  8256. if (currentView) {
  8257. currentView.unselect();
  8258. }
  8259. }
  8260. /* Date
  8261. -----------------------------------------------------------------------------*/
  8262. function prev() {
  8263. date = currentView.computePrevDate(date);
  8264. renderView();
  8265. }
  8266. function next() {
  8267. date = currentView.computeNextDate(date);
  8268. renderView();
  8269. }
  8270. function prevYear() {
  8271. date.add(-1, 'years');
  8272. renderView();
  8273. }
  8274. function nextYear() {
  8275. date.add(1, 'years');
  8276. renderView();
  8277. }
  8278. function today() {
  8279. date = t.getNow();
  8280. renderView();
  8281. }
  8282. function gotoDate(zonedDateInput) {
  8283. date = t.moment(zonedDateInput).stripZone();
  8284. renderView();
  8285. }
  8286. function incrementDate(delta) {
  8287. date.add(moment.duration(delta));
  8288. renderView();
  8289. }
  8290. // Forces navigation to a view for the given date.
  8291. // `viewType` can be a specific view name or a generic one like "week" or "day".
  8292. function zoomTo(newDate, viewType) {
  8293. var spec;
  8294. viewType = viewType || 'day'; // day is default zoom
  8295. spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  8296. date = newDate.clone();
  8297. renderView(spec ? spec.type : null);
  8298. }
  8299. // for external API
  8300. function getDate() {
  8301. return t.applyTimezone(date); // infuse the calendar's timezone
  8302. }
  8303. /* Height "Freezing"
  8304. -----------------------------------------------------------------------------*/
  8305. t.freezeContentHeight = freezeContentHeight;
  8306. t.thawContentHeight = thawContentHeight;
  8307. var freezeContentHeightDepth = 0;
  8308. function freezeContentHeight() {
  8309. if (!(freezeContentHeightDepth++)) {
  8310. content.css({
  8311. width: '100%',
  8312. height: content.height(),
  8313. overflow: 'hidden'
  8314. });
  8315. }
  8316. }
  8317. function thawContentHeight() {
  8318. if (!(--freezeContentHeightDepth)) {
  8319. content.css({
  8320. width: '',
  8321. height: '',
  8322. overflow: ''
  8323. });
  8324. }
  8325. }
  8326. /* Misc
  8327. -----------------------------------------------------------------------------*/
  8328. function getCalendar() {
  8329. return t;
  8330. }
  8331. function getView() {
  8332. return currentView;
  8333. }
  8334. function option(name, value) {
  8335. var newOptionHash;
  8336. if (typeof name === 'string') {
  8337. if (value === undefined) { // getter
  8338. return t.options[name];
  8339. }
  8340. else { // setter for individual option
  8341. newOptionHash = {};
  8342. newOptionHash[name] = value;
  8343. setOptions(newOptionHash);
  8344. }
  8345. }
  8346. else if (typeof name === 'object') { // compound setter with object input
  8347. setOptions(name);
  8348. }
  8349. }
  8350. function setOptions(newOptionHash) {
  8351. var optionCnt = 0;
  8352. var optionName;
  8353. for (optionName in newOptionHash) {
  8354. t.dynamicOverrides[optionName] = newOptionHash[optionName];
  8355. }
  8356. t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
  8357. t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
  8358. // trigger handlers after this.options has been updated
  8359. for (optionName in newOptionHash) {
  8360. t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
  8361. optionCnt++;
  8362. }
  8363. // special-case handling of single option change.
  8364. // if only one option change, `optionName` will be its name.
  8365. if (optionCnt === 1) {
  8366. if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
  8367. updateSize(true); // true = allow recalculation of height
  8368. return;
  8369. }
  8370. else if (optionName === 'defaultDate') {
  8371. return; // can't change date this way. use gotoDate instead
  8372. }
  8373. else if (optionName === 'businessHours') {
  8374. if (currentView) {
  8375. currentView.unrenderBusinessHours();
  8376. currentView.renderBusinessHours();
  8377. }
  8378. return;
  8379. }
  8380. else if (optionName === 'timezone') {
  8381. t.rezoneArrayEventSources();
  8382. t.refetchEvents();
  8383. return;
  8384. }
  8385. }
  8386. // catch-all. rerender the header and footer and rebuild/rerender the current view
  8387. renderHeader();
  8388. renderFooter();
  8389. viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
  8390. reinitView();
  8391. }
  8392. function publiclyTrigger(name, thisObj) {
  8393. var args = Array.prototype.slice.call(arguments, 2);
  8394. thisObj = thisObj || _element;
  8395. this.triggerWith(name, thisObj, args); // Emitter's method
  8396. if (t.options[name]) {
  8397. return t.options[name].apply(thisObj, args);
  8398. }
  8399. }
  8400. t.initialize();
  8401. }
  8402. ;;
  8403. /*
  8404. Options binding/triggering system.
  8405. */
  8406. Calendar.mixin({
  8407. // A map of option names to arrays of handler objects. Initialized to {} in Calendar.
  8408. // Format for a handler object:
  8409. // {
  8410. // func // callback function to be called upon change
  8411. // names // option names whose values should be given to func
  8412. // }
  8413. optionHandlers: null,
  8414. // Calls handlerFunc immediately, and when the given option has changed.
  8415. // handlerFunc will be given the option value.
  8416. bindOption: function(optionName, handlerFunc) {
  8417. this.bindOptions([ optionName ], handlerFunc);
  8418. },
  8419. // Calls handlerFunc immediately, and when any of the given options change.
  8420. // handlerFunc will be given each option value as ordered function arguments.
  8421. bindOptions: function(optionNames, handlerFunc) {
  8422. var handlerObj = { func: handlerFunc, names: optionNames };
  8423. var i;
  8424. for (i = 0; i < optionNames.length; i++) {
  8425. this.registerOptionHandlerObj(optionNames[i], handlerObj);
  8426. }
  8427. this.triggerOptionHandlerObj(handlerObj);
  8428. },
  8429. // Puts the given handler object into the internal hash
  8430. registerOptionHandlerObj: function(optionName, handlerObj) {
  8431. (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = []))
  8432. .push(handlerObj);
  8433. },
  8434. // Reports that the given option has changed, and calls all appropriate handlers.
  8435. triggerOptionHandlers: function(optionName) {
  8436. var handlerObjs = this.optionHandlers[optionName] || [];
  8437. var i;
  8438. for (i = 0; i < handlerObjs.length; i++) {
  8439. this.triggerOptionHandlerObj(handlerObjs[i]);
  8440. }
  8441. },
  8442. // Calls the callback for a specific handler object, passing in the appropriate arguments.
  8443. triggerOptionHandlerObj: function(handlerObj) {
  8444. var optionNames = handlerObj.names;
  8445. var optionValues = [];
  8446. var i;
  8447. for (i = 0; i < optionNames.length; i++) {
  8448. optionValues.push(this.options[optionNames[i]]);
  8449. }
  8450. handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context
  8451. }
  8452. });
  8453. ;;
  8454. Calendar.defaults = {
  8455. titleRangeSeparator: ' \u2013 ', // en dash
  8456. monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
  8457. defaultTimedEventDuration: '02:00:00',
  8458. defaultAllDayEventDuration: { days: 1 },
  8459. forceEventDuration: false,
  8460. nextDayThreshold: '09:00:00', // 9am
  8461. // display
  8462. defaultView: 'month',
  8463. aspectRatio: 1.35,
  8464. header: {
  8465. left: 'title',
  8466. center: '',
  8467. right: 'today prev,next'
  8468. },
  8469. weekends: true,
  8470. weekNumbers: false,
  8471. weekNumberTitle: 'W',
  8472. weekNumberCalculation: 'local',
  8473. //editable: false,
  8474. //nowIndicator: false,
  8475. scrollTime: '06:00:00',
  8476. // event ajax
  8477. lazyFetching: true,
  8478. startParam: 'start',
  8479. endParam: 'end',
  8480. timezoneParam: 'timezone',
  8481. timezone: false,
  8482. //allDayDefault: undefined,
  8483. // locale
  8484. isRTL: false,
  8485. buttonText: {
  8486. prev: "prev",
  8487. next: "next",
  8488. prevYear: "prev year",
  8489. nextYear: "next year",
  8490. year: 'year', // TODO: locale files need to specify this
  8491. today: 'today',
  8492. month: 'month',
  8493. week: 'week',
  8494. day: 'day'
  8495. },
  8496. buttonIcons: {
  8497. prev: 'left-single-arrow',
  8498. next: 'right-single-arrow',
  8499. prevYear: 'left-double-arrow',
  8500. nextYear: 'right-double-arrow'
  8501. },
  8502. allDayText: 'all-day',
  8503. // jquery-ui theming
  8504. theme: false,
  8505. themeButtonIcons: {
  8506. prev: 'circle-triangle-w',
  8507. next: 'circle-triangle-e',
  8508. prevYear: 'seek-prev',
  8509. nextYear: 'seek-next'
  8510. },
  8511. //eventResizableFromStart: false,
  8512. dragOpacity: .75,
  8513. dragRevertDuration: 500,
  8514. dragScroll: true,
  8515. //selectable: false,
  8516. unselectAuto: true,
  8517. dropAccept: '*',
  8518. eventOrder: 'title',
  8519. //eventRenderWait: null,
  8520. eventLimit: false,
  8521. eventLimitText: 'more',
  8522. eventLimitClick: 'popover',
  8523. dayPopoverFormat: 'LL',
  8524. handleWindowResize: true,
  8525. windowResizeDelay: 100, // milliseconds before an updateSize happens
  8526. longPressDelay: 1000
  8527. };
  8528. Calendar.englishDefaults = { // used by locale.js
  8529. dayPopoverFormat: 'dddd, MMMM D'
  8530. };
  8531. Calendar.rtlDefaults = { // right-to-left defaults
  8532. header: { // TODO: smarter solution (first/center/last ?)
  8533. left: 'next,prev today',
  8534. center: '',
  8535. right: 'title'
  8536. },
  8537. buttonIcons: {
  8538. prev: 'right-single-arrow',
  8539. next: 'left-single-arrow',
  8540. prevYear: 'right-double-arrow',
  8541. nextYear: 'left-double-arrow'
  8542. },
  8543. themeButtonIcons: {
  8544. prev: 'circle-triangle-e',
  8545. next: 'circle-triangle-w',
  8546. nextYear: 'seek-prev',
  8547. prevYear: 'seek-next'
  8548. }
  8549. };
  8550. ;;
  8551. var localeOptionHash = FC.locales = {}; // initialize and expose
  8552. // TODO: document the structure and ordering of a FullCalendar locale file
  8553. // Initialize jQuery UI datepicker translations while using some of the translations
  8554. // Will set this as the default locales for datepicker.
  8555. FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
  8556. // get the FullCalendar internal option hash for this locale. create if necessary
  8557. var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
  8558. // transfer some simple options from datepicker to fc
  8559. fcOptions.isRTL = dpOptions.isRTL;
  8560. fcOptions.weekNumberTitle = dpOptions.weekHeader;
  8561. // compute some more complex options from datepicker
  8562. $.each(dpComputableOptions, function(name, func) {
  8563. fcOptions[name] = func(dpOptions);
  8564. });
  8565. // is jQuery UI Datepicker is on the page?
  8566. if ($.datepicker) {
  8567. // Register the locale data.
  8568. // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
  8569. // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
  8570. // Make an alias so the locale can be referenced either way.
  8571. $.datepicker.regional[dpLocaleCode] =
  8572. $.datepicker.regional[localeCode] = // alias
  8573. dpOptions;
  8574. // Alias 'en' to the default locale data. Do this every time.
  8575. $.datepicker.regional.en = $.datepicker.regional[''];
  8576. // Set as Datepicker's global defaults.
  8577. $.datepicker.setDefaults(dpOptions);
  8578. }
  8579. };
  8580. // Sets FullCalendar-specific translations. Will set the locales as the global default.
  8581. FC.locale = function(localeCode, newFcOptions) {
  8582. var fcOptions;
  8583. var momOptions;
  8584. // get the FullCalendar internal option hash for this locale. create if necessary
  8585. fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
  8586. // provided new options for this locales? merge them in
  8587. if (newFcOptions) {
  8588. fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]);
  8589. }
  8590. // compute locale options that weren't defined.
  8591. // always do this. newFcOptions can be undefined when initializing from i18n file,
  8592. // so no way to tell if this is an initialization or a default-setting.
  8593. momOptions = getMomentLocaleData(localeCode); // will fall back to en
  8594. $.each(momComputableOptions, function(name, func) {
  8595. if (fcOptions[name] == null) {
  8596. fcOptions[name] = func(momOptions, fcOptions);
  8597. }
  8598. });
  8599. // set it as the default locale for FullCalendar
  8600. Calendar.defaults.locale = localeCode;
  8601. };
  8602. // NOTE: can't guarantee any of these computations will run because not every locale has datepicker
  8603. // configs, so make sure there are English fallbacks for these in the defaults file.
  8604. var dpComputableOptions = {
  8605. buttonText: function(dpOptions) {
  8606. return {
  8607. // the translations sometimes wrongly contain HTML entities
  8608. prev: stripHtmlEntities(dpOptions.prevText),
  8609. next: stripHtmlEntities(dpOptions.nextText),
  8610. today: stripHtmlEntities(dpOptions.currentText)
  8611. };
  8612. },
  8613. // Produces format strings like "MMMM YYYY" -> "September 2014"
  8614. monthYearFormat: function(dpOptions) {
  8615. return dpOptions.showMonthAfterYear ?
  8616. 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
  8617. 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
  8618. }
  8619. };
  8620. var momComputableOptions = {
  8621. // Produces format strings like "ddd M/D" -> "Fri 9/15"
  8622. dayOfMonthFormat: function(momOptions, fcOptions) {
  8623. var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
  8624. // strip the year off the edge, as well as other misc non-whitespace chars
  8625. format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
  8626. if (fcOptions.isRTL) {
  8627. format += ' ddd'; // for RTL, add day-of-week to end
  8628. }
  8629. else {
  8630. format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  8631. }
  8632. return format;
  8633. },
  8634. // Produces format strings like "h:mma" -> "6:00pm"
  8635. mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
  8636. return momOptions.longDateFormat('LT')
  8637. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8638. },
  8639. // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
  8640. smallTimeFormat: function(momOptions) {
  8641. return momOptions.longDateFormat('LT')
  8642. .replace(':mm', '(:mm)')
  8643. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
  8644. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8645. },
  8646. // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
  8647. extraSmallTimeFormat: function(momOptions) {
  8648. return momOptions.longDateFormat('LT')
  8649. .replace(':mm', '(:mm)')
  8650. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
  8651. .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  8652. },
  8653. // Produces format strings like "ha" / "H" -> "6pm" / "18"
  8654. hourFormat: function(momOptions) {
  8655. return momOptions.longDateFormat('LT')
  8656. .replace(':mm', '')
  8657. .replace(/(\Wmm)$/, '') // like above, but for foreign locales
  8658. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8659. },
  8660. // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
  8661. noMeridiemTimeFormat: function(momOptions) {
  8662. return momOptions.longDateFormat('LT')
  8663. .replace(/\s*a$/i, ''); // remove trailing AM/PM
  8664. }
  8665. };
  8666. // options that should be computed off live calendar options (considers override options)
  8667. // TODO: best place for this? related to locale?
  8668. // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
  8669. var instanceComputableOptions = {
  8670. // Produces format strings for results like "Mo 16"
  8671. smallDayDateFormat: function(options) {
  8672. return options.isRTL ?
  8673. 'D dd' :
  8674. 'dd D';
  8675. },
  8676. // Produces format strings for results like "Wk 5"
  8677. weekFormat: function(options) {
  8678. return options.isRTL ?
  8679. 'w[ ' + options.weekNumberTitle + ']' :
  8680. '[' + options.weekNumberTitle + ' ]w';
  8681. },
  8682. // Produces format strings for results like "Wk5"
  8683. smallWeekFormat: function(options) {
  8684. return options.isRTL ?
  8685. 'w[' + options.weekNumberTitle + ']' :
  8686. '[' + options.weekNumberTitle + ']w';
  8687. }
  8688. };
  8689. function populateInstanceComputableOptions(options) {
  8690. $.each(instanceComputableOptions, function(name, func) {
  8691. if (options[name] == null) {
  8692. options[name] = func(options);
  8693. }
  8694. });
  8695. }
  8696. // Returns moment's internal locale data. If doesn't exist, returns English.
  8697. function getMomentLocaleData(localeCode) {
  8698. return moment.localeData(localeCode) || moment.localeData('en');
  8699. }
  8700. // Initialize English by forcing computation of moment-derived options.
  8701. // Also, sets it as the default.
  8702. FC.locale('en', Calendar.englishDefaults);
  8703. ;;
  8704. FC.sourceNormalizers = [];
  8705. FC.sourceFetchers = [];
  8706. var ajaxDefaults = {
  8707. dataType: 'json',
  8708. cache: false
  8709. };
  8710. var eventGUID = 1;
  8711. function EventManager() { // assumed to be a calendar
  8712. var t = this;
  8713. // exports
  8714. t.requestEvents = requestEvents;
  8715. t.reportEventChange = reportEventChange;
  8716. t.isFetchNeeded = isFetchNeeded;
  8717. t.fetchEvents = fetchEvents;
  8718. t.fetchEventSources = fetchEventSources;
  8719. t.refetchEvents = refetchEvents;
  8720. t.refetchEventSources = refetchEventSources;
  8721. t.getEventSources = getEventSources;
  8722. t.getEventSourceById = getEventSourceById;
  8723. t.addEventSource = addEventSource;
  8724. t.removeEventSource = removeEventSource;
  8725. t.removeEventSources = removeEventSources;
  8726. t.updateEvent = updateEvent;
  8727. t.updateEvents = updateEvents;
  8728. t.renderEvent = renderEvent;
  8729. t.renderEvents = renderEvents;
  8730. t.removeEvents = removeEvents;
  8731. t.clientEvents = clientEvents;
  8732. t.mutateEvent = mutateEvent;
  8733. t.normalizeEventDates = normalizeEventDates;
  8734. t.normalizeEventTimes = normalizeEventTimes;
  8735. // locals
  8736. var stickySource = { events: [] };
  8737. var sources = [ stickySource ];
  8738. var rangeStart, rangeEnd;
  8739. var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
  8740. var cache = []; // holds events that have already been expanded
  8741. var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd
  8742. $.each(
  8743. (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []),
  8744. function(i, sourceInput) {
  8745. var source = buildEventSource(sourceInput);
  8746. if (source) {
  8747. sources.push(source);
  8748. }
  8749. }
  8750. );
  8751. function requestEvents(start, end) {
  8752. if (!t.options.lazyFetching || isFetchNeeded(start, end)) {
  8753. return fetchEvents(start, end);
  8754. }
  8755. else {
  8756. return Promise.resolve(prunedCache);
  8757. }
  8758. }
  8759. function reportEventChange() {
  8760. prunedCache = filterEventsWithinRange(cache);
  8761. t.trigger('eventsReset', prunedCache);
  8762. }
  8763. function filterEventsWithinRange(events) {
  8764. var filteredEvents = [];
  8765. var i, event;
  8766. for (i = 0; i < events.length; i++) {
  8767. event = events[i];
  8768. if (
  8769. event.start.clone().stripZone() < rangeEnd &&
  8770. t.getEventEnd(event).stripZone() > rangeStart
  8771. ) {
  8772. filteredEvents.push(event);
  8773. }
  8774. }
  8775. return filteredEvents;
  8776. }
  8777. t.getEventCache = function() {
  8778. return cache;
  8779. };
  8780. t.getPrunedEventCache = function() {
  8781. return prunedCache;
  8782. };
  8783. /* Fetching
  8784. -----------------------------------------------------------------------------*/
  8785. // start and end are assumed to be unzoned
  8786. function isFetchNeeded(start, end) {
  8787. return !rangeStart || // nothing has been fetched yet?
  8788. start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
  8789. }
  8790. function fetchEvents(start, end) {
  8791. rangeStart = start;
  8792. rangeEnd = end;
  8793. return refetchEvents();
  8794. }
  8795. // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`.
  8796. function refetchEvents() {
  8797. return fetchEventSources(sources, 'reset');
  8798. }
  8799. // poorly named. fetches a subset of event sources.
  8800. function refetchEventSources(matchInputs) {
  8801. return fetchEventSources(getEventSourcesByMatchArray(matchInputs));
  8802. }
  8803. // expects an array of event source objects (the originals, not copies)
  8804. // `specialFetchType` is an optimization parameter that affects purging of the event cache.
  8805. function fetchEventSources(specificSources, specialFetchType) {
  8806. var i, source;
  8807. if (specialFetchType === 'reset') {
  8808. cache = [];
  8809. }
  8810. else if (specialFetchType !== 'add') {
  8811. cache = excludeEventsBySources(cache, specificSources);
  8812. }
  8813. for (i = 0; i < specificSources.length; i++) {
  8814. source = specificSources[i];
  8815. // already-pending sources have already been accounted for in pendingSourceCnt
  8816. if (source._status !== 'pending') {
  8817. pendingSourceCnt++;
  8818. }
  8819. source._fetchId = (source._fetchId || 0) + 1;
  8820. source._status = 'pending';
  8821. }
  8822. for (i = 0; i < specificSources.length; i++) {
  8823. source = specificSources[i];
  8824. tryFetchEventSource(source, source._fetchId);
  8825. }
  8826. if (pendingSourceCnt) {
  8827. return new Promise(function(resolve) {
  8828. t.one('eventsReceived', resolve); // will send prunedCache
  8829. });
  8830. }
  8831. else { // executed all synchronously, or no sources at all
  8832. return Promise.resolve(prunedCache);
  8833. }
  8834. }
  8835. // fetches an event source and processes its result ONLY if it is still the current fetch.
  8836. // caller is responsible for incrementing pendingSourceCnt first.
  8837. function tryFetchEventSource(source, fetchId) {
  8838. _fetchEventSource(source, function(eventInputs) {
  8839. var isArraySource = $.isArray(source.events);
  8840. var i, eventInput;
  8841. var abstractEvent;
  8842. if (
  8843. // is this the source's most recent fetch?
  8844. // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
  8845. fetchId === source._fetchId &&
  8846. // event source no longer valid?
  8847. source._status !== 'rejected'
  8848. ) {
  8849. source._status = 'resolved';
  8850. if (eventInputs) {
  8851. for (i = 0; i < eventInputs.length; i++) {
  8852. eventInput = eventInputs[i];
  8853. if (isArraySource) { // array sources have already been convert to Event Objects
  8854. abstractEvent = eventInput;
  8855. }
  8856. else {
  8857. abstractEvent = buildEventFromInput(eventInput, source);
  8858. }
  8859. if (abstractEvent) { // not false (an invalid event)
  8860. cache.push.apply( // append
  8861. cache,
  8862. expandEvent(abstractEvent) // add individual expanded events to the cache
  8863. );
  8864. }
  8865. }
  8866. }
  8867. decrementPendingSourceCnt();
  8868. }
  8869. });
  8870. }
  8871. function rejectEventSource(source) {
  8872. var wasPending = source._status === 'pending';
  8873. source._status = 'rejected';
  8874. if (wasPending) {
  8875. decrementPendingSourceCnt();
  8876. }
  8877. }
  8878. function decrementPendingSourceCnt() {
  8879. pendingSourceCnt--;
  8880. if (!pendingSourceCnt) {
  8881. reportEventChange(cache); // updates prunedCache
  8882. t.trigger('eventsReceived', prunedCache);
  8883. }
  8884. }
  8885. function _fetchEventSource(source, callback) {
  8886. var i;
  8887. var fetchers = FC.sourceFetchers;
  8888. var res;
  8889. for (i=0; i<fetchers.length; i++) {
  8890. res = fetchers[i].call(
  8891. t, // this, the Calendar object
  8892. source,
  8893. rangeStart.clone(),
  8894. rangeEnd.clone(),
  8895. t.options.timezone,
  8896. callback
  8897. );
  8898. if (res === true) {
  8899. // the fetcher is in charge. made its own async request
  8900. return;
  8901. }
  8902. else if (typeof res == 'object') {
  8903. // the fetcher returned a new source. process it
  8904. _fetchEventSource(res, callback);
  8905. return;
  8906. }
  8907. }
  8908. var events = source.events;
  8909. if (events) {
  8910. if ($.isFunction(events)) {
  8911. t.pushLoading();
  8912. events.call(
  8913. t, // this, the Calendar object
  8914. rangeStart.clone(),
  8915. rangeEnd.clone(),
  8916. t.options.timezone,
  8917. function(events) {
  8918. callback(events);
  8919. t.popLoading();
  8920. }
  8921. );
  8922. }
  8923. else if ($.isArray(events)) {
  8924. callback(events);
  8925. }
  8926. else {
  8927. callback();
  8928. }
  8929. }else{
  8930. var url = source.url;
  8931. if (url) {
  8932. var success = source.success;
  8933. var error = source.error;
  8934. var complete = source.complete;
  8935. // retrieve any outbound GET/POST $.ajax data from the options
  8936. var customData;
  8937. if ($.isFunction(source.data)) {
  8938. // supplied as a function that returns a key/value object
  8939. customData = source.data();
  8940. }
  8941. else {
  8942. // supplied as a straight key/value object
  8943. customData = source.data;
  8944. }
  8945. // use a copy of the custom data so we can modify the parameters
  8946. // and not affect the passed-in object.
  8947. var data = $.extend({}, customData || {});
  8948. var startParam = firstDefined(source.startParam, t.options.startParam);
  8949. var endParam = firstDefined(source.endParam, t.options.endParam);
  8950. var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
  8951. if (startParam) {
  8952. data[startParam] = rangeStart.format();
  8953. }
  8954. if (endParam) {
  8955. data[endParam] = rangeEnd.format();
  8956. }
  8957. if (t.options.timezone && t.options.timezone != 'local') {
  8958. data[timezoneParam] = t.options.timezone;
  8959. }
  8960. t.pushLoading();
  8961. $.ajax($.extend({}, ajaxDefaults, source, {
  8962. data: data,
  8963. success: function(events) {
  8964. events = events || [];
  8965. var res = applyAll(success, this, arguments);
  8966. if ($.isArray(res)) {
  8967. events = res;
  8968. }
  8969. callback(events);
  8970. },
  8971. error: function() {
  8972. applyAll(error, this, arguments);
  8973. callback();
  8974. },
  8975. complete: function() {
  8976. applyAll(complete, this, arguments);
  8977. t.popLoading();
  8978. }
  8979. }));
  8980. }else{
  8981. callback();
  8982. }
  8983. }
  8984. }
  8985. /* Sources
  8986. -----------------------------------------------------------------------------*/
  8987. function addEventSource(sourceInput) {
  8988. var source = buildEventSource(sourceInput);
  8989. if (source) {
  8990. sources.push(source);
  8991. fetchEventSources([ source ], 'add'); // will eventually call reportEventChange
  8992. }
  8993. }
  8994. function buildEventSource(sourceInput) { // will return undefined if invalid source
  8995. var normalizers = FC.sourceNormalizers;
  8996. var source;
  8997. var i;
  8998. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  8999. source = { events: sourceInput };
  9000. }
  9001. else if (typeof sourceInput === 'string') {
  9002. source = { url: sourceInput };
  9003. }
  9004. else if (typeof sourceInput === 'object') {
  9005. source = $.extend({}, sourceInput); // shallow copy
  9006. }
  9007. if (source) {
  9008. // TODO: repeat code, same code for event classNames
  9009. if (source.className) {
  9010. if (typeof source.className === 'string') {
  9011. source.className = source.className.split(/\s+/);
  9012. }
  9013. // otherwise, assumed to be an array
  9014. }
  9015. else {
  9016. source.className = [];
  9017. }
  9018. // for array sources, we convert to standard Event Objects up front
  9019. if ($.isArray(source.events)) {
  9020. source.origArray = source.events; // for removeEventSource
  9021. source.events = $.map(source.events, function(eventInput) {
  9022. return buildEventFromInput(eventInput, source);
  9023. });
  9024. }
  9025. for (i=0; i<normalizers.length; i++) {
  9026. normalizers[i].call(t, source);
  9027. }
  9028. return source;
  9029. }
  9030. }
  9031. function removeEventSource(matchInput) {
  9032. removeSpecificEventSources(
  9033. getEventSourcesByMatch(matchInput)
  9034. );
  9035. }
  9036. // if called with no arguments, removes all.
  9037. function removeEventSources(matchInputs) {
  9038. if (matchInputs == null) {
  9039. removeSpecificEventSources(sources, true); // isAll=true
  9040. }
  9041. else {
  9042. removeSpecificEventSources(
  9043. getEventSourcesByMatchArray(matchInputs)
  9044. );
  9045. }
  9046. }
  9047. function removeSpecificEventSources(targetSources, isAll) {
  9048. var i;
  9049. // cancel pending requests
  9050. for (i = 0; i < targetSources.length; i++) {
  9051. rejectEventSource(targetSources[i]);
  9052. }
  9053. if (isAll) { // an optimization
  9054. sources = [];
  9055. cache = [];
  9056. }
  9057. else {
  9058. // remove from persisted source list
  9059. sources = $.grep(sources, function(source) {
  9060. for (i = 0; i < targetSources.length; i++) {
  9061. if (source === targetSources[i]) {
  9062. return false; // exclude
  9063. }
  9064. }
  9065. return true; // include
  9066. });
  9067. cache = excludeEventsBySources(cache, targetSources);
  9068. }
  9069. reportEventChange();
  9070. }
  9071. function getEventSources() {
  9072. return sources.slice(1); // returns a shallow copy of sources with stickySource removed
  9073. }
  9074. function getEventSourceById(id) {
  9075. return $.grep(sources, function(source) {
  9076. return source.id && source.id === id;
  9077. })[0];
  9078. }
  9079. // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
  9080. function getEventSourcesByMatchArray(matchInputs) {
  9081. // coerce into an array
  9082. if (!matchInputs) {
  9083. matchInputs = [];
  9084. }
  9085. else if (!$.isArray(matchInputs)) {
  9086. matchInputs = [ matchInputs ];
  9087. }
  9088. var matchingSources = [];
  9089. var i;
  9090. // resolve raw inputs to real event source objects
  9091. for (i = 0; i < matchInputs.length; i++) {
  9092. matchingSources.push.apply( // append
  9093. matchingSources,
  9094. getEventSourcesByMatch(matchInputs[i])
  9095. );
  9096. }
  9097. return matchingSources;
  9098. }
  9099. // matchInput can either by a real event source object, an ID, or the function/URL for the source.
  9100. // returns an array of matching source objects.
  9101. function getEventSourcesByMatch(matchInput) {
  9102. var i, source;
  9103. // given an proper event source object
  9104. for (i = 0; i < sources.length; i++) {
  9105. source = sources[i];
  9106. if (source === matchInput) {
  9107. return [ source ];
  9108. }
  9109. }
  9110. // an ID match
  9111. source = getEventSourceById(matchInput);
  9112. if (source) {
  9113. return [ source ];
  9114. }
  9115. return $.grep(sources, function(source) {
  9116. return isSourcesEquivalent(matchInput, source);
  9117. });
  9118. }
  9119. function isSourcesEquivalent(source1, source2) {
  9120. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  9121. }
  9122. function getSourcePrimitive(source) {
  9123. return (
  9124. (typeof source === 'object') ? // a normalized event source?
  9125. (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  9126. null
  9127. ) ||
  9128. source; // the given argument *is* the primitive
  9129. }
  9130. // util
  9131. // returns a filtered array without events that are part of any of the given sources
  9132. function excludeEventsBySources(specificEvents, specificSources) {
  9133. return $.grep(specificEvents, function(event) {
  9134. for (var i = 0; i < specificSources.length; i++) {
  9135. if (event.source === specificSources[i]) {
  9136. return false; // exclude
  9137. }
  9138. }
  9139. return true; // keep
  9140. });
  9141. }
  9142. /* Manipulation
  9143. -----------------------------------------------------------------------------*/
  9144. // Only ever called from the externally-facing API
  9145. function updateEvent(event) {
  9146. updateEvents([ event ]);
  9147. }
  9148. // Only ever called from the externally-facing API
  9149. function updateEvents(events) {
  9150. var i, event;
  9151. for (i = 0; i < events.length; i++) {
  9152. event = events[i];
  9153. // massage start/end values, even if date string values
  9154. event.start = t.moment(event.start);
  9155. if (event.end) {
  9156. event.end = t.moment(event.end);
  9157. }
  9158. else {
  9159. event.end = null;
  9160. }
  9161. mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  9162. }
  9163. reportEventChange(); // reports event modifications (so we can redraw)
  9164. }
  9165. // Returns a hash of misc event properties that should be copied over to related events.
  9166. function getMiscEventProps(event) {
  9167. var props = {};
  9168. $.each(event, function(name, val) {
  9169. if (isMiscEventPropName(name)) {
  9170. if (val !== undefined && isAtomic(val)) { // a defined non-object
  9171. props[name] = val;
  9172. }
  9173. }
  9174. });
  9175. return props;
  9176. }
  9177. // non-date-related, non-id-related, non-secret
  9178. function isMiscEventPropName(name) {
  9179. return !/^_|^(id|allDay|start|end)$/.test(name);
  9180. }
  9181. // returns the expanded events that were created
  9182. function renderEvent(eventInput, stick) {
  9183. return renderEvents([ eventInput ], stick);
  9184. }
  9185. // returns the expanded events that were created
  9186. function renderEvents(eventInputs, stick) {
  9187. var renderedEvents = [];
  9188. var renderableEvents;
  9189. var abstractEvent;
  9190. var i, j, event;
  9191. for (i = 0; i < eventInputs.length; i++) {
  9192. abstractEvent = buildEventFromInput(eventInputs[i]);
  9193. if (abstractEvent) { // not false (a valid input)
  9194. renderableEvents = expandEvent(abstractEvent);
  9195. for (j = 0; j < renderableEvents.length; j++) {
  9196. event = renderableEvents[j];
  9197. if (!event.source) {
  9198. if (stick) {
  9199. stickySource.events.push(event);
  9200. event.source = stickySource;
  9201. }
  9202. cache.push(event);
  9203. }
  9204. }
  9205. renderedEvents = renderedEvents.concat(renderableEvents);
  9206. }
  9207. }
  9208. if (renderedEvents.length) { // any new events rendered?
  9209. reportEventChange();
  9210. }
  9211. return renderedEvents;
  9212. }
  9213. function removeEvents(filter) {
  9214. var eventID;
  9215. var i;
  9216. if (filter == null) { // null or undefined. remove all events
  9217. filter = function() { return true; }; // will always match
  9218. }
  9219. else if (!$.isFunction(filter)) { // an event ID
  9220. eventID = filter + '';
  9221. filter = function(event) {
  9222. return event._id == eventID;
  9223. };
  9224. }
  9225. // Purge event(s) from our local cache
  9226. cache = $.grep(cache, filter, true); // inverse=true
  9227. // Remove events from array sources.
  9228. // This works because they have been converted to official Event Objects up front.
  9229. // (and as a result, event._id has been calculated).
  9230. for (i=0; i<sources.length; i++) {
  9231. if ($.isArray(sources[i].events)) {
  9232. sources[i].events = $.grep(sources[i].events, filter, true);
  9233. }
  9234. }
  9235. reportEventChange();
  9236. }
  9237. function clientEvents(filter) {
  9238. if ($.isFunction(filter)) {
  9239. return $.grep(cache, filter);
  9240. }
  9241. else if (filter != null) { // not null, not undefined. an event ID
  9242. filter += '';
  9243. return $.grep(cache, function(e) {
  9244. return e._id == filter;
  9245. });
  9246. }
  9247. return cache; // else, return all
  9248. }
  9249. // Makes sure all array event sources have their internal event objects
  9250. // converted over to the Calendar's current timezone.
  9251. t.rezoneArrayEventSources = function() {
  9252. var i;
  9253. var events;
  9254. var j;
  9255. for (i = 0; i < sources.length; i++) {
  9256. events = sources[i].events;
  9257. if ($.isArray(events)) {
  9258. for (j = 0; j < events.length; j++) {
  9259. rezoneEventDates(events[j]);
  9260. }
  9261. }
  9262. }
  9263. };
  9264. function rezoneEventDates(event) {
  9265. event.start = t.moment(event.start);
  9266. if (event.end) {
  9267. event.end = t.moment(event.end);
  9268. }
  9269. backupEventDates(event);
  9270. }
  9271. /* Event Normalization
  9272. -----------------------------------------------------------------------------*/
  9273. // Given a raw object with key/value properties, returns an "abstract" Event object.
  9274. // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  9275. // Will return `false` when input is invalid.
  9276. // `source` is optional
  9277. function buildEventFromInput(input, source) {
  9278. var out = {};
  9279. var start, end;
  9280. var allDay;
  9281. if (t.options.eventDataTransform) {
  9282. input = t.options.eventDataTransform(input);
  9283. }
  9284. if (source && source.eventDataTransform) {
  9285. input = source.eventDataTransform(input);
  9286. }
  9287. // Copy all properties over to the resulting object.
  9288. // The special-case properties will be copied over afterwards.
  9289. $.extend(out, input);
  9290. if (source) {
  9291. out.source = source;
  9292. }
  9293. out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  9294. if (input.className) {
  9295. if (typeof input.className == 'string') {
  9296. out.className = input.className.split(/\s+/);
  9297. }
  9298. else { // assumed to be an array
  9299. out.className = input.className;
  9300. }
  9301. }
  9302. else {
  9303. out.className = [];
  9304. }
  9305. start = input.start || input.date; // "date" is an alias for "start"
  9306. end = input.end;
  9307. // parse as a time (Duration) if applicable
  9308. if (isTimeString(start)) {
  9309. start = moment.duration(start);
  9310. }
  9311. if (isTimeString(end)) {
  9312. end = moment.duration(end);
  9313. }
  9314. if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  9315. // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  9316. out.start = start ? moment.duration(start) : null; // will be a Duration or null
  9317. out.end = end ? moment.duration(end) : null; // will be a Duration or null
  9318. out._recurring = true; // our internal marker
  9319. }
  9320. else {
  9321. if (start) {
  9322. start = t.moment(start);
  9323. if (!start.isValid()) {
  9324. return false;
  9325. }
  9326. }
  9327. if (end) {
  9328. end = t.moment(end);
  9329. if (!end.isValid()) {
  9330. end = null; // let defaults take over
  9331. }
  9332. }
  9333. allDay = input.allDay;
  9334. if (allDay === undefined) { // still undefined? fallback to default
  9335. allDay = firstDefined(
  9336. source ? source.allDayDefault : undefined,
  9337. t.options.allDayDefault
  9338. );
  9339. // still undefined? normalizeEventDates will calculate it
  9340. }
  9341. assignDatesToEvent(start, end, allDay, out);
  9342. }
  9343. t.normalizeEvent(out); // hook for external use. a prototype method
  9344. return out;
  9345. }
  9346. t.buildEventFromInput = buildEventFromInput;
  9347. // Normalizes and assigns the given dates to the given partially-formed event object.
  9348. // NOTE: mutates the given start/end moments. does not make a copy.
  9349. function assignDatesToEvent(start, end, allDay, event) {
  9350. event.start = start;
  9351. event.end = end;
  9352. event.allDay = allDay;
  9353. normalizeEventDates(event);
  9354. backupEventDates(event);
  9355. }
  9356. // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  9357. // NOTE: Will modify the given object.
  9358. function normalizeEventDates(eventProps) {
  9359. normalizeEventTimes(eventProps);
  9360. if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
  9361. eventProps.end = null;
  9362. }
  9363. if (!eventProps.end) {
  9364. if (t.options.forceEventDuration) {
  9365. eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
  9366. }
  9367. else {
  9368. eventProps.end = null;
  9369. }
  9370. }
  9371. }
  9372. // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  9373. function normalizeEventTimes(eventProps) {
  9374. if (eventProps.allDay == null) {
  9375. eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
  9376. }
  9377. if (eventProps.allDay) {
  9378. eventProps.start.stripTime();
  9379. if (eventProps.end) {
  9380. // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  9381. eventProps.end.stripTime();
  9382. }
  9383. }
  9384. else {
  9385. if (!eventProps.start.hasTime()) {
  9386. eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
  9387. }
  9388. if (eventProps.end && !eventProps.end.hasTime()) {
  9389. eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
  9390. }
  9391. }
  9392. }
  9393. // If the given event is a recurring event, break it down into an array of individual instances.
  9394. // If not a recurring event, return an array with the single original event.
  9395. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  9396. // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  9397. function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  9398. var events = [];
  9399. var dowHash;
  9400. var dow;
  9401. var i;
  9402. var date;
  9403. var startTime, endTime;
  9404. var start, end;
  9405. var event;
  9406. _rangeStart = _rangeStart || rangeStart;
  9407. _rangeEnd = _rangeEnd || rangeEnd;
  9408. if (abstractEvent) {
  9409. if (abstractEvent._recurring) {
  9410. // make a boolean hash as to whether the event occurs on each day-of-week
  9411. if ((dow = abstractEvent.dow)) {
  9412. dowHash = {};
  9413. for (i = 0; i < dow.length; i++) {
  9414. dowHash[dow[i]] = true;
  9415. }
  9416. }
  9417. // iterate through every day in the current range
  9418. date = _rangeStart.clone().stripTime(); // holds the date of the current day
  9419. while (date.isBefore(_rangeEnd)) {
  9420. if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  9421. startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  9422. endTime = abstractEvent.end; // "
  9423. start = date.clone();
  9424. end = null;
  9425. if (startTime) {
  9426. start = start.time(startTime);
  9427. }
  9428. if (endTime) {
  9429. end = date.clone().time(endTime);
  9430. }
  9431. event = $.extend({}, abstractEvent); // make a copy of the original
  9432. assignDatesToEvent(
  9433. start, end,
  9434. !startTime && !endTime, // allDay?
  9435. event
  9436. );
  9437. events.push(event);
  9438. }
  9439. date.add(1, 'days');
  9440. }
  9441. }
  9442. else {
  9443. events.push(abstractEvent); // return the original event. will be a one-item array
  9444. }
  9445. }
  9446. return events;
  9447. }
  9448. t.expandEvent = expandEvent;
  9449. /* Event Modification Math
  9450. -----------------------------------------------------------------------------------------*/
  9451. // Modifies an event and all related events by applying the given properties.
  9452. // Special date-diffing logic is used for manipulation of dates.
  9453. // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  9454. // All date comparisons are done against the event's pristine _start and _end dates.
  9455. // Returns an object with delta information and a function to undo all operations.
  9456. // For making computations in a granularity greater than day/time, specify largeUnit.
  9457. // NOTE: The given `newProps` might be mutated for normalization purposes.
  9458. function mutateEvent(event, newProps, largeUnit) {
  9459. var miscProps = {};
  9460. var oldProps;
  9461. var clearEnd;
  9462. var startDelta;
  9463. var endDelta;
  9464. var durationDelta;
  9465. var undoFunc;
  9466. // diffs the dates in the appropriate way, returning a duration
  9467. function diffDates(date1, date0) { // date1 - date0
  9468. if (largeUnit) {
  9469. return diffByUnit(date1, date0, largeUnit);
  9470. }
  9471. else if (newProps.allDay) {
  9472. return diffDay(date1, date0);
  9473. }
  9474. else {
  9475. return diffDayTime(date1, date0);
  9476. }
  9477. }
  9478. newProps = newProps || {};
  9479. // normalize new date-related properties
  9480. if (!newProps.start) {
  9481. newProps.start = event.start.clone();
  9482. }
  9483. if (newProps.end === undefined) {
  9484. newProps.end = event.end ? event.end.clone() : null;
  9485. }
  9486. if (newProps.allDay == null) { // is null or undefined?
  9487. newProps.allDay = event.allDay;
  9488. }
  9489. normalizeEventDates(newProps);
  9490. // create normalized versions of the original props to compare against
  9491. // need a real end value, for diffing
  9492. oldProps = {
  9493. start: event._start.clone(),
  9494. end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  9495. allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  9496. };
  9497. normalizeEventDates(oldProps);
  9498. // need to clear the end date if explicitly changed to null
  9499. clearEnd = event._end !== null && newProps.end === null;
  9500. // compute the delta for moving the start date
  9501. startDelta = diffDates(newProps.start, oldProps.start);
  9502. // compute the delta for moving the end date
  9503. if (newProps.end) {
  9504. endDelta = diffDates(newProps.end, oldProps.end);
  9505. durationDelta = endDelta.subtract(startDelta);
  9506. }
  9507. else {
  9508. durationDelta = null;
  9509. }
  9510. // gather all non-date-related properties
  9511. $.each(newProps, function(name, val) {
  9512. if (isMiscEventPropName(name)) {
  9513. if (val !== undefined) {
  9514. miscProps[name] = val;
  9515. }
  9516. }
  9517. });
  9518. // apply the operations to the event and all related events
  9519. undoFunc = mutateEvents(
  9520. clientEvents(event._id), // get events with this ID
  9521. clearEnd,
  9522. newProps.allDay,
  9523. startDelta,
  9524. durationDelta,
  9525. miscProps
  9526. );
  9527. return {
  9528. dateDelta: startDelta,
  9529. durationDelta: durationDelta,
  9530. undo: undoFunc
  9531. };
  9532. }
  9533. // Modifies an array of events in the following ways (operations are in order):
  9534. // - clear the event's `end`
  9535. // - convert the event to allDay
  9536. // - add `dateDelta` to the start and end
  9537. // - add `durationDelta` to the event's duration
  9538. // - assign `miscProps` to the event
  9539. //
  9540. // Returns a function that can be called to undo all the operations.
  9541. //
  9542. // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  9543. //
  9544. function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  9545. var isAmbigTimezone = t.getIsAmbigTimezone();
  9546. var undoFunctions = [];
  9547. // normalize zero-length deltas to be null
  9548. if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  9549. if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  9550. $.each(events, function(i, event) {
  9551. var oldProps;
  9552. var newProps;
  9553. // build an object holding all the old values, both date-related and misc.
  9554. // for the undo function.
  9555. oldProps = {
  9556. start: event.start.clone(),
  9557. end: event.end ? event.end.clone() : null,
  9558. allDay: event.allDay
  9559. };
  9560. $.each(miscProps, function(name) {
  9561. oldProps[name] = event[name];
  9562. });
  9563. // new date-related properties. work off the original date snapshot.
  9564. // ok to use references because they will be thrown away when backupEventDates is called.
  9565. newProps = {
  9566. start: event._start,
  9567. end: event._end,
  9568. allDay: allDay // normalize the dates in the same regard as the new properties
  9569. };
  9570. normalizeEventDates(newProps); // massages start/end/allDay
  9571. // strip or ensure the end date
  9572. if (clearEnd) {
  9573. newProps.end = null;
  9574. }
  9575. else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  9576. newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  9577. }
  9578. if (dateDelta) {
  9579. newProps.start.add(dateDelta);
  9580. if (newProps.end) {
  9581. newProps.end.add(dateDelta);
  9582. }
  9583. }
  9584. if (durationDelta) {
  9585. newProps.end.add(durationDelta); // end already ensured above
  9586. }
  9587. // if the dates have changed, and we know it is impossible to recompute the
  9588. // timezone offsets, strip the zone.
  9589. if (
  9590. isAmbigTimezone &&
  9591. !newProps.allDay &&
  9592. (dateDelta || durationDelta)
  9593. ) {
  9594. newProps.start.stripZone();
  9595. if (newProps.end) {
  9596. newProps.end.stripZone();
  9597. }
  9598. }
  9599. $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  9600. backupEventDates(event); // regenerate internal _start/_end/_allDay
  9601. undoFunctions.push(function() {
  9602. $.extend(event, oldProps);
  9603. backupEventDates(event); // regenerate internal _start/_end/_allDay
  9604. });
  9605. });
  9606. return function() {
  9607. for (var i = 0; i < undoFunctions.length; i++) {
  9608. undoFunctions[i]();
  9609. }
  9610. };
  9611. }
  9612. }
  9613. // hook for external libs to manipulate event properties upon creation.
  9614. // should manipulate the event in-place.
  9615. Calendar.prototype.normalizeEvent = function(event) {
  9616. };
  9617. // Does the given span (start, end, and other location information)
  9618. // fully contain the other?
  9619. Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
  9620. var eventStart = outerSpan.start.clone().stripZone();
  9621. var eventEnd = this.getEventEnd(outerSpan).stripZone();
  9622. return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
  9623. };
  9624. // Returns a list of events that the given event should be compared against when being considered for a move to
  9625. // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  9626. Calendar.prototype.getPeerEvents = function(span, event) {
  9627. var cache = this.getEventCache();
  9628. var peerEvents = [];
  9629. var i, otherEvent;
  9630. for (i = 0; i < cache.length; i++) {
  9631. otherEvent = cache[i];
  9632. if (
  9633. !event ||
  9634. event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  9635. ) {
  9636. peerEvents.push(otherEvent);
  9637. }
  9638. }
  9639. return peerEvents;
  9640. };
  9641. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  9642. function backupEventDates(event) {
  9643. event._allDay = event.allDay;
  9644. event._start = event.start.clone();
  9645. event._end = event.end ? event.end.clone() : null;
  9646. }
  9647. /* Overlapping / Constraining
  9648. -----------------------------------------------------------------------------------------*/
  9649. // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
  9650. Calendar.prototype.isEventSpanAllowed = function(span, event) {
  9651. var source = event.source || {};
  9652. var constraint = firstDefined(
  9653. event.constraint,
  9654. source.constraint,
  9655. this.options.eventConstraint
  9656. );
  9657. var overlap = firstDefined(
  9658. event.overlap,
  9659. source.overlap,
  9660. this.options.eventOverlap
  9661. );
  9662. return this.isSpanAllowed(span, constraint, overlap, event) &&
  9663. (!this.options.eventAllow || this.options.eventAllow(span, event) !== false);
  9664. };
  9665. // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
  9666. Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) {
  9667. var eventInput;
  9668. var event;
  9669. // note: very similar logic is in View's reportExternalDrop
  9670. if (eventProps) {
  9671. eventInput = $.extend({}, eventProps, eventLocation);
  9672. event = this.expandEvent(
  9673. this.buildEventFromInput(eventInput)
  9674. )[0];
  9675. }
  9676. if (event) {
  9677. return this.isEventSpanAllowed(eventSpan, event);
  9678. }
  9679. else { // treat it as a selection
  9680. return this.isSelectionSpanAllowed(eventSpan);
  9681. }
  9682. };
  9683. // Determines the given span (unzoned start/end with other misc data) can be selected.
  9684. Calendar.prototype.isSelectionSpanAllowed = function(span) {
  9685. return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) &&
  9686. (!this.options.selectAllow || this.options.selectAllow(span) !== false);
  9687. };
  9688. // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
  9689. // according to the constraint/overlap settings.
  9690. // `event` is not required if checking a selection.
  9691. Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) {
  9692. var constraintEvents;
  9693. var anyContainment;
  9694. var peerEvents;
  9695. var i, peerEvent;
  9696. var peerOverlap;
  9697. // the range must be fully contained by at least one of produced constraint events
  9698. if (constraint != null) {
  9699. // not treated as an event! intermediate data structure
  9700. // TODO: use ranges in the future
  9701. constraintEvents = this.constraintToEvents(constraint);
  9702. if (constraintEvents) { // not invalid
  9703. anyContainment = false;
  9704. for (i = 0; i < constraintEvents.length; i++) {
  9705. if (this.spanContainsSpan(constraintEvents[i], span)) {
  9706. anyContainment = true;
  9707. break;
  9708. }
  9709. }
  9710. if (!anyContainment) {
  9711. return false;
  9712. }
  9713. }
  9714. }
  9715. peerEvents = this.getPeerEvents(span, event);
  9716. for (i = 0; i < peerEvents.length; i++) {
  9717. peerEvent = peerEvents[i];
  9718. // there needs to be an actual intersection before disallowing anything
  9719. if (this.eventIntersectsRange(peerEvent, span)) {
  9720. // evaluate overlap for the given range and short-circuit if necessary
  9721. if (overlap === false) {
  9722. return false;
  9723. }
  9724. // if the event's overlap is a test function, pass the peer event in question as the first param
  9725. else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  9726. return false;
  9727. }
  9728. // if we are computing if the given range is allowable for an event, consider the other event's
  9729. // EventObject-specific or Source-specific `overlap` property
  9730. if (event) {
  9731. peerOverlap = firstDefined(
  9732. peerEvent.overlap,
  9733. (peerEvent.source || {}).overlap
  9734. // we already considered the global `eventOverlap`
  9735. );
  9736. if (peerOverlap === false) {
  9737. return false;
  9738. }
  9739. // if the peer event's overlap is a test function, pass the subject event as the first param
  9740. if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  9741. return false;
  9742. }
  9743. }
  9744. }
  9745. }
  9746. return true;
  9747. };
  9748. // Given an event input from the API, produces an array of event objects. Possible event inputs:
  9749. // 'businessHours'
  9750. // An event ID (number or string)
  9751. // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  9752. Calendar.prototype.constraintToEvents = function(constraintInput) {
  9753. if (constraintInput === 'businessHours') {
  9754. return this.getCurrentBusinessHourEvents();
  9755. }
  9756. if (typeof constraintInput === 'object') {
  9757. if (constraintInput.start != null) { // needs to be event-like input
  9758. return this.expandEvent(this.buildEventFromInput(constraintInput));
  9759. }
  9760. else {
  9761. return null; // invalid
  9762. }
  9763. }
  9764. return this.clientEvents(constraintInput); // probably an ID
  9765. };
  9766. // Does the event's date range intersect with the given range?
  9767. // start/end already assumed to have stripped zones :(
  9768. Calendar.prototype.eventIntersectsRange = function(event, range) {
  9769. var eventStart = event.start.clone().stripZone();
  9770. var eventEnd = this.getEventEnd(event).stripZone();
  9771. return range.start < eventEnd && range.end > eventStart;
  9772. };
  9773. /* Business Hours
  9774. -----------------------------------------------------------------------------------------*/
  9775. var BUSINESS_HOUR_EVENT_DEFAULTS = {
  9776. id: '_fcBusinessHours', // will relate events from different calls to expandEvent
  9777. start: '09:00',
  9778. end: '17:00',
  9779. dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  9780. rendering: 'inverse-background'
  9781. // classNames are defined in businessHoursSegClasses
  9782. };
  9783. // Return events objects for business hours within the current view.
  9784. // Abuse of our event system :(
  9785. Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) {
  9786. return this.computeBusinessHourEvents(wholeDay, this.options.businessHours);
  9787. };
  9788. // Given a raw input value from options, return events objects for business hours within the current view.
  9789. Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) {
  9790. if (input === true) {
  9791. return this.expandBusinessHourEvents(wholeDay, [ {} ]);
  9792. }
  9793. else if ($.isPlainObject(input)) {
  9794. return this.expandBusinessHourEvents(wholeDay, [ input ]);
  9795. }
  9796. else if ($.isArray(input)) {
  9797. return this.expandBusinessHourEvents(wholeDay, input, true);
  9798. }
  9799. else {
  9800. return [];
  9801. }
  9802. };
  9803. // inputs expected to be an array of objects.
  9804. // if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
  9805. Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) {
  9806. var view = this.getView();
  9807. var events = [];
  9808. var i, input;
  9809. for (i = 0; i < inputs.length; i++) {
  9810. input = inputs[i];
  9811. if (ignoreNoDow && !input.dow) {
  9812. continue;
  9813. }
  9814. // give defaults. will make a copy
  9815. input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input);
  9816. // if a whole-day series is requested, clear the start/end times
  9817. if (wholeDay) {
  9818. input.start = null;
  9819. input.end = null;
  9820. }
  9821. events.push.apply(events, // append
  9822. this.expandEvent(
  9823. this.buildEventFromInput(input),
  9824. view.start,
  9825. view.end
  9826. )
  9827. );
  9828. }
  9829. return events;
  9830. };
  9831. ;;
  9832. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  9833. ----------------------------------------------------------------------------------------------------------------------*/
  9834. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  9835. // It is responsible for managing width/height.
  9836. var BasicView = FC.BasicView = View.extend({
  9837. scroller: null,
  9838. dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
  9839. dayGrid: null, // the main subcomponent that does most of the heavy lifting
  9840. dayNumbersVisible: false, // display day numbers on each day cell?
  9841. colWeekNumbersVisible: false, // display week numbers along the side?
  9842. cellWeekNumbersVisible: false, // display week numbers in day cell?
  9843. weekNumberWidth: null, // width of all the week-number cells running down the side
  9844. headContainerEl: null, // div that hold's the dayGrid's rendered date header
  9845. headRowEl: null, // the fake row element of the day-of-week header
  9846. initialize: function() {
  9847. this.dayGrid = this.instantiateDayGrid();
  9848. this.scroller = new Scroller({
  9849. overflowX: 'hidden',
  9850. overflowY: 'auto'
  9851. });
  9852. },
  9853. // Generates the DayGrid object this view needs. Draws from this.dayGridClass
  9854. instantiateDayGrid: function() {
  9855. // generate a subclass on the fly with BasicView-specific behavior
  9856. // TODO: cache this subclass
  9857. var subclass = this.dayGridClass.extend(basicDayGridMethods);
  9858. return new subclass(this);
  9859. },
  9860. // Sets the display range and computes all necessary dates
  9861. setRange: function(range) {
  9862. View.prototype.setRange.call(this, range); // call the super-method
  9863. this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
  9864. this.dayGrid.setRange(range);
  9865. },
  9866. // Compute the value to feed into setRange. Overrides superclass.
  9867. computeRange: function(date) {
  9868. var range = View.prototype.computeRange.call(this, date); // get value from the super-method
  9869. // year and month views should be aligned with weeks. this is already done for week
  9870. if (/year|month/.test(range.intervalUnit)) {
  9871. range.start.startOf('week');
  9872. range.start = this.skipHiddenDays(range.start);
  9873. // make end-of-week if not already
  9874. if (range.end.weekday()) {
  9875. range.end.add(1, 'week').startOf('week');
  9876. range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
  9877. }
  9878. }
  9879. return range;
  9880. },
  9881. // Renders the view into `this.el`, which should already be assigned
  9882. renderDates: function() {
  9883. this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
  9884. if (this.opt('weekNumbers')) {
  9885. if (this.opt('weekNumbersWithinDays')) {
  9886. this.cellWeekNumbersVisible = true;
  9887. this.colWeekNumbersVisible = false;
  9888. }
  9889. else {
  9890. this.cellWeekNumbersVisible = false;
  9891. this.colWeekNumbersVisible = true;
  9892. };
  9893. }
  9894. this.dayGrid.numbersVisible = this.dayNumbersVisible ||
  9895. this.cellWeekNumbersVisible || this.colWeekNumbersVisible;
  9896. this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
  9897. this.renderHead();
  9898. this.scroller.render();
  9899. var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
  9900. var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
  9901. this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
  9902. this.dayGrid.setElement(dayGridEl);
  9903. this.dayGrid.renderDates(this.hasRigidRows());
  9904. },
  9905. // render the day-of-week headers
  9906. renderHead: function() {
  9907. this.headContainerEl =
  9908. this.el.find('.fc-head-container')
  9909. .html(this.dayGrid.renderHeadHtml());
  9910. this.headRowEl = this.headContainerEl.find('.fc-row');
  9911. },
  9912. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  9913. // always completely kill the dayGrid's rendering.
  9914. unrenderDates: function() {
  9915. this.dayGrid.unrenderDates();
  9916. this.dayGrid.removeElement();
  9917. this.scroller.destroy();
  9918. },
  9919. renderBusinessHours: function() {
  9920. this.dayGrid.renderBusinessHours();
  9921. },
  9922. unrenderBusinessHours: function() {
  9923. this.dayGrid.unrenderBusinessHours();
  9924. },
  9925. // Builds the HTML skeleton for the view.
  9926. // The day-grid component will render inside of a container defined by this HTML.
  9927. renderSkeletonHtml: function() {
  9928. return '' +
  9929. '<table>' +
  9930. '<thead class="fc-head">' +
  9931. '<tr>' +
  9932. '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
  9933. '</tr>' +
  9934. '</thead>' +
  9935. '<tbody class="fc-body">' +
  9936. '<tr>' +
  9937. '<td class="' + this.widgetContentClass + '"></td>' +
  9938. '</tr>' +
  9939. '</tbody>' +
  9940. '</table>';
  9941. },
  9942. // Generates an HTML attribute string for setting the width of the week number column, if it is known
  9943. weekNumberStyleAttr: function() {
  9944. if (this.weekNumberWidth !== null) {
  9945. return 'style="width:' + this.weekNumberWidth + 'px"';
  9946. }
  9947. return '';
  9948. },
  9949. // Determines whether each row should have a constant height
  9950. hasRigidRows: function() {
  9951. var eventLimit = this.opt('eventLimit');
  9952. return eventLimit && typeof eventLimit !== 'number';
  9953. },
  9954. /* Dimensions
  9955. ------------------------------------------------------------------------------------------------------------------*/
  9956. // Refreshes the horizontal dimensions of the view
  9957. updateWidth: function() {
  9958. if (this.colWeekNumbersVisible) {
  9959. // Make sure all week number cells running down the side have the same width.
  9960. // Record the width for cells created later.
  9961. this.weekNumberWidth = matchCellWidths(
  9962. this.el.find('.fc-week-number')
  9963. );
  9964. }
  9965. },
  9966. // Adjusts the vertical dimensions of the view to the specified values
  9967. setHeight: function(totalHeight, isAuto) {
  9968. var eventLimit = this.opt('eventLimit');
  9969. var scrollerHeight;
  9970. var scrollbarWidths;
  9971. // reset all heights to be natural
  9972. this.scroller.clear();
  9973. uncompensateScroll(this.headRowEl);
  9974. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  9975. // is the event limit a constant level number?
  9976. if (eventLimit && typeof eventLimit === 'number') {
  9977. this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  9978. }
  9979. // distribute the height to the rows
  9980. // (totalHeight is a "recommended" value if isAuto)
  9981. scrollerHeight = this.computeScrollerHeight(totalHeight);
  9982. this.setGridHeight(scrollerHeight, isAuto);
  9983. // is the event limit dynamically calculated?
  9984. if (eventLimit && typeof eventLimit !== 'number') {
  9985. this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  9986. }
  9987. if (!isAuto) { // should we force dimensions of the scroll container?
  9988. this.scroller.setHeight(scrollerHeight);
  9989. scrollbarWidths = this.scroller.getScrollbarWidths();
  9990. if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
  9991. compensateScroll(this.headRowEl, scrollbarWidths);
  9992. // doing the scrollbar compensation might have created text overflow which created more height. redo
  9993. scrollerHeight = this.computeScrollerHeight(totalHeight);
  9994. this.scroller.setHeight(scrollerHeight);
  9995. }
  9996. // guarantees the same scrollbar widths
  9997. this.scroller.lockOverflow(scrollbarWidths);
  9998. }
  9999. },
  10000. // given a desired total height of the view, returns what the height of the scroller should be
  10001. computeScrollerHeight: function(totalHeight) {
  10002. return totalHeight -
  10003. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10004. },
  10005. // Sets the height of just the DayGrid component in this view
  10006. setGridHeight: function(height, isAuto) {
  10007. if (isAuto) {
  10008. undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  10009. }
  10010. else {
  10011. distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  10012. }
  10013. },
  10014. /* Scroll
  10015. ------------------------------------------------------------------------------------------------------------------*/
  10016. computeInitialScroll: function() {
  10017. return { top: 0 };
  10018. },
  10019. queryScroll: function() {
  10020. return { top: this.scroller.getScrollTop() };
  10021. },
  10022. setScroll: function(scroll) {
  10023. this.scroller.setScrollTop(scroll.top);
  10024. },
  10025. /* Hit Areas
  10026. ------------------------------------------------------------------------------------------------------------------*/
  10027. // forward all hit-related method calls to dayGrid
  10028. prepareHits: function() {
  10029. this.dayGrid.prepareHits();
  10030. },
  10031. releaseHits: function() {
  10032. this.dayGrid.releaseHits();
  10033. },
  10034. queryHit: function(left, top) {
  10035. return this.dayGrid.queryHit(left, top);
  10036. },
  10037. getHitSpan: function(hit) {
  10038. return this.dayGrid.getHitSpan(hit);
  10039. },
  10040. getHitEl: function(hit) {
  10041. return this.dayGrid.getHitEl(hit);
  10042. },
  10043. /* Events
  10044. ------------------------------------------------------------------------------------------------------------------*/
  10045. // Renders the given events onto the view and populates the segments array
  10046. renderEvents: function(events) {
  10047. this.dayGrid.renderEvents(events);
  10048. this.updateHeight(); // must compensate for events that overflow the row
  10049. },
  10050. // Retrieves all segment objects that are rendered in the view
  10051. getEventSegs: function() {
  10052. return this.dayGrid.getEventSegs();
  10053. },
  10054. // Unrenders all event elements and clears internal segment data
  10055. unrenderEvents: function() {
  10056. this.dayGrid.unrenderEvents();
  10057. // we DON'T need to call updateHeight() because
  10058. // a renderEvents() call always happens after this, which will eventually call updateHeight()
  10059. },
  10060. /* Dragging (for both events and external elements)
  10061. ------------------------------------------------------------------------------------------------------------------*/
  10062. // A returned value of `true` signals that a mock "helper" event has been rendered.
  10063. renderDrag: function(dropLocation, seg) {
  10064. return this.dayGrid.renderDrag(dropLocation, seg);
  10065. },
  10066. unrenderDrag: function() {
  10067. this.dayGrid.unrenderDrag();
  10068. },
  10069. /* Selection
  10070. ------------------------------------------------------------------------------------------------------------------*/
  10071. // Renders a visual indication of a selection
  10072. renderSelection: function(span) {
  10073. this.dayGrid.renderSelection(span);
  10074. },
  10075. // Unrenders a visual indications of a selection
  10076. unrenderSelection: function() {
  10077. this.dayGrid.unrenderSelection();
  10078. }
  10079. });
  10080. // Methods that will customize the rendering behavior of the BasicView's dayGrid
  10081. var basicDayGridMethods = {
  10082. // Generates the HTML that will go before the day-of week header cells
  10083. renderHeadIntroHtml: function() {
  10084. var view = this.view;
  10085. if (view.colWeekNumbersVisible) {
  10086. return '' +
  10087. '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
  10088. '<span>' + // needed for matchCellWidths
  10089. htmlEscape(view.opt('weekNumberTitle')) +
  10090. '</span>' +
  10091. '</th>';
  10092. }
  10093. return '';
  10094. },
  10095. // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
  10096. renderNumberIntroHtml: function(row) {
  10097. var view = this.view;
  10098. var weekStart = this.getCellDate(row, 0);
  10099. if (view.colWeekNumbersVisible) {
  10100. return '' +
  10101. '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
  10102. view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
  10103. { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
  10104. weekStart.format('w') // inner HTML
  10105. ) +
  10106. '</td>';
  10107. }
  10108. return '';
  10109. },
  10110. // Generates the HTML that goes before the day bg cells for each day-row
  10111. renderBgIntroHtml: function() {
  10112. var view = this.view;
  10113. if (view.colWeekNumbersVisible) {
  10114. return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
  10115. view.weekNumberStyleAttr() + '></td>';
  10116. }
  10117. return '';
  10118. },
  10119. // Generates the HTML that goes before every other type of row generated by DayGrid.
  10120. // Affects helper-skeleton and highlight-skeleton rows.
  10121. renderIntroHtml: function() {
  10122. var view = this.view;
  10123. if (view.colWeekNumbersVisible) {
  10124. return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
  10125. }
  10126. return '';
  10127. }
  10128. };
  10129. ;;
  10130. /* A month view with day cells running in rows (one-per-week) and columns
  10131. ----------------------------------------------------------------------------------------------------------------------*/
  10132. var MonthView = FC.MonthView = BasicView.extend({
  10133. // Produces information about what range to display
  10134. computeRange: function(date) {
  10135. var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
  10136. var rowCnt;
  10137. // ensure 6 weeks
  10138. if (this.isFixedWeeks()) {
  10139. rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
  10140. range.end.add(6 - rowCnt, 'weeks');
  10141. }
  10142. return range;
  10143. },
  10144. // Overrides the default BasicView behavior to have special multi-week auto-height logic
  10145. setGridHeight: function(height, isAuto) {
  10146. // if auto, make the height of each row the height that it would be if there were 6 weeks
  10147. if (isAuto) {
  10148. height *= this.rowCnt / 6;
  10149. }
  10150. distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  10151. },
  10152. isFixedWeeks: function() {
  10153. return this.opt('fixedWeekCount');
  10154. }
  10155. });
  10156. ;;
  10157. fcViews.basic = {
  10158. 'class': BasicView
  10159. };
  10160. fcViews.basicDay = {
  10161. type: 'basic',
  10162. duration: { days: 1 }
  10163. };
  10164. fcViews.basicWeek = {
  10165. type: 'basic',
  10166. duration: { weeks: 1 }
  10167. };
  10168. fcViews.month = {
  10169. 'class': MonthView,
  10170. duration: { months: 1 }, // important for prev/next
  10171. defaults: {
  10172. fixedWeekCount: true
  10173. }
  10174. };
  10175. ;;
  10176. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  10177. ----------------------------------------------------------------------------------------------------------------------*/
  10178. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  10179. // Responsible for managing width/height.
  10180. var AgendaView = FC.AgendaView = View.extend({
  10181. scroller: null,
  10182. timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
  10183. timeGrid: null, // the main time-grid subcomponent of this view
  10184. dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override
  10185. dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  10186. axisWidth: null, // the width of the time axis running down the side
  10187. headContainerEl: null, // div that hold's the timeGrid's rendered date header
  10188. noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
  10189. // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  10190. bottomRuleEl: null,
  10191. initialize: function() {
  10192. this.timeGrid = this.instantiateTimeGrid();
  10193. if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  10194. this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
  10195. }
  10196. this.scroller = new Scroller({
  10197. overflowX: 'hidden',
  10198. overflowY: 'auto'
  10199. });
  10200. },
  10201. // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
  10202. instantiateTimeGrid: function() {
  10203. var subclass = this.timeGridClass.extend(agendaTimeGridMethods);
  10204. return new subclass(this);
  10205. },
  10206. // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
  10207. instantiateDayGrid: function() {
  10208. var subclass = this.dayGridClass.extend(agendaDayGridMethods);
  10209. return new subclass(this);
  10210. },
  10211. /* Rendering
  10212. ------------------------------------------------------------------------------------------------------------------*/
  10213. // Sets the display range and computes all necessary dates
  10214. setRange: function(range) {
  10215. View.prototype.setRange.call(this, range); // call the super-method
  10216. this.timeGrid.setRange(range);
  10217. if (this.dayGrid) {
  10218. this.dayGrid.setRange(range);
  10219. }
  10220. },
  10221. // Renders the view into `this.el`, which has already been assigned
  10222. renderDates: function() {
  10223. this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
  10224. this.renderHead();
  10225. this.scroller.render();
  10226. var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
  10227. var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
  10228. this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
  10229. this.timeGrid.setElement(timeGridEl);
  10230. this.timeGrid.renderDates();
  10231. // the <hr> that sometimes displays under the time-grid
  10232. this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
  10233. .appendTo(this.timeGrid.el); // inject it into the time-grid
  10234. if (this.dayGrid) {
  10235. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  10236. this.dayGrid.renderDates();
  10237. // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  10238. this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  10239. }
  10240. this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  10241. },
  10242. // render the day-of-week headers
  10243. renderHead: function() {
  10244. this.headContainerEl =
  10245. this.el.find('.fc-head-container')
  10246. .html(this.timeGrid.renderHeadHtml());
  10247. },
  10248. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  10249. // always completely kill each grid's rendering.
  10250. unrenderDates: function() {
  10251. this.timeGrid.unrenderDates();
  10252. this.timeGrid.removeElement();
  10253. if (this.dayGrid) {
  10254. this.dayGrid.unrenderDates();
  10255. this.dayGrid.removeElement();
  10256. }
  10257. this.scroller.destroy();
  10258. },
  10259. // Builds the HTML skeleton for the view.
  10260. // The day-grid and time-grid components will render inside containers defined by this HTML.
  10261. renderSkeletonHtml: function() {
  10262. return '' +
  10263. '<table>' +
  10264. '<thead class="fc-head">' +
  10265. '<tr>' +
  10266. '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
  10267. '</tr>' +
  10268. '</thead>' +
  10269. '<tbody class="fc-body">' +
  10270. '<tr>' +
  10271. '<td class="' + this.widgetContentClass + '">' +
  10272. (this.dayGrid ?
  10273. '<div class="fc-day-grid"/>' +
  10274. '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
  10275. ''
  10276. ) +
  10277. '</td>' +
  10278. '</tr>' +
  10279. '</tbody>' +
  10280. '</table>';
  10281. },
  10282. // Generates an HTML attribute string for setting the width of the axis, if it is known
  10283. axisStyleAttr: function() {
  10284. if (this.axisWidth !== null) {
  10285. return 'style="width:' + this.axisWidth + 'px"';
  10286. }
  10287. return '';
  10288. },
  10289. /* Business Hours
  10290. ------------------------------------------------------------------------------------------------------------------*/
  10291. renderBusinessHours: function() {
  10292. this.timeGrid.renderBusinessHours();
  10293. if (this.dayGrid) {
  10294. this.dayGrid.renderBusinessHours();
  10295. }
  10296. },
  10297. unrenderBusinessHours: function() {
  10298. this.timeGrid.unrenderBusinessHours();
  10299. if (this.dayGrid) {
  10300. this.dayGrid.unrenderBusinessHours();
  10301. }
  10302. },
  10303. /* Now Indicator
  10304. ------------------------------------------------------------------------------------------------------------------*/
  10305. getNowIndicatorUnit: function() {
  10306. return this.timeGrid.getNowIndicatorUnit();
  10307. },
  10308. renderNowIndicator: function(date) {
  10309. this.timeGrid.renderNowIndicator(date);
  10310. },
  10311. unrenderNowIndicator: function() {
  10312. this.timeGrid.unrenderNowIndicator();
  10313. },
  10314. /* Dimensions
  10315. ------------------------------------------------------------------------------------------------------------------*/
  10316. updateSize: function(isResize) {
  10317. this.timeGrid.updateSize(isResize);
  10318. View.prototype.updateSize.call(this, isResize); // call the super-method
  10319. },
  10320. // Refreshes the horizontal dimensions of the view
  10321. updateWidth: function() {
  10322. // make all axis cells line up, and record the width so newly created axis cells will have it
  10323. this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  10324. },
  10325. // Adjusts the vertical dimensions of the view to the specified values
  10326. setHeight: function(totalHeight, isAuto) {
  10327. var eventLimit;
  10328. var scrollerHeight;
  10329. var scrollbarWidths;
  10330. // reset all dimensions back to the original state
  10331. this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  10332. this.scroller.clear(); // sets height to 'auto' and clears overflow
  10333. uncompensateScroll(this.noScrollRowEls);
  10334. // limit number of events in the all-day area
  10335. if (this.dayGrid) {
  10336. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  10337. eventLimit = this.opt('eventLimit');
  10338. if (eventLimit && typeof eventLimit !== 'number') {
  10339. eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  10340. }
  10341. if (eventLimit) {
  10342. this.dayGrid.limitRows(eventLimit);
  10343. }
  10344. }
  10345. if (!isAuto) { // should we force dimensions of the scroll container?
  10346. scrollerHeight = this.computeScrollerHeight(totalHeight);
  10347. this.scroller.setHeight(scrollerHeight);
  10348. scrollbarWidths = this.scroller.getScrollbarWidths();
  10349. if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
  10350. // make the all-day and header rows lines up
  10351. compensateScroll(this.noScrollRowEls, scrollbarWidths);
  10352. // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  10353. // and reapply the desired height to the scroller.
  10354. scrollerHeight = this.computeScrollerHeight(totalHeight);
  10355. this.scroller.setHeight(scrollerHeight);
  10356. }
  10357. // guarantees the same scrollbar widths
  10358. this.scroller.lockOverflow(scrollbarWidths);
  10359. // if there's any space below the slats, show the horizontal rule.
  10360. // this won't cause any new overflow, because lockOverflow already called.
  10361. if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
  10362. this.bottomRuleEl.show();
  10363. }
  10364. }
  10365. },
  10366. // given a desired total height of the view, returns what the height of the scroller should be
  10367. computeScrollerHeight: function(totalHeight) {
  10368. return totalHeight -
  10369. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10370. },
  10371. /* Scroll
  10372. ------------------------------------------------------------------------------------------------------------------*/
  10373. // Computes the initial pre-configured scroll state prior to allowing the user to change it
  10374. computeInitialScroll: function() {
  10375. var scrollTime = moment.duration(this.opt('scrollTime'));
  10376. var top = this.timeGrid.computeTimeTop(scrollTime);
  10377. // zoom can give weird floating-point values. rather scroll a little bit further
  10378. top = Math.ceil(top);
  10379. if (top) {
  10380. top++; // to overcome top border that slots beyond the first have. looks better
  10381. }
  10382. return { top: top };
  10383. },
  10384. queryScroll: function() {
  10385. return { top: this.scroller.getScrollTop() };
  10386. },
  10387. setScroll: function(scroll) {
  10388. this.scroller.setScrollTop(scroll.top);
  10389. },
  10390. /* Hit Areas
  10391. ------------------------------------------------------------------------------------------------------------------*/
  10392. // forward all hit-related method calls to the grids (dayGrid might not be defined)
  10393. prepareHits: function() {
  10394. this.timeGrid.prepareHits();
  10395. if (this.dayGrid) {
  10396. this.dayGrid.prepareHits();
  10397. }
  10398. },
  10399. releaseHits: function() {
  10400. this.timeGrid.releaseHits();
  10401. if (this.dayGrid) {
  10402. this.dayGrid.releaseHits();
  10403. }
  10404. },
  10405. queryHit: function(left, top) {
  10406. var hit = this.timeGrid.queryHit(left, top);
  10407. if (!hit && this.dayGrid) {
  10408. hit = this.dayGrid.queryHit(left, top);
  10409. }
  10410. return hit;
  10411. },
  10412. getHitSpan: function(hit) {
  10413. // TODO: hit.component is set as a hack to identify where the hit came from
  10414. return hit.component.getHitSpan(hit);
  10415. },
  10416. getHitEl: function(hit) {
  10417. // TODO: hit.component is set as a hack to identify where the hit came from
  10418. return hit.component.getHitEl(hit);
  10419. },
  10420. /* Events
  10421. ------------------------------------------------------------------------------------------------------------------*/
  10422. // Renders events onto the view and populates the View's segment array
  10423. renderEvents: function(events) {
  10424. var dayEvents = [];
  10425. var timedEvents = [];
  10426. var daySegs = [];
  10427. var timedSegs;
  10428. var i;
  10429. // separate the events into all-day and timed
  10430. for (i = 0; i < events.length; i++) {
  10431. if (events[i].allDay) {
  10432. dayEvents.push(events[i]);
  10433. }
  10434. else {
  10435. timedEvents.push(events[i]);
  10436. }
  10437. }
  10438. // render the events in the subcomponents
  10439. timedSegs = this.timeGrid.renderEvents(timedEvents);
  10440. if (this.dayGrid) {
  10441. daySegs = this.dayGrid.renderEvents(dayEvents);
  10442. }
  10443. // the all-day area is flexible and might have a lot of events, so shift the height
  10444. this.updateHeight();
  10445. },
  10446. // Retrieves all segment objects that are rendered in the view
  10447. getEventSegs: function() {
  10448. return this.timeGrid.getEventSegs().concat(
  10449. this.dayGrid ? this.dayGrid.getEventSegs() : []
  10450. );
  10451. },
  10452. // Unrenders all event elements and clears internal segment data
  10453. unrenderEvents: function() {
  10454. // unrender the events in the subcomponents
  10455. this.timeGrid.unrenderEvents();
  10456. if (this.dayGrid) {
  10457. this.dayGrid.unrenderEvents();
  10458. }
  10459. // we DON'T need to call updateHeight() because
  10460. // a renderEvents() call always happens after this, which will eventually call updateHeight()
  10461. },
  10462. /* Dragging (for events and external elements)
  10463. ------------------------------------------------------------------------------------------------------------------*/
  10464. // A returned value of `true` signals that a mock "helper" event has been rendered.
  10465. renderDrag: function(dropLocation, seg) {
  10466. if (dropLocation.start.hasTime()) {
  10467. return this.timeGrid.renderDrag(dropLocation, seg);
  10468. }
  10469. else if (this.dayGrid) {
  10470. return this.dayGrid.renderDrag(dropLocation, seg);
  10471. }
  10472. },
  10473. unrenderDrag: function() {
  10474. this.timeGrid.unrenderDrag();
  10475. if (this.dayGrid) {
  10476. this.dayGrid.unrenderDrag();
  10477. }
  10478. },
  10479. /* Selection
  10480. ------------------------------------------------------------------------------------------------------------------*/
  10481. // Renders a visual indication of a selection
  10482. renderSelection: function(span) {
  10483. if (span.start.hasTime() || span.end.hasTime()) {
  10484. this.timeGrid.renderSelection(span);
  10485. }
  10486. else if (this.dayGrid) {
  10487. this.dayGrid.renderSelection(span);
  10488. }
  10489. },
  10490. // Unrenders a visual indications of a selection
  10491. unrenderSelection: function() {
  10492. this.timeGrid.unrenderSelection();
  10493. if (this.dayGrid) {
  10494. this.dayGrid.unrenderSelection();
  10495. }
  10496. }
  10497. });
  10498. // Methods that will customize the rendering behavior of the AgendaView's timeGrid
  10499. // TODO: move into TimeGrid
  10500. var agendaTimeGridMethods = {
  10501. // Generates the HTML that will go before the day-of week header cells
  10502. renderHeadIntroHtml: function() {
  10503. var view = this.view;
  10504. var weekText;
  10505. if (view.opt('weekNumbers')) {
  10506. weekText = this.start.format(view.opt('smallWeekFormat'));
  10507. return '' +
  10508. '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
  10509. view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
  10510. { date: this.start, type: 'week', forceOff: this.colCnt > 1 },
  10511. htmlEscape(weekText) // inner HTML
  10512. ) +
  10513. '</th>';
  10514. }
  10515. else {
  10516. return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>';
  10517. }
  10518. },
  10519. // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  10520. renderBgIntroHtml: function() {
  10521. var view = this.view;
  10522. return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>';
  10523. },
  10524. // Generates the HTML that goes before all other types of cells.
  10525. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  10526. renderIntroHtml: function() {
  10527. var view = this.view;
  10528. return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
  10529. }
  10530. };
  10531. // Methods that will customize the rendering behavior of the AgendaView's dayGrid
  10532. var agendaDayGridMethods = {
  10533. // Generates the HTML that goes before the all-day cells
  10534. renderBgIntroHtml: function() {
  10535. var view = this.view;
  10536. return '' +
  10537. '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  10538. '<span>' + // needed for matchCellWidths
  10539. view.getAllDayHtml() +
  10540. '</span>' +
  10541. '</td>';
  10542. },
  10543. // Generates the HTML that goes before all other types of cells.
  10544. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  10545. renderIntroHtml: function() {
  10546. var view = this.view;
  10547. return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
  10548. }
  10549. };
  10550. ;;
  10551. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  10552. // potential nice values for the slot-duration and interval-duration
  10553. // from largest to smallest
  10554. var AGENDA_STOCK_SUB_DURATIONS = [
  10555. { hours: 1 },
  10556. { minutes: 30 },
  10557. { minutes: 15 },
  10558. { seconds: 30 },
  10559. { seconds: 15 }
  10560. ];
  10561. fcViews.agenda = {
  10562. 'class': AgendaView,
  10563. defaults: {
  10564. allDaySlot: true,
  10565. slotDuration: '00:30:00',
  10566. minTime: '00:00:00',
  10567. maxTime: '24:00:00',
  10568. slotEventOverlap: true // a bad name. confused with overlap/constraint system
  10569. }
  10570. };
  10571. fcViews.agendaDay = {
  10572. type: 'agenda',
  10573. duration: { days: 1 }
  10574. };
  10575. fcViews.agendaWeek = {
  10576. type: 'agenda',
  10577. duration: { weeks: 1 }
  10578. };
  10579. ;;
  10580. /*
  10581. Responsible for the scroller, and forwarding event-related actions into the "grid"
  10582. */
  10583. var ListView = View.extend({
  10584. grid: null,
  10585. scroller: null,
  10586. initialize: function() {
  10587. this.grid = new ListViewGrid(this);
  10588. this.scroller = new Scroller({
  10589. overflowX: 'hidden',
  10590. overflowY: 'auto'
  10591. });
  10592. },
  10593. setRange: function(range) {
  10594. View.prototype.setRange.call(this, range); // super
  10595. this.grid.setRange(range); // needs to process range-related options
  10596. },
  10597. renderSkeleton: function() {
  10598. this.el.addClass(
  10599. 'fc-list-view ' +
  10600. this.widgetContentClass
  10601. );
  10602. this.scroller.render();
  10603. this.scroller.el.appendTo(this.el);
  10604. this.grid.setElement(this.scroller.scrollEl);
  10605. },
  10606. unrenderSkeleton: function() {
  10607. this.scroller.destroy(); // will remove the Grid too
  10608. },
  10609. setHeight: function(totalHeight, isAuto) {
  10610. this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
  10611. },
  10612. computeScrollerHeight: function(totalHeight) {
  10613. return totalHeight -
  10614. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  10615. },
  10616. renderEvents: function(events) {
  10617. this.grid.renderEvents(events);
  10618. },
  10619. unrenderEvents: function() {
  10620. this.grid.unrenderEvents();
  10621. },
  10622. isEventResizable: function(event) {
  10623. return false;
  10624. },
  10625. isEventDraggable: function(event) {
  10626. return false;
  10627. }
  10628. });
  10629. /*
  10630. Responsible for event rendering and user-interaction.
  10631. Its "el" is the inner-content of the above view's scroller.
  10632. */
  10633. var ListViewGrid = Grid.extend({
  10634. segSelector: '.fc-list-item', // which elements accept event actions
  10635. hasDayInteractions: false, // no day selection or day clicking
  10636. // slices by day
  10637. spanToSegs: function(span) {
  10638. var view = this.view;
  10639. var dayStart = view.start.clone().time(0); // timed, so segs get times!
  10640. var dayIndex = 0;
  10641. var seg;
  10642. var segs = [];
  10643. while (dayStart < view.end) {
  10644. seg = intersectRanges(span, {
  10645. start: dayStart,
  10646. end: dayStart.clone().add(1, 'day')
  10647. });
  10648. if (seg) {
  10649. seg.dayIndex = dayIndex;
  10650. segs.push(seg);
  10651. }
  10652. dayStart.add(1, 'day');
  10653. dayIndex++;
  10654. // detect when span won't go fully into the next day,
  10655. // and mutate the latest seg to the be the end.
  10656. if (
  10657. seg && !seg.isEnd && span.end.hasTime() &&
  10658. span.end < dayStart.clone().add(this.view.nextDayThreshold)
  10659. ) {
  10660. seg.end = span.end.clone();
  10661. seg.isEnd = true;
  10662. break;
  10663. }
  10664. }
  10665. return segs;
  10666. },
  10667. // like "4:00am"
  10668. computeEventTimeFormat: function() {
  10669. return this.view.opt('mediumTimeFormat');
  10670. },
  10671. // for events with a url, the whole <tr> should be clickable,
  10672. // but it's impossible to wrap with an <a> tag. simulate this.
  10673. handleSegClick: function(seg, ev) {
  10674. var url;
  10675. Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action
  10676. // not clicking on or within an <a> with an href
  10677. if (!$(ev.target).closest('a[href]').length) {
  10678. url = seg.event.url;
  10679. if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
  10680. window.location.href = url; // simulate link click
  10681. }
  10682. }
  10683. },
  10684. // returns list of foreground segs that were actually rendered
  10685. renderFgSegs: function(segs) {
  10686. segs = this.renderFgSegEls(segs); // might filter away hidden events
  10687. if (!segs.length) {
  10688. this.renderEmptyMessage();
  10689. }
  10690. else {
  10691. this.renderSegList(segs);
  10692. }
  10693. return segs;
  10694. },
  10695. renderEmptyMessage: function() {
  10696. this.el.html(
  10697. '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
  10698. '<div class="fc-list-empty-wrap1">' +
  10699. '<div class="fc-list-empty">' +
  10700. htmlEscape(this.view.opt('noEventsMessage')) +
  10701. '</div>' +
  10702. '</div>' +
  10703. '</div>'
  10704. );
  10705. },
  10706. // render the event segments in the view
  10707. renderSegList: function(allSegs) {
  10708. var segsByDay = this.groupSegsByDay(allSegs); // sparse array
  10709. var dayIndex;
  10710. var daySegs;
  10711. var i;
  10712. var tableEl = $('<table class="fc-list-table"><tbody/></table>');
  10713. var tbodyEl = tableEl.find('tbody');
  10714. for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
  10715. daySegs = segsByDay[dayIndex];
  10716. if (daySegs) { // sparse array, so might be undefined
  10717. // append a day header
  10718. tbodyEl.append(this.dayHeaderHtml(
  10719. this.view.start.clone().add(dayIndex, 'days')
  10720. ));
  10721. this.sortEventSegs(daySegs);
  10722. for (i = 0; i < daySegs.length; i++) {
  10723. tbodyEl.append(daySegs[i].el); // append event row
  10724. }
  10725. }
  10726. }
  10727. this.el.empty().append(tableEl);
  10728. },
  10729. // Returns a sparse array of arrays, segs grouped by their dayIndex
  10730. groupSegsByDay: function(segs) {
  10731. var segsByDay = []; // sparse array
  10732. var i, seg;
  10733. for (i = 0; i < segs.length; i++) {
  10734. seg = segs[i];
  10735. (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
  10736. .push(seg);
  10737. }
  10738. return segsByDay;
  10739. },
  10740. // generates the HTML for the day headers that live amongst the event rows
  10741. dayHeaderHtml: function(dayDate) {
  10742. var view = this.view;
  10743. var mainFormat = view.opt('listDayFormat');
  10744. var altFormat = view.opt('listDayAltFormat');
  10745. return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
  10746. '<td class="' + view.widgetHeaderClass + '" colspan="3">' +
  10747. (mainFormat ?
  10748. view.buildGotoAnchorHtml(
  10749. dayDate,
  10750. { 'class': 'fc-list-heading-main' },
  10751. htmlEscape(dayDate.format(mainFormat)) // inner HTML
  10752. ) :
  10753. '') +
  10754. (altFormat ?
  10755. view.buildGotoAnchorHtml(
  10756. dayDate,
  10757. { 'class': 'fc-list-heading-alt' },
  10758. htmlEscape(dayDate.format(altFormat)) // inner HTML
  10759. ) :
  10760. '') +
  10761. '</td>' +
  10762. '</tr>';
  10763. },
  10764. // generates the HTML for a single event row
  10765. fgSegHtml: function(seg) {
  10766. var view = this.view;
  10767. var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg));
  10768. var bgColor = this.getSegBackgroundColor(seg);
  10769. var event = seg.event;
  10770. var url = event.url;
  10771. var timeHtml;
  10772. if (event.allDay) {
  10773. timeHtml = view.getAllDayHtml();
  10774. }
  10775. else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day
  10776. if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
  10777. timeHtml = htmlEscape(this.getEventTimeText(seg));
  10778. }
  10779. else { // inner segment that lasts the whole day
  10780. timeHtml = view.getAllDayHtml();
  10781. }
  10782. }
  10783. else {
  10784. // Display the normal time text for the *event's* times
  10785. timeHtml = htmlEscape(this.getEventTimeText(event));
  10786. }
  10787. if (url) {
  10788. classes.push('fc-has-url');
  10789. }
  10790. return '<tr class="' + classes.join(' ') + '">' +
  10791. (this.displayEventTime ?
  10792. '<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
  10793. (timeHtml || '') +
  10794. '</td>' :
  10795. '') +
  10796. '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
  10797. '<span class="fc-event-dot"' +
  10798. (bgColor ?
  10799. ' style="background-color:' + bgColor + '"' :
  10800. '') +
  10801. '></span>' +
  10802. '</td>' +
  10803. '<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
  10804. '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
  10805. htmlEscape(seg.event.title || '') +
  10806. '</a>' +
  10807. '</td>' +
  10808. '</tr>';
  10809. }
  10810. });
  10811. ;;
  10812. fcViews.list = {
  10813. 'class': ListView,
  10814. buttonTextKey: 'list', // what to lookup in locale files
  10815. defaults: {
  10816. buttonText: 'list', // text to display for English
  10817. listDayFormat: 'LL', // like "January 1, 2016"
  10818. noEventsMessage: 'No events to display'
  10819. }
  10820. };
  10821. fcViews.listDay = {
  10822. type: 'list',
  10823. duration: { days: 1 },
  10824. defaults: {
  10825. listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header
  10826. }
  10827. };
  10828. fcViews.listWeek = {
  10829. type: 'list',
  10830. duration: { weeks: 1 },
  10831. defaults: {
  10832. listDayFormat: 'dddd', // day-of-week is more important
  10833. listDayAltFormat: 'LL'
  10834. }
  10835. };
  10836. fcViews.listMonth = {
  10837. type: 'list',
  10838. duration: { month: 1 },
  10839. defaults: {
  10840. listDayAltFormat: 'dddd' // day-of-week is nice-to-have
  10841. }
  10842. };
  10843. fcViews.listYear = {
  10844. type: 'list',
  10845. duration: { year: 1 },
  10846. defaults: {
  10847. listDayAltFormat: 'dddd' // day-of-week is nice-to-have
  10848. }
  10849. };