foundation.es6.js 307 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373
  1. import $ from 'jquery';
  2. // Core Foundation Utilities, utilized in a number of places.
  3. /**
  4. * Returns a boolean for RTL support
  5. */
  6. function rtl() {
  7. return $('html').attr('dir') === 'rtl';
  8. }
  9. /**
  10. * returns a random base-36 uid with namespacing
  11. * @function
  12. * @param {Number} length - number of random base-36 digits desired. Increase for more random strings.
  13. * @param {String} namespace - name of plugin to be incorporated in uid, optional.
  14. * @default {String} '' - if no plugin name is provided, nothing is appended to the uid.
  15. * @returns {String} - unique id
  16. */
  17. function GetYoDigits(length, namespace){
  18. length = length || 6;
  19. return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1) + (namespace ? `-${namespace}` : '');
  20. }
  21. /**
  22. * Escape a string so it can be used as a regexp pattern
  23. * @function
  24. * @see https://stackoverflow.com/a/9310752/4317384
  25. *
  26. * @param {String} str - string to escape.
  27. * @returns {String} - escaped string
  28. */
  29. function RegExpEscape(str){
  30. return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
  31. }
  32. function transitionend($elem){
  33. var transitions = {
  34. 'transition': 'transitionend',
  35. 'WebkitTransition': 'webkitTransitionEnd',
  36. 'MozTransition': 'transitionend',
  37. 'OTransition': 'otransitionend'
  38. };
  39. var elem = document.createElement('div'),
  40. end;
  41. for (var t in transitions){
  42. if (typeof elem.style[t] !== 'undefined'){
  43. end = transitions[t];
  44. }
  45. }
  46. if(end){
  47. return end;
  48. }else{
  49. end = setTimeout(function(){
  50. $elem.triggerHandler('transitionend', [$elem]);
  51. }, 1);
  52. return 'transitionend';
  53. }
  54. }
  55. /**
  56. * Return an event type to listen for window load.
  57. *
  58. * If `$elem` is passed, an event will be triggered on `$elem`. If window is already loaded, the event will still be triggered.
  59. * If `handler` is passed, attach it to the event on `$elem`.
  60. * Calling `onLoad` without handler allows you to get the event type that will be triggered before attaching the handler by yourself.
  61. * @function
  62. *
  63. * @param {Object} [] $elem - jQuery element on which the event will be triggered if passed.
  64. * @param {Function} [] handler - function to attach to the event.
  65. * @returns {String} - event type that should or will be triggered.
  66. */
  67. function onLoad($elem, handler) {
  68. const didLoad = document.readyState === 'complete';
  69. const eventType = (didLoad ? '_didLoad' : 'load') + '.zf.util.onLoad';
  70. const cb = () => $elem.triggerHandler(eventType);
  71. if ($elem) {
  72. if (handler) $elem.one(eventType, handler);
  73. if (didLoad)
  74. setTimeout(cb);
  75. else
  76. $(window).one('load', cb);
  77. }
  78. return eventType;
  79. }
  80. /**
  81. * Retuns an handler for the `mouseleave` that ignore disappeared mouses.
  82. *
  83. * If the mouse "disappeared" from the document (like when going on a browser UI element, See https://git.io/zf-11410),
  84. * the event is ignored.
  85. * - If the `ignoreLeaveWindow` is `true`, the event is ignored when the user actually left the window
  86. * (like by switching to an other window with [Alt]+[Tab]).
  87. * - If the `ignoreReappear` is `true`, the event will be ignored when the mouse will reappear later on the document
  88. * outside of the element it left.
  89. *
  90. * @function
  91. *
  92. * @param {Function} [] handler - handler for the filtered `mouseleave` event to watch.
  93. * @param {Object} [] options - object of options:
  94. * - {Boolean} [false] ignoreLeaveWindow - also ignore when the user switched windows.
  95. * - {Boolean} [false] ignoreReappear - also ignore when the mouse reappeared outside of the element it left.
  96. * @returns {Function} - filtered handler to use to listen on the `mouseleave` event.
  97. */
  98. function ignoreMousedisappear(handler, { ignoreLeaveWindow = false, ignoreReappear = false } = {}) {
  99. return function leaveEventHandler(eLeave, ...rest) {
  100. const callback = handler.bind(this, eLeave, ...rest);
  101. // The mouse left: call the given callback if the mouse entered elsewhere
  102. if (eLeave.relatedTarget !== null) {
  103. return callback();
  104. }
  105. // Otherwise, check if the mouse actually left the window.
  106. // In firefox if the user switched between windows, the window sill have the focus by the time
  107. // the event is triggered. We have to debounce the event to test this case.
  108. setTimeout(function leaveEventDebouncer() {
  109. if (!ignoreLeaveWindow && document.hasFocus && !document.hasFocus()) {
  110. return callback();
  111. }
  112. // Otherwise, wait for the mouse to reeapear outside of the element,
  113. if (!ignoreReappear) {
  114. $(document).one('mouseenter', function reenterEventHandler(eReenter) {
  115. if (!$(eLeave.currentTarget).has(eReenter.target).length) {
  116. // Fill where the mouse finally entered.
  117. eLeave.relatedTarget = eReenter.target;
  118. callback();
  119. }
  120. });
  121. }
  122. }, 0);
  123. };
  124. }
  125. var foundation_core_utils = /*#__PURE__*/Object.freeze({
  126. rtl: rtl,
  127. GetYoDigits: GetYoDigits,
  128. RegExpEscape: RegExpEscape,
  129. transitionend: transitionend,
  130. onLoad: onLoad,
  131. ignoreMousedisappear: ignoreMousedisappear
  132. });
  133. // matchMedia() polyfill - Test a CSS media type/query in JS.
  134. // Authors & copyright(c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. MIT license
  135. /* eslint-disable */
  136. window.matchMedia || (window.matchMedia = (function () {
  137. // For browsers that support matchMedium api such as IE 9 and webkit
  138. var styleMedia = (window.styleMedia || window.media);
  139. // For those that don't support matchMedium
  140. if (!styleMedia) {
  141. var style = document.createElement('style'),
  142. script = document.getElementsByTagName('script')[0],
  143. info = null;
  144. style.type = 'text/css';
  145. style.id = 'matchmediajs-test';
  146. if (!script) {
  147. document.head.appendChild(style);
  148. } else {
  149. script.parentNode.insertBefore(style, script);
  150. }
  151. // 'style.currentStyle' is used by IE <= 8 and 'window.getComputedStyle' for all other browsers
  152. info = ('getComputedStyle' in window) && window.getComputedStyle(style, null) || style.currentStyle;
  153. styleMedia = {
  154. matchMedium: function (media) {
  155. var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }';
  156. // 'style.styleSheet' is used by IE <= 8 and 'style.textContent' for all other browsers
  157. if (style.styleSheet) {
  158. style.styleSheet.cssText = text;
  159. } else {
  160. style.textContent = text;
  161. }
  162. // Test if media query is true or false
  163. return info.width === '1px';
  164. }
  165. };
  166. }
  167. return function(media) {
  168. return {
  169. matches: styleMedia.matchMedium(media || 'all'),
  170. media: media || 'all'
  171. };
  172. };
  173. })());
  174. /* eslint-enable */
  175. var MediaQuery = {
  176. queries: [],
  177. current: '',
  178. /**
  179. * Initializes the media query helper, by extracting the breakpoint list from the CSS and activating the breakpoint watcher.
  180. * @function
  181. * @private
  182. */
  183. _init() {
  184. var self = this;
  185. var $meta = $('meta.foundation-mq');
  186. if(!$meta.length){
  187. $('<meta class="foundation-mq">').appendTo(document.head);
  188. }
  189. var extractedStyles = $('.foundation-mq').css('font-family');
  190. var namedQueries;
  191. namedQueries = parseStyleToObject(extractedStyles);
  192. for (var key in namedQueries) {
  193. if(namedQueries.hasOwnProperty(key)) {
  194. self.queries.push({
  195. name: key,
  196. value: `only screen and (min-width: ${namedQueries[key]})`
  197. });
  198. }
  199. }
  200. this.current = this._getCurrentSize();
  201. this._watcher();
  202. },
  203. /**
  204. * Checks if the screen is at least as wide as a breakpoint.
  205. * @function
  206. * @param {String} size - Name of the breakpoint to check.
  207. * @returns {Boolean} `true` if the breakpoint matches, `false` if it's smaller.
  208. */
  209. atLeast(size) {
  210. var query = this.get(size);
  211. if (query) {
  212. return window.matchMedia(query).matches;
  213. }
  214. return false;
  215. },
  216. /**
  217. * Checks if the screen matches to a breakpoint.
  218. * @function
  219. * @param {String} size - Name of the breakpoint to check, either 'small only' or 'small'. Omitting 'only' falls back to using atLeast() method.
  220. * @returns {Boolean} `true` if the breakpoint matches, `false` if it does not.
  221. */
  222. is(size) {
  223. size = size.trim().split(' ');
  224. if(size.length > 1 && size[1] === 'only') {
  225. if(size[0] === this._getCurrentSize()) return true;
  226. } else {
  227. return this.atLeast(size[0]);
  228. }
  229. return false;
  230. },
  231. /**
  232. * Gets the media query of a breakpoint.
  233. * @function
  234. * @param {String} size - Name of the breakpoint to get.
  235. * @returns {String|null} - The media query of the breakpoint, or `null` if the breakpoint doesn't exist.
  236. */
  237. get(size) {
  238. for (var i in this.queries) {
  239. if(this.queries.hasOwnProperty(i)) {
  240. var query = this.queries[i];
  241. if (size === query.name) return query.value;
  242. }
  243. }
  244. return null;
  245. },
  246. /**
  247. * Gets the current breakpoint name by testing every breakpoint and returning the last one to match (the biggest one).
  248. * @function
  249. * @private
  250. * @returns {String} Name of the current breakpoint.
  251. */
  252. _getCurrentSize() {
  253. var matched;
  254. for (var i = 0; i < this.queries.length; i++) {
  255. var query = this.queries[i];
  256. if (window.matchMedia(query.value).matches) {
  257. matched = query;
  258. }
  259. }
  260. if (typeof matched === 'object') {
  261. return matched.name;
  262. } else {
  263. return matched;
  264. }
  265. },
  266. /**
  267. * Activates the breakpoint watcher, which fires an event on the window whenever the breakpoint changes.
  268. * @function
  269. * @private
  270. */
  271. _watcher() {
  272. $(window).off('resize.zf.mediaquery').on('resize.zf.mediaquery', () => {
  273. var newSize = this._getCurrentSize(), currentSize = this.current;
  274. if (newSize !== currentSize) {
  275. // Change the current media query
  276. this.current = newSize;
  277. // Broadcast the media query change on the window
  278. $(window).trigger('changed.zf.mediaquery', [newSize, currentSize]);
  279. }
  280. });
  281. }
  282. };
  283. // Thank you: https://github.com/sindresorhus/query-string
  284. function parseStyleToObject(str) {
  285. var styleObject = {};
  286. if (typeof str !== 'string') {
  287. return styleObject;
  288. }
  289. str = str.trim().slice(1, -1); // browsers re-quote string style values
  290. if (!str) {
  291. return styleObject;
  292. }
  293. styleObject = str.split('&').reduce(function(ret, param) {
  294. var parts = param.replace(/\+/g, ' ').split('=');
  295. var key = parts[0];
  296. var val = parts[1];
  297. key = decodeURIComponent(key);
  298. // missing `=` should be `null`:
  299. // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
  300. val = typeof val === 'undefined' ? null : decodeURIComponent(val);
  301. if (!ret.hasOwnProperty(key)) {
  302. ret[key] = val;
  303. } else if (Array.isArray(ret[key])) {
  304. ret[key].push(val);
  305. } else {
  306. ret[key] = [ret[key], val];
  307. }
  308. return ret;
  309. }, {});
  310. return styleObject;
  311. }
  312. var FOUNDATION_VERSION = '6.5.3';
  313. // Global Foundation object
  314. // This is attached to the window, or used as a module for AMD/Browserify
  315. var Foundation = {
  316. version: FOUNDATION_VERSION,
  317. /**
  318. * Stores initialized plugins.
  319. */
  320. _plugins: {},
  321. /**
  322. * Stores generated unique ids for plugin instances
  323. */
  324. _uuids: [],
  325. /**
  326. * Defines a Foundation plugin, adding it to the `Foundation` namespace and the list of plugins to initialize when reflowing.
  327. * @param {Object} plugin - The constructor of the plugin.
  328. */
  329. plugin: function(plugin, name) {
  330. // Object key to use when adding to global Foundation object
  331. // Examples: Foundation.Reveal, Foundation.OffCanvas
  332. var className = (name || functionName(plugin));
  333. // Object key to use when storing the plugin, also used to create the identifying data attribute for the plugin
  334. // Examples: data-reveal, data-off-canvas
  335. var attrName = hyphenate(className);
  336. // Add to the Foundation object and the plugins list (for reflowing)
  337. this._plugins[attrName] = this[className] = plugin;
  338. },
  339. /**
  340. * @function
  341. * Populates the _uuids array with pointers to each individual plugin instance.
  342. * Adds the `zfPlugin` data-attribute to programmatically created plugins to allow use of $(selector).foundation(method) calls.
  343. * Also fires the initialization event for each plugin, consolidating repetitive code.
  344. * @param {Object} plugin - an instance of a plugin, usually `this` in context.
  345. * @param {String} name - the name of the plugin, passed as a camelCased string.
  346. * @fires Plugin#init
  347. */
  348. registerPlugin: function(plugin, name){
  349. var pluginName = name ? hyphenate(name) : functionName(plugin.constructor).toLowerCase();
  350. plugin.uuid = GetYoDigits(6, pluginName);
  351. if(!plugin.$element.attr(`data-${pluginName}`)){ plugin.$element.attr(`data-${pluginName}`, plugin.uuid); }
  352. if(!plugin.$element.data('zfPlugin')){ plugin.$element.data('zfPlugin', plugin); }
  353. /**
  354. * Fires when the plugin has initialized.
  355. * @event Plugin#init
  356. */
  357. plugin.$element.trigger(`init.zf.${pluginName}`);
  358. this._uuids.push(plugin.uuid);
  359. return;
  360. },
  361. /**
  362. * @function
  363. * Removes the plugins uuid from the _uuids array.
  364. * Removes the zfPlugin data attribute, as well as the data-plugin-name attribute.
  365. * Also fires the destroyed event for the plugin, consolidating repetitive code.
  366. * @param {Object} plugin - an instance of a plugin, usually `this` in context.
  367. * @fires Plugin#destroyed
  368. */
  369. unregisterPlugin: function(plugin){
  370. var pluginName = hyphenate(functionName(plugin.$element.data('zfPlugin').constructor));
  371. this._uuids.splice(this._uuids.indexOf(plugin.uuid), 1);
  372. plugin.$element.removeAttr(`data-${pluginName}`).removeData('zfPlugin')
  373. /**
  374. * Fires when the plugin has been destroyed.
  375. * @event Plugin#destroyed
  376. */
  377. .trigger(`destroyed.zf.${pluginName}`);
  378. for(var prop in plugin){
  379. plugin[prop] = null;//clean up script to prep for garbage collection.
  380. }
  381. return;
  382. },
  383. /**
  384. * @function
  385. * Causes one or more active plugins to re-initialize, resetting event listeners, recalculating positions, etc.
  386. * @param {String} plugins - optional string of an individual plugin key, attained by calling `$(element).data('pluginName')`, or string of a plugin class i.e. `'dropdown'`
  387. * @default If no argument is passed, reflow all currently active plugins.
  388. */
  389. reInit: function(plugins){
  390. var isJQ = plugins instanceof $;
  391. try{
  392. if(isJQ){
  393. plugins.each(function(){
  394. $(this).data('zfPlugin')._init();
  395. });
  396. }else{
  397. var type = typeof plugins,
  398. _this = this,
  399. fns = {
  400. 'object': function(plgs){
  401. plgs.forEach(function(p){
  402. p = hyphenate(p);
  403. $('[data-'+ p +']').foundation('_init');
  404. });
  405. },
  406. 'string': function(){
  407. plugins = hyphenate(plugins);
  408. $('[data-'+ plugins +']').foundation('_init');
  409. },
  410. 'undefined': function(){
  411. this['object'](Object.keys(_this._plugins));
  412. }
  413. };
  414. fns[type](plugins);
  415. }
  416. }catch(err){
  417. console.error(err);
  418. }finally{
  419. return plugins;
  420. }
  421. },
  422. /**
  423. * Initialize plugins on any elements within `elem` (and `elem` itself) that aren't already initialized.
  424. * @param {Object} elem - jQuery object containing the element to check inside. Also checks the element itself, unless it's the `document` object.
  425. * @param {String|Array} plugins - A list of plugins to initialize. Leave this out to initialize everything.
  426. */
  427. reflow: function(elem, plugins) {
  428. // If plugins is undefined, just grab everything
  429. if (typeof plugins === 'undefined') {
  430. plugins = Object.keys(this._plugins);
  431. }
  432. // If plugins is a string, convert it to an array with one item
  433. else if (typeof plugins === 'string') {
  434. plugins = [plugins];
  435. }
  436. var _this = this;
  437. // Iterate through each plugin
  438. $.each(plugins, function(i, name) {
  439. // Get the current plugin
  440. var plugin = _this._plugins[name];
  441. // Localize the search to all elements inside elem, as well as elem itself, unless elem === document
  442. var $elem = $(elem).find('[data-'+name+']').addBack('[data-'+name+']');
  443. // For each plugin found, initialize it
  444. $elem.each(function() {
  445. var $el = $(this),
  446. opts = {};
  447. // Don't double-dip on plugins
  448. if ($el.data('zfPlugin')) {
  449. console.warn("Tried to initialize "+name+" on an element that already has a Foundation plugin.");
  450. return;
  451. }
  452. if($el.attr('data-options')){
  453. var thing = $el.attr('data-options').split(';').forEach(function(e, i){
  454. var opt = e.split(':').map(function(el){ return el.trim(); });
  455. if(opt[0]) opts[opt[0]] = parseValue(opt[1]);
  456. });
  457. }
  458. try{
  459. $el.data('zfPlugin', new plugin($(this), opts));
  460. }catch(er){
  461. console.error(er);
  462. }finally{
  463. return;
  464. }
  465. });
  466. });
  467. },
  468. getFnName: functionName,
  469. addToJquery: function($$$1) {
  470. // TODO: consider not making this a jQuery function
  471. // TODO: need way to reflow vs. re-initialize
  472. /**
  473. * The Foundation jQuery method.
  474. * @param {String|Array} method - An action to perform on the current jQuery object.
  475. */
  476. var foundation = function(method) {
  477. var type = typeof method,
  478. $noJS = $$$1('.no-js');
  479. if($noJS.length){
  480. $noJS.removeClass('no-js');
  481. }
  482. if(type === 'undefined'){//needs to initialize the Foundation object, or an individual plugin.
  483. MediaQuery._init();
  484. Foundation.reflow(this);
  485. }else if(type === 'string'){//an individual method to invoke on a plugin or group of plugins
  486. var args = Array.prototype.slice.call(arguments, 1);//collect all the arguments, if necessary
  487. var plugClass = this.data('zfPlugin');//determine the class of plugin
  488. if(typeof plugClass !== 'undefined' && typeof plugClass[method] !== 'undefined'){//make sure both the class and method exist
  489. if(this.length === 1){//if there's only one, call it directly.
  490. plugClass[method].apply(plugClass, args);
  491. }else{
  492. this.each(function(i, el){//otherwise loop through the jQuery collection and invoke the method on each
  493. plugClass[method].apply($$$1(el).data('zfPlugin'), args);
  494. });
  495. }
  496. }else{//error for no class or no method
  497. throw new ReferenceError("We're sorry, '" + method + "' is not an available method for " + (plugClass ? functionName(plugClass) : 'this element') + '.');
  498. }
  499. }else{//error for invalid argument type
  500. throw new TypeError(`We're sorry, ${type} is not a valid parameter. You must use a string representing the method you wish to invoke.`);
  501. }
  502. return this;
  503. };
  504. $$$1.fn.foundation = foundation;
  505. return $$$1;
  506. }
  507. };
  508. Foundation.util = {
  509. /**
  510. * Function for applying a debounce effect to a function call.
  511. * @function
  512. * @param {Function} func - Function to be called at end of timeout.
  513. * @param {Number} delay - Time in ms to delay the call of `func`.
  514. * @returns function
  515. */
  516. throttle: function (func, delay) {
  517. var timer = null;
  518. return function () {
  519. var context = this, args = arguments;
  520. if (timer === null) {
  521. timer = setTimeout(function () {
  522. func.apply(context, args);
  523. timer = null;
  524. }, delay);
  525. }
  526. };
  527. }
  528. };
  529. window.Foundation = Foundation;
  530. // Polyfill for requestAnimationFrame
  531. (function() {
  532. if (!Date.now || !window.Date.now)
  533. window.Date.now = Date.now = function() { return new Date().getTime(); };
  534. var vendors = ['webkit', 'moz'];
  535. for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
  536. var vp = vendors[i];
  537. window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
  538. window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
  539. || window[vp+'CancelRequestAnimationFrame']);
  540. }
  541. if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent)
  542. || !window.requestAnimationFrame || !window.cancelAnimationFrame) {
  543. var lastTime = 0;
  544. window.requestAnimationFrame = function(callback) {
  545. var now = Date.now();
  546. var nextTime = Math.max(lastTime + 16, now);
  547. return setTimeout(function() { callback(lastTime = nextTime); },
  548. nextTime - now);
  549. };
  550. window.cancelAnimationFrame = clearTimeout;
  551. }
  552. /**
  553. * Polyfill for performance.now, required by rAF
  554. */
  555. if(!window.performance || !window.performance.now){
  556. window.performance = {
  557. start: Date.now(),
  558. now: function(){ return Date.now() - this.start; }
  559. };
  560. }
  561. })();
  562. if (!Function.prototype.bind) {
  563. Function.prototype.bind = function(oThis) {
  564. if (typeof this !== 'function') {
  565. // closest thing possible to the ECMAScript 5
  566. // internal IsCallable function
  567. throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  568. }
  569. var aArgs = Array.prototype.slice.call(arguments, 1),
  570. fToBind = this,
  571. fNOP = function() {},
  572. fBound = function() {
  573. return fToBind.apply(this instanceof fNOP
  574. ? this
  575. : oThis,
  576. aArgs.concat(Array.prototype.slice.call(arguments)));
  577. };
  578. if (this.prototype) {
  579. // native functions don't have a prototype
  580. fNOP.prototype = this.prototype;
  581. }
  582. fBound.prototype = new fNOP();
  583. return fBound;
  584. };
  585. }
  586. // Polyfill to get the name of a function in IE9
  587. function functionName(fn) {
  588. if (typeof Function.prototype.name === 'undefined') {
  589. var funcNameRegex = /function\s([^(]{1,})\(/;
  590. var results = (funcNameRegex).exec((fn).toString());
  591. return (results && results.length > 1) ? results[1].trim() : "";
  592. }
  593. else if (typeof fn.prototype === 'undefined') {
  594. return fn.constructor.name;
  595. }
  596. else {
  597. return fn.prototype.constructor.name;
  598. }
  599. }
  600. function parseValue(str){
  601. if ('true' === str) return true;
  602. else if ('false' === str) return false;
  603. else if (!isNaN(str * 1)) return parseFloat(str);
  604. return str;
  605. }
  606. // Convert PascalCase to kebab-case
  607. // Thank you: http://stackoverflow.com/a/8955580
  608. function hyphenate(str) {
  609. return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  610. }
  611. var Box = {
  612. ImNotTouchingYou: ImNotTouchingYou,
  613. OverlapArea: OverlapArea,
  614. GetDimensions: GetDimensions,
  615. GetOffsets: GetOffsets,
  616. GetExplicitOffsets: GetExplicitOffsets
  617. };
  618. /**
  619. * Compares the dimensions of an element to a container and determines collision events with container.
  620. * @function
  621. * @param {jQuery} element - jQuery object to test for collisions.
  622. * @param {jQuery} parent - jQuery object to use as bounding container.
  623. * @param {Boolean} lrOnly - set to true to check left and right values only.
  624. * @param {Boolean} tbOnly - set to true to check top and bottom values only.
  625. * @default if no parent object passed, detects collisions with `window`.
  626. * @returns {Boolean} - true if collision free, false if a collision in any direction.
  627. */
  628. function ImNotTouchingYou(element, parent, lrOnly, tbOnly, ignoreBottom) {
  629. return OverlapArea(element, parent, lrOnly, tbOnly, ignoreBottom) === 0;
  630. }
  631. function OverlapArea(element, parent, lrOnly, tbOnly, ignoreBottom) {
  632. var eleDims = GetDimensions(element),
  633. topOver, bottomOver, leftOver, rightOver;
  634. if (parent) {
  635. var parDims = GetDimensions(parent);
  636. bottomOver = (parDims.height + parDims.offset.top) - (eleDims.offset.top + eleDims.height);
  637. topOver = eleDims.offset.top - parDims.offset.top;
  638. leftOver = eleDims.offset.left - parDims.offset.left;
  639. rightOver = (parDims.width + parDims.offset.left) - (eleDims.offset.left + eleDims.width);
  640. }
  641. else {
  642. bottomOver = (eleDims.windowDims.height + eleDims.windowDims.offset.top) - (eleDims.offset.top + eleDims.height);
  643. topOver = eleDims.offset.top - eleDims.windowDims.offset.top;
  644. leftOver = eleDims.offset.left - eleDims.windowDims.offset.left;
  645. rightOver = eleDims.windowDims.width - (eleDims.offset.left + eleDims.width);
  646. }
  647. bottomOver = ignoreBottom ? 0 : Math.min(bottomOver, 0);
  648. topOver = Math.min(topOver, 0);
  649. leftOver = Math.min(leftOver, 0);
  650. rightOver = Math.min(rightOver, 0);
  651. if (lrOnly) {
  652. return leftOver + rightOver;
  653. }
  654. if (tbOnly) {
  655. return topOver + bottomOver;
  656. }
  657. // use sum of squares b/c we care about overlap area.
  658. return Math.sqrt((topOver * topOver) + (bottomOver * bottomOver) + (leftOver * leftOver) + (rightOver * rightOver));
  659. }
  660. /**
  661. * Uses native methods to return an object of dimension values.
  662. * @function
  663. * @param {jQuery || HTML} element - jQuery object or DOM element for which to get the dimensions. Can be any element other that document or window.
  664. * @returns {Object} - nested object of integer pixel values
  665. * TODO - if element is window, return only those values.
  666. */
  667. function GetDimensions(elem){
  668. elem = elem.length ? elem[0] : elem;
  669. if (elem === window || elem === document) {
  670. throw new Error("I'm sorry, Dave. I'm afraid I can't do that.");
  671. }
  672. var rect = elem.getBoundingClientRect(),
  673. parRect = elem.parentNode.getBoundingClientRect(),
  674. winRect = document.body.getBoundingClientRect(),
  675. winY = window.pageYOffset,
  676. winX = window.pageXOffset;
  677. return {
  678. width: rect.width,
  679. height: rect.height,
  680. offset: {
  681. top: rect.top + winY,
  682. left: rect.left + winX
  683. },
  684. parentDims: {
  685. width: parRect.width,
  686. height: parRect.height,
  687. offset: {
  688. top: parRect.top + winY,
  689. left: parRect.left + winX
  690. }
  691. },
  692. windowDims: {
  693. width: winRect.width,
  694. height: winRect.height,
  695. offset: {
  696. top: winY,
  697. left: winX
  698. }
  699. }
  700. }
  701. }
  702. /**
  703. * Returns an object of top and left integer pixel values for dynamically rendered elements,
  704. * such as: Tooltip, Reveal, and Dropdown. Maintained for backwards compatibility, and where
  705. * you don't know alignment, but generally from
  706. * 6.4 forward you should use GetExplicitOffsets, as GetOffsets conflates position and alignment.
  707. * @function
  708. * @param {jQuery} element - jQuery object for the element being positioned.
  709. * @param {jQuery} anchor - jQuery object for the element's anchor point.
  710. * @param {String} position - a string relating to the desired position of the element, relative to it's anchor
  711. * @param {Number} vOffset - integer pixel value of desired vertical separation between anchor and element.
  712. * @param {Number} hOffset - integer pixel value of desired horizontal separation between anchor and element.
  713. * @param {Boolean} isOverflow - if a collision event is detected, sets to true to default the element to full width - any desired offset.
  714. * TODO alter/rewrite to work with `em` values as well/instead of pixels
  715. */
  716. function GetOffsets(element, anchor, position, vOffset, hOffset, isOverflow) {
  717. console.log("NOTE: GetOffsets is deprecated in favor of GetExplicitOffsets and will be removed in 6.5");
  718. switch (position) {
  719. case 'top':
  720. return rtl() ?
  721. GetExplicitOffsets(element, anchor, 'top', 'left', vOffset, hOffset, isOverflow) :
  722. GetExplicitOffsets(element, anchor, 'top', 'right', vOffset, hOffset, isOverflow);
  723. case 'bottom':
  724. return rtl() ?
  725. GetExplicitOffsets(element, anchor, 'bottom', 'left', vOffset, hOffset, isOverflow) :
  726. GetExplicitOffsets(element, anchor, 'bottom', 'right', vOffset, hOffset, isOverflow);
  727. case 'center top':
  728. return GetExplicitOffsets(element, anchor, 'top', 'center', vOffset, hOffset, isOverflow);
  729. case 'center bottom':
  730. return GetExplicitOffsets(element, anchor, 'bottom', 'center', vOffset, hOffset, isOverflow);
  731. case 'center left':
  732. return GetExplicitOffsets(element, anchor, 'left', 'center', vOffset, hOffset, isOverflow);
  733. case 'center right':
  734. return GetExplicitOffsets(element, anchor, 'right', 'center', vOffset, hOffset, isOverflow);
  735. case 'left bottom':
  736. return GetExplicitOffsets(element, anchor, 'bottom', 'left', vOffset, hOffset, isOverflow);
  737. case 'right bottom':
  738. return GetExplicitOffsets(element, anchor, 'bottom', 'right', vOffset, hOffset, isOverflow);
  739. // Backwards compatibility... this along with the reveal and reveal full
  740. // classes are the only ones that didn't reference anchor
  741. case 'center':
  742. return {
  743. left: ($eleDims.windowDims.offset.left + ($eleDims.windowDims.width / 2)) - ($eleDims.width / 2) + hOffset,
  744. top: ($eleDims.windowDims.offset.top + ($eleDims.windowDims.height / 2)) - ($eleDims.height / 2 + vOffset)
  745. }
  746. case 'reveal':
  747. return {
  748. left: ($eleDims.windowDims.width - $eleDims.width) / 2 + hOffset,
  749. top: $eleDims.windowDims.offset.top + vOffset
  750. }
  751. case 'reveal full':
  752. return {
  753. left: $eleDims.windowDims.offset.left,
  754. top: $eleDims.windowDims.offset.top
  755. }
  756. break;
  757. default:
  758. return {
  759. left: (rtl() ? $anchorDims.offset.left - $eleDims.width + $anchorDims.width - hOffset: $anchorDims.offset.left + hOffset),
  760. top: $anchorDims.offset.top + $anchorDims.height + vOffset
  761. }
  762. }
  763. }
  764. function GetExplicitOffsets(element, anchor, position, alignment, vOffset, hOffset, isOverflow) {
  765. var $eleDims = GetDimensions(element),
  766. $anchorDims = anchor ? GetDimensions(anchor) : null;
  767. var topVal, leftVal;
  768. // set position related attribute
  769. switch (position) {
  770. case 'top':
  771. topVal = $anchorDims.offset.top - ($eleDims.height + vOffset);
  772. break;
  773. case 'bottom':
  774. topVal = $anchorDims.offset.top + $anchorDims.height + vOffset;
  775. break;
  776. case 'left':
  777. leftVal = $anchorDims.offset.left - ($eleDims.width + hOffset);
  778. break;
  779. case 'right':
  780. leftVal = $anchorDims.offset.left + $anchorDims.width + hOffset;
  781. break;
  782. }
  783. // set alignment related attribute
  784. switch (position) {
  785. case 'top':
  786. case 'bottom':
  787. switch (alignment) {
  788. case 'left':
  789. leftVal = $anchorDims.offset.left + hOffset;
  790. break;
  791. case 'right':
  792. leftVal = $anchorDims.offset.left - $eleDims.width + $anchorDims.width - hOffset;
  793. break;
  794. case 'center':
  795. leftVal = isOverflow ? hOffset : (($anchorDims.offset.left + ($anchorDims.width / 2)) - ($eleDims.width / 2)) + hOffset;
  796. break;
  797. }
  798. break;
  799. case 'right':
  800. case 'left':
  801. switch (alignment) {
  802. case 'bottom':
  803. topVal = $anchorDims.offset.top - vOffset + $anchorDims.height - $eleDims.height;
  804. break;
  805. case 'top':
  806. topVal = $anchorDims.offset.top + vOffset;
  807. break;
  808. case 'center':
  809. topVal = ($anchorDims.offset.top + vOffset + ($anchorDims.height / 2)) - ($eleDims.height / 2);
  810. break;
  811. }
  812. break;
  813. }
  814. return {top: topVal, left: leftVal};
  815. }
  816. /**
  817. * Runs a callback function when images are fully loaded.
  818. * @param {Object} images - Image(s) to check if loaded.
  819. * @param {Func} callback - Function to execute when image is fully loaded.
  820. */
  821. function onImagesLoaded(images, callback){
  822. var unloaded = images.length;
  823. if (unloaded === 0) {
  824. callback();
  825. }
  826. images.each(function(){
  827. // Check if image is loaded
  828. if (this.complete && typeof this.naturalWidth !== 'undefined') {
  829. singleImageLoaded();
  830. }
  831. else {
  832. // If the above check failed, simulate loading on detached element.
  833. var image = new Image();
  834. // Still count image as loaded if it finalizes with an error.
  835. var events = "load.zf.images error.zf.images";
  836. $(image).one(events, function me(event){
  837. // Unbind the event listeners. We're using 'one' but only one of the two events will have fired.
  838. $(this).off(events, me);
  839. singleImageLoaded();
  840. });
  841. image.src = $(this).attr('src');
  842. }
  843. });
  844. function singleImageLoaded() {
  845. unloaded--;
  846. if (unloaded === 0) {
  847. callback();
  848. }
  849. }
  850. }
  851. /*******************************************
  852. * *
  853. * This util was created by Marius Olbertz *
  854. * Please thank Marius on GitHub /owlbertz *
  855. * or the web http://www.mariusolbertz.de/ *
  856. * *
  857. ******************************************/
  858. const keyCodes = {
  859. 9: 'TAB',
  860. 13: 'ENTER',
  861. 27: 'ESCAPE',
  862. 32: 'SPACE',
  863. 35: 'END',
  864. 36: 'HOME',
  865. 37: 'ARROW_LEFT',
  866. 38: 'ARROW_UP',
  867. 39: 'ARROW_RIGHT',
  868. 40: 'ARROW_DOWN'
  869. };
  870. var commands = {};
  871. // Functions pulled out to be referenceable from internals
  872. function findFocusable($element) {
  873. if(!$element) {return false; }
  874. return $element.find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]').filter(function() {
  875. if (!$(this).is(':visible') || $(this).attr('tabindex') < 0) { return false; } //only have visible elements and those that have a tabindex greater or equal 0
  876. return true;
  877. });
  878. }
  879. function parseKey(event) {
  880. var key = keyCodes[event.which || event.keyCode] || String.fromCharCode(event.which).toUpperCase();
  881. // Remove un-printable characters, e.g. for `fromCharCode` calls for CTRL only events
  882. key = key.replace(/\W+/, '');
  883. if (event.shiftKey) key = `SHIFT_${key}`;
  884. if (event.ctrlKey) key = `CTRL_${key}`;
  885. if (event.altKey) key = `ALT_${key}`;
  886. // Remove trailing underscore, in case only modifiers were used (e.g. only `CTRL_ALT`)
  887. key = key.replace(/_$/, '');
  888. return key;
  889. }
  890. var Keyboard = {
  891. keys: getKeyCodes(keyCodes),
  892. /**
  893. * Parses the (keyboard) event and returns a String that represents its key
  894. * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
  895. * @param {Event} event - the event generated by the event handler
  896. * @return String key - String that represents the key pressed
  897. */
  898. parseKey: parseKey,
  899. /**
  900. * Handles the given (keyboard) event
  901. * @param {Event} event - the event generated by the event handler
  902. * @param {String} component - Foundation component's name, e.g. Slider or Reveal
  903. * @param {Objects} functions - collection of functions that are to be executed
  904. */
  905. handleKey(event, component, functions) {
  906. var commandList = commands[component],
  907. keyCode = this.parseKey(event),
  908. cmds,
  909. command,
  910. fn;
  911. if (!commandList) return console.warn('Component not defined!');
  912. if (typeof commandList.ltr === 'undefined') { // this component does not differentiate between ltr and rtl
  913. cmds = commandList; // use plain list
  914. } else { // merge ltr and rtl: if document is rtl, rtl overwrites ltr and vice versa
  915. if (rtl()) cmds = $.extend({}, commandList.ltr, commandList.rtl);
  916. else cmds = $.extend({}, commandList.rtl, commandList.ltr);
  917. }
  918. command = cmds[keyCode];
  919. fn = functions[command];
  920. if (fn && typeof fn === 'function') { // execute function if exists
  921. var returnValue = fn.apply();
  922. if (functions.handled || typeof functions.handled === 'function') { // execute function when event was handled
  923. functions.handled(returnValue);
  924. }
  925. } else {
  926. if (functions.unhandled || typeof functions.unhandled === 'function') { // execute function when event was not handled
  927. functions.unhandled();
  928. }
  929. }
  930. },
  931. /**
  932. * Finds all focusable elements within the given `$element`
  933. * @param {jQuery} $element - jQuery object to search within
  934. * @return {jQuery} $focusable - all focusable elements within `$element`
  935. */
  936. findFocusable: findFocusable,
  937. /**
  938. * Returns the component name name
  939. * @param {Object} component - Foundation component, e.g. Slider or Reveal
  940. * @return String componentName
  941. */
  942. register(componentName, cmds) {
  943. commands[componentName] = cmds;
  944. },
  945. // TODO9438: These references to Keyboard need to not require global. Will 'this' work in this context?
  946. //
  947. /**
  948. * Traps the focus in the given element.
  949. * @param {jQuery} $element jQuery object to trap the foucs into.
  950. */
  951. trapFocus($element) {
  952. var $focusable = findFocusable($element),
  953. $firstFocusable = $focusable.eq(0),
  954. $lastFocusable = $focusable.eq(-1);
  955. $element.on('keydown.zf.trapfocus', function(event) {
  956. if (event.target === $lastFocusable[0] && parseKey(event) === 'TAB') {
  957. event.preventDefault();
  958. $firstFocusable.focus();
  959. }
  960. else if (event.target === $firstFocusable[0] && parseKey(event) === 'SHIFT_TAB') {
  961. event.preventDefault();
  962. $lastFocusable.focus();
  963. }
  964. });
  965. },
  966. /**
  967. * Releases the trapped focus from the given element.
  968. * @param {jQuery} $element jQuery object to release the focus for.
  969. */
  970. releaseFocus($element) {
  971. $element.off('keydown.zf.trapfocus');
  972. }
  973. };
  974. /*
  975. * Constants for easier comparing.
  976. * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
  977. */
  978. function getKeyCodes(kcs) {
  979. var k = {};
  980. for (var kc in kcs) k[kcs[kc]] = kcs[kc];
  981. return k;
  982. }
  983. /**
  984. * Motion module.
  985. * @module foundation.motion
  986. */
  987. const initClasses = ['mui-enter', 'mui-leave'];
  988. const activeClasses = ['mui-enter-active', 'mui-leave-active'];
  989. const Motion = {
  990. animateIn: function(element, animation, cb) {
  991. animate(true, element, animation, cb);
  992. },
  993. animateOut: function(element, animation, cb) {
  994. animate(false, element, animation, cb);
  995. }
  996. };
  997. function Move(duration, elem, fn){
  998. var anim, prog, start = null;
  999. // console.log('called');
  1000. if (duration === 0) {
  1001. fn.apply(elem);
  1002. elem.trigger('finished.zf.animate', [elem]).triggerHandler('finished.zf.animate', [elem]);
  1003. return;
  1004. }
  1005. function move(ts){
  1006. if(!start) start = ts;
  1007. // console.log(start, ts);
  1008. prog = ts - start;
  1009. fn.apply(elem);
  1010. if(prog < duration){ anim = window.requestAnimationFrame(move, elem); }
  1011. else{
  1012. window.cancelAnimationFrame(anim);
  1013. elem.trigger('finished.zf.animate', [elem]).triggerHandler('finished.zf.animate', [elem]);
  1014. }
  1015. }
  1016. anim = window.requestAnimationFrame(move);
  1017. }
  1018. /**
  1019. * Animates an element in or out using a CSS transition class.
  1020. * @function
  1021. * @private
  1022. * @param {Boolean} isIn - Defines if the animation is in or out.
  1023. * @param {Object} element - jQuery or HTML object to animate.
  1024. * @param {String} animation - CSS class to use.
  1025. * @param {Function} cb - Callback to run when animation is finished.
  1026. */
  1027. function animate(isIn, element, animation, cb) {
  1028. element = $(element).eq(0);
  1029. if (!element.length) return;
  1030. var initClass = isIn ? initClasses[0] : initClasses[1];
  1031. var activeClass = isIn ? activeClasses[0] : activeClasses[1];
  1032. // Set up the animation
  1033. reset();
  1034. element
  1035. .addClass(animation)
  1036. .css('transition', 'none');
  1037. requestAnimationFrame(() => {
  1038. element.addClass(initClass);
  1039. if (isIn) element.show();
  1040. });
  1041. // Start the animation
  1042. requestAnimationFrame(() => {
  1043. element[0].offsetWidth;
  1044. element
  1045. .css('transition', '')
  1046. .addClass(activeClass);
  1047. });
  1048. // Clean up the animation when it finishes
  1049. element.one(transitionend(element), finish);
  1050. // Hides the element (for out animations), resets the element, and runs a callback
  1051. function finish() {
  1052. if (!isIn) element.hide();
  1053. reset();
  1054. if (cb) cb.apply(element);
  1055. }
  1056. // Resets transitions and removes motion-specific classes
  1057. function reset() {
  1058. element[0].style.transitionDuration = 0;
  1059. element.removeClass(`${initClass} ${activeClass} ${animation}`);
  1060. }
  1061. }
  1062. const Nest = {
  1063. Feather(menu, type = 'zf') {
  1064. menu.attr('role', 'menubar');
  1065. var items = menu.find('li').attr({'role': 'menuitem'}),
  1066. subMenuClass = `is-${type}-submenu`,
  1067. subItemClass = `${subMenuClass}-item`,
  1068. hasSubClass = `is-${type}-submenu-parent`,
  1069. applyAria = (type !== 'accordion'); // Accordions handle their own ARIA attriutes.
  1070. items.each(function() {
  1071. var $item = $(this),
  1072. $sub = $item.children('ul');
  1073. if ($sub.length) {
  1074. $item.addClass(hasSubClass);
  1075. if(applyAria) {
  1076. $item.attr({
  1077. 'aria-haspopup': true,
  1078. 'aria-label': $item.children('a:first').text()
  1079. });
  1080. // Note: Drilldowns behave differently in how they hide, and so need
  1081. // additional attributes. We should look if this possibly over-generalized
  1082. // utility (Nest) is appropriate when we rework menus in 6.4
  1083. if(type === 'drilldown') {
  1084. $item.attr({'aria-expanded': false});
  1085. }
  1086. }
  1087. $sub
  1088. .addClass(`submenu ${subMenuClass}`)
  1089. .attr({
  1090. 'data-submenu': '',
  1091. 'role': 'menubar'
  1092. });
  1093. if(type === 'drilldown') {
  1094. $sub.attr({'aria-hidden': true});
  1095. }
  1096. }
  1097. if ($item.parent('[data-submenu]').length) {
  1098. $item.addClass(`is-submenu-item ${subItemClass}`);
  1099. }
  1100. });
  1101. return;
  1102. },
  1103. Burn(menu, type) {
  1104. var //items = menu.find('li'),
  1105. subMenuClass = `is-${type}-submenu`,
  1106. subItemClass = `${subMenuClass}-item`,
  1107. hasSubClass = `is-${type}-submenu-parent`;
  1108. menu
  1109. .find('>li, > li > ul, .menu, .menu > li, [data-submenu] > li')
  1110. .removeClass(`${subMenuClass} ${subItemClass} ${hasSubClass} is-submenu-item submenu is-active`)
  1111. .removeAttr('data-submenu').css('display', '');
  1112. }
  1113. };
  1114. function Timer(elem, options, cb) {
  1115. var _this = this,
  1116. duration = options.duration,//options is an object for easily adding features later.
  1117. nameSpace = Object.keys(elem.data())[0] || 'timer',
  1118. remain = -1,
  1119. start,
  1120. timer;
  1121. this.isPaused = false;
  1122. this.restart = function() {
  1123. remain = -1;
  1124. clearTimeout(timer);
  1125. this.start();
  1126. };
  1127. this.start = function() {
  1128. this.isPaused = false;
  1129. // if(!elem.data('paused')){ return false; }//maybe implement this sanity check if used for other things.
  1130. clearTimeout(timer);
  1131. remain = remain <= 0 ? duration : remain;
  1132. elem.data('paused', false);
  1133. start = Date.now();
  1134. timer = setTimeout(function(){
  1135. if(options.infinite){
  1136. _this.restart();//rerun the timer.
  1137. }
  1138. if (cb && typeof cb === 'function') { cb(); }
  1139. }, remain);
  1140. elem.trigger(`timerstart.zf.${nameSpace}`);
  1141. };
  1142. this.pause = function() {
  1143. this.isPaused = true;
  1144. //if(elem.data('paused')){ return false; }//maybe implement this sanity check if used for other things.
  1145. clearTimeout(timer);
  1146. elem.data('paused', true);
  1147. var end = Date.now();
  1148. remain = remain - (end - start);
  1149. elem.trigger(`timerpaused.zf.${nameSpace}`);
  1150. };
  1151. }
  1152. //**************************************************
  1153. var Touch = {};
  1154. var startPosX,
  1155. startPosY,
  1156. startTime,
  1157. elapsedTime,
  1158. startEvent,
  1159. isMoving = false,
  1160. didMoved = false;
  1161. function onTouchEnd(e) {
  1162. this.removeEventListener('touchmove', onTouchMove);
  1163. this.removeEventListener('touchend', onTouchEnd);
  1164. // If the touch did not move, consider it as a "tap"
  1165. if (!didMoved) {
  1166. var tapEvent = $.Event('tap', startEvent || e);
  1167. $(this).trigger(tapEvent);
  1168. }
  1169. startEvent = null;
  1170. isMoving = false;
  1171. didMoved = false;
  1172. }
  1173. function onTouchMove(e) {
  1174. if ($.spotSwipe.preventDefault) { e.preventDefault(); }
  1175. if(isMoving) {
  1176. var x = e.touches[0].pageX;
  1177. var y = e.touches[0].pageY;
  1178. var dx = startPosX - x;
  1179. var dir;
  1180. didMoved = true;
  1181. elapsedTime = new Date().getTime() - startTime;
  1182. if(Math.abs(dx) >= $.spotSwipe.moveThreshold && elapsedTime <= $.spotSwipe.timeThreshold) {
  1183. dir = dx > 0 ? 'left' : 'right';
  1184. }
  1185. // else if(Math.abs(dy) >= $.spotSwipe.moveThreshold && elapsedTime <= $.spotSwipe.timeThreshold) {
  1186. // dir = dy > 0 ? 'down' : 'up';
  1187. // }
  1188. if(dir) {
  1189. e.preventDefault();
  1190. onTouchEnd.apply(this, arguments);
  1191. $(this)
  1192. .trigger($.Event('swipe', e), dir)
  1193. .trigger($.Event(`swipe${dir}`, e));
  1194. }
  1195. }
  1196. }
  1197. function onTouchStart(e) {
  1198. if (e.touches.length == 1) {
  1199. startPosX = e.touches[0].pageX;
  1200. startPosY = e.touches[0].pageY;
  1201. startEvent = e;
  1202. isMoving = true;
  1203. didMoved = false;
  1204. startTime = new Date().getTime();
  1205. this.addEventListener('touchmove', onTouchMove, false);
  1206. this.addEventListener('touchend', onTouchEnd, false);
  1207. }
  1208. }
  1209. function init() {
  1210. this.addEventListener && this.addEventListener('touchstart', onTouchStart, false);
  1211. }
  1212. class SpotSwipe {
  1213. constructor($$$1) {
  1214. this.version = '1.0.0';
  1215. this.enabled = 'ontouchstart' in document.documentElement;
  1216. this.preventDefault = false;
  1217. this.moveThreshold = 75;
  1218. this.timeThreshold = 200;
  1219. this.$ = $$$1;
  1220. this._init();
  1221. }
  1222. _init() {
  1223. var $$$1 = this.$;
  1224. $$$1.event.special.swipe = { setup: init };
  1225. $$$1.event.special.tap = { setup: init };
  1226. $$$1.each(['left', 'up', 'down', 'right'], function () {
  1227. $$$1.event.special[`swipe${this}`] = { setup: function(){
  1228. $$$1(this).on('swipe', $$$1.noop);
  1229. } };
  1230. });
  1231. }
  1232. }
  1233. /****************************************************
  1234. * As far as I can tell, both setupSpotSwipe and *
  1235. * setupTouchHandler should be idempotent, *
  1236. * because they directly replace functions & *
  1237. * values, and do not add event handlers directly. *
  1238. ****************************************************/
  1239. Touch.setupSpotSwipe = function($$$1) {
  1240. $$$1.spotSwipe = new SpotSwipe($$$1);
  1241. };
  1242. /****************************************************
  1243. * Method for adding pseudo drag events to elements *
  1244. ***************************************************/
  1245. Touch.setupTouchHandler = function($$$1) {
  1246. $$$1.fn.addTouch = function(){
  1247. this.each(function(i,el){
  1248. $$$1(el).bind('touchstart touchmove touchend touchcancel', function(event) {
  1249. //we pass the original event object because the jQuery event
  1250. //object is normalized to w3c specs and does not provide the TouchList
  1251. handleTouch(event);
  1252. });
  1253. });
  1254. var handleTouch = function(event){
  1255. var touches = event.changedTouches,
  1256. first = touches[0],
  1257. eventTypes = {
  1258. touchstart: 'mousedown',
  1259. touchmove: 'mousemove',
  1260. touchend: 'mouseup'
  1261. },
  1262. type = eventTypes[event.type],
  1263. simulatedEvent
  1264. ;
  1265. if('MouseEvent' in window && typeof window.MouseEvent === 'function') {
  1266. simulatedEvent = new window.MouseEvent(type, {
  1267. 'bubbles': true,
  1268. 'cancelable': true,
  1269. 'screenX': first.screenX,
  1270. 'screenY': first.screenY,
  1271. 'clientX': first.clientX,
  1272. 'clientY': first.clientY
  1273. });
  1274. } else {
  1275. simulatedEvent = document.createEvent('MouseEvent');
  1276. simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0/*left*/, null);
  1277. }
  1278. first.target.dispatchEvent(simulatedEvent);
  1279. };
  1280. };
  1281. };
  1282. Touch.init = function ($$$1) {
  1283. if(typeof($$$1.spotSwipe) === 'undefined') {
  1284. Touch.setupSpotSwipe($$$1);
  1285. Touch.setupTouchHandler($$$1);
  1286. }
  1287. };
  1288. const MutationObserver = (function () {
  1289. var prefixes = ['WebKit', 'Moz', 'O', 'Ms', ''];
  1290. for (var i=0; i < prefixes.length; i++) {
  1291. if (`${prefixes[i]}MutationObserver` in window) {
  1292. return window[`${prefixes[i]}MutationObserver`];
  1293. }
  1294. }
  1295. return false;
  1296. }());
  1297. const triggers = (el, type) => {
  1298. el.data(type).split(' ').forEach(id => {
  1299. $(`#${id}`)[ type === 'close' ? 'trigger' : 'triggerHandler'](`${type}.zf.trigger`, [el]);
  1300. });
  1301. };
  1302. var Triggers = {
  1303. Listeners: {
  1304. Basic: {},
  1305. Global: {}
  1306. },
  1307. Initializers: {}
  1308. };
  1309. Triggers.Listeners.Basic = {
  1310. openListener: function() {
  1311. triggers($(this), 'open');
  1312. },
  1313. closeListener: function() {
  1314. let id = $(this).data('close');
  1315. if (id) {
  1316. triggers($(this), 'close');
  1317. }
  1318. else {
  1319. $(this).trigger('close.zf.trigger');
  1320. }
  1321. },
  1322. toggleListener: function() {
  1323. let id = $(this).data('toggle');
  1324. if (id) {
  1325. triggers($(this), 'toggle');
  1326. } else {
  1327. $(this).trigger('toggle.zf.trigger');
  1328. }
  1329. },
  1330. closeableListener: function(e) {
  1331. e.stopPropagation();
  1332. let animation = $(this).data('closable');
  1333. if(animation !== ''){
  1334. Motion.animateOut($(this), animation, function() {
  1335. $(this).trigger('closed.zf');
  1336. });
  1337. }else{
  1338. $(this).fadeOut().trigger('closed.zf');
  1339. }
  1340. },
  1341. toggleFocusListener: function() {
  1342. let id = $(this).data('toggle-focus');
  1343. $(`#${id}`).triggerHandler('toggle.zf.trigger', [$(this)]);
  1344. }
  1345. };
  1346. // Elements with [data-open] will reveal a plugin that supports it when clicked.
  1347. Triggers.Initializers.addOpenListener = ($elem) => {
  1348. $elem.off('click.zf.trigger', Triggers.Listeners.Basic.openListener);
  1349. $elem.on('click.zf.trigger', '[data-open]', Triggers.Listeners.Basic.openListener);
  1350. };
  1351. // Elements with [data-close] will close a plugin that supports it when clicked.
  1352. // If used without a value on [data-close], the event will bubble, allowing it to close a parent component.
  1353. Triggers.Initializers.addCloseListener = ($elem) => {
  1354. $elem.off('click.zf.trigger', Triggers.Listeners.Basic.closeListener);
  1355. $elem.on('click.zf.trigger', '[data-close]', Triggers.Listeners.Basic.closeListener);
  1356. };
  1357. // Elements with [data-toggle] will toggle a plugin that supports it when clicked.
  1358. Triggers.Initializers.addToggleListener = ($elem) => {
  1359. $elem.off('click.zf.trigger', Triggers.Listeners.Basic.toggleListener);
  1360. $elem.on('click.zf.trigger', '[data-toggle]', Triggers.Listeners.Basic.toggleListener);
  1361. };
  1362. // Elements with [data-closable] will respond to close.zf.trigger events.
  1363. Triggers.Initializers.addCloseableListener = ($elem) => {
  1364. $elem.off('close.zf.trigger', Triggers.Listeners.Basic.closeableListener);
  1365. $elem.on('close.zf.trigger', '[data-closeable], [data-closable]', Triggers.Listeners.Basic.closeableListener);
  1366. };
  1367. // Elements with [data-toggle-focus] will respond to coming in and out of focus
  1368. Triggers.Initializers.addToggleFocusListener = ($elem) => {
  1369. $elem.off('focus.zf.trigger blur.zf.trigger', Triggers.Listeners.Basic.toggleFocusListener);
  1370. $elem.on('focus.zf.trigger blur.zf.trigger', '[data-toggle-focus]', Triggers.Listeners.Basic.toggleFocusListener);
  1371. };
  1372. // More Global/complex listeners and triggers
  1373. Triggers.Listeners.Global = {
  1374. resizeListener: function($nodes) {
  1375. if(!MutationObserver){//fallback for IE 9
  1376. $nodes.each(function(){
  1377. $(this).triggerHandler('resizeme.zf.trigger');
  1378. });
  1379. }
  1380. //trigger all listening elements and signal a resize event
  1381. $nodes.attr('data-events', "resize");
  1382. },
  1383. scrollListener: function($nodes) {
  1384. if(!MutationObserver){//fallback for IE 9
  1385. $nodes.each(function(){
  1386. $(this).triggerHandler('scrollme.zf.trigger');
  1387. });
  1388. }
  1389. //trigger all listening elements and signal a scroll event
  1390. $nodes.attr('data-events', "scroll");
  1391. },
  1392. closeMeListener: function(e, pluginId){
  1393. let plugin = e.namespace.split('.')[0];
  1394. let plugins = $(`[data-${plugin}]`).not(`[data-yeti-box="${pluginId}"]`);
  1395. plugins.each(function(){
  1396. let _this = $(this);
  1397. _this.triggerHandler('close.zf.trigger', [_this]);
  1398. });
  1399. }
  1400. };
  1401. // Global, parses whole document.
  1402. Triggers.Initializers.addClosemeListener = function(pluginName) {
  1403. var yetiBoxes = $('[data-yeti-box]'),
  1404. plugNames = ['dropdown', 'tooltip', 'reveal'];
  1405. if(pluginName){
  1406. if(typeof pluginName === 'string'){
  1407. plugNames.push(pluginName);
  1408. }else if(typeof pluginName === 'object' && typeof pluginName[0] === 'string'){
  1409. plugNames = plugNames.concat(pluginName);
  1410. }else{
  1411. console.error('Plugin names must be strings');
  1412. }
  1413. }
  1414. if(yetiBoxes.length){
  1415. let listeners = plugNames.map((name) => {
  1416. return `closeme.zf.${name}`;
  1417. }).join(' ');
  1418. $(window).off(listeners).on(listeners, Triggers.Listeners.Global.closeMeListener);
  1419. }
  1420. };
  1421. function debounceGlobalListener(debounce, trigger, listener) {
  1422. let timer, args = Array.prototype.slice.call(arguments, 3);
  1423. $(window).off(trigger).on(trigger, function(e) {
  1424. if (timer) { clearTimeout(timer); }
  1425. timer = setTimeout(function(){
  1426. listener.apply(null, args);
  1427. }, debounce || 10);//default time to emit scroll event
  1428. });
  1429. }
  1430. Triggers.Initializers.addResizeListener = function(debounce){
  1431. let $nodes = $('[data-resize]');
  1432. if($nodes.length){
  1433. debounceGlobalListener(debounce, 'resize.zf.trigger', Triggers.Listeners.Global.resizeListener, $nodes);
  1434. }
  1435. };
  1436. Triggers.Initializers.addScrollListener = function(debounce){
  1437. let $nodes = $('[data-scroll]');
  1438. if($nodes.length){
  1439. debounceGlobalListener(debounce, 'scroll.zf.trigger', Triggers.Listeners.Global.scrollListener, $nodes);
  1440. }
  1441. };
  1442. Triggers.Initializers.addMutationEventsListener = function($elem) {
  1443. if(!MutationObserver){ return false; }
  1444. let $nodes = $elem.find('[data-resize], [data-scroll], [data-mutate]');
  1445. //element callback
  1446. var listeningElementsMutation = function (mutationRecordsList) {
  1447. var $target = $(mutationRecordsList[0].target);
  1448. //trigger the event handler for the element depending on type
  1449. switch (mutationRecordsList[0].type) {
  1450. case "attributes":
  1451. if ($target.attr("data-events") === "scroll" && mutationRecordsList[0].attributeName === "data-events") {
  1452. $target.triggerHandler('scrollme.zf.trigger', [$target, window.pageYOffset]);
  1453. }
  1454. if ($target.attr("data-events") === "resize" && mutationRecordsList[0].attributeName === "data-events") {
  1455. $target.triggerHandler('resizeme.zf.trigger', [$target]);
  1456. }
  1457. if (mutationRecordsList[0].attributeName === "style") {
  1458. $target.closest("[data-mutate]").attr("data-events","mutate");
  1459. $target.closest("[data-mutate]").triggerHandler('mutateme.zf.trigger', [$target.closest("[data-mutate]")]);
  1460. }
  1461. break;
  1462. case "childList":
  1463. $target.closest("[data-mutate]").attr("data-events","mutate");
  1464. $target.closest("[data-mutate]").triggerHandler('mutateme.zf.trigger', [$target.closest("[data-mutate]")]);
  1465. break;
  1466. default:
  1467. return false;
  1468. //nothing
  1469. }
  1470. };
  1471. if ($nodes.length) {
  1472. //for each element that needs to listen for resizing, scrolling, or mutation add a single observer
  1473. for (var i = 0; i <= $nodes.length - 1; i++) {
  1474. var elementObserver = new MutationObserver(listeningElementsMutation);
  1475. elementObserver.observe($nodes[i], { attributes: true, childList: true, characterData: false, subtree: true, attributeFilter: ["data-events", "style"] });
  1476. }
  1477. }
  1478. };
  1479. Triggers.Initializers.addSimpleListeners = function() {
  1480. let $document = $(document);
  1481. Triggers.Initializers.addOpenListener($document);
  1482. Triggers.Initializers.addCloseListener($document);
  1483. Triggers.Initializers.addToggleListener($document);
  1484. Triggers.Initializers.addCloseableListener($document);
  1485. Triggers.Initializers.addToggleFocusListener($document);
  1486. };
  1487. Triggers.Initializers.addGlobalListeners = function() {
  1488. let $document = $(document);
  1489. Triggers.Initializers.addMutationEventsListener($document);
  1490. Triggers.Initializers.addResizeListener();
  1491. Triggers.Initializers.addScrollListener();
  1492. Triggers.Initializers.addClosemeListener();
  1493. };
  1494. Triggers.init = function ($$$1, Foundation) {
  1495. onLoad($$$1(window), function () {
  1496. if ($$$1.triggersInitialized !== true) {
  1497. Triggers.Initializers.addSimpleListeners();
  1498. Triggers.Initializers.addGlobalListeners();
  1499. $$$1.triggersInitialized = true;
  1500. }
  1501. });
  1502. if(Foundation) {
  1503. Foundation.Triggers = Triggers;
  1504. // Legacy included to be backwards compatible for now.
  1505. Foundation.IHearYou = Triggers.Initializers.addGlobalListeners;
  1506. }
  1507. };
  1508. // Abstract class for providing lifecycle hooks. Expect plugins to define AT LEAST
  1509. // {function} _setup (replaces previous constructor),
  1510. // {function} _destroy (replaces previous destroy)
  1511. class Plugin {
  1512. constructor(element, options) {
  1513. this._setup(element, options);
  1514. var pluginName = getPluginName(this);
  1515. this.uuid = GetYoDigits(6, pluginName);
  1516. if(!this.$element.attr(`data-${pluginName}`)){ this.$element.attr(`data-${pluginName}`, this.uuid); }
  1517. if(!this.$element.data('zfPlugin')){ this.$element.data('zfPlugin', this); }
  1518. /**
  1519. * Fires when the plugin has initialized.
  1520. * @event Plugin#init
  1521. */
  1522. this.$element.trigger(`init.zf.${pluginName}`);
  1523. }
  1524. destroy() {
  1525. this._destroy();
  1526. var pluginName = getPluginName(this);
  1527. this.$element.removeAttr(`data-${pluginName}`).removeData('zfPlugin')
  1528. /**
  1529. * Fires when the plugin has been destroyed.
  1530. * @event Plugin#destroyed
  1531. */
  1532. .trigger(`destroyed.zf.${pluginName}`);
  1533. for(var prop in this){
  1534. this[prop] = null;//clean up script to prep for garbage collection.
  1535. }
  1536. }
  1537. }
  1538. // Convert PascalCase to kebab-case
  1539. // Thank you: http://stackoverflow.com/a/8955580
  1540. function hyphenate$1(str) {
  1541. return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  1542. }
  1543. function getPluginName(obj) {
  1544. if(typeof(obj.constructor.name) !== 'undefined') {
  1545. return hyphenate$1(obj.constructor.name);
  1546. } else {
  1547. return hyphenate$1(obj.className);
  1548. }
  1549. }
  1550. /**
  1551. * Abide module.
  1552. * @module foundation.abide
  1553. */
  1554. class Abide extends Plugin {
  1555. /**
  1556. * Creates a new instance of Abide.
  1557. * @class
  1558. * @name Abide
  1559. * @fires Abide#init
  1560. * @param {Object} element - jQuery object to add the trigger to.
  1561. * @param {Object} options - Overrides to the default plugin settings.
  1562. */
  1563. _setup(element, options = {}) {
  1564. this.$element = element;
  1565. this.options = $.extend(true, {}, Abide.defaults, this.$element.data(), options);
  1566. this.className = 'Abide'; // ie9 back compat
  1567. this._init();
  1568. }
  1569. /**
  1570. * Initializes the Abide plugin and calls functions to get Abide functioning on load.
  1571. * @private
  1572. */
  1573. _init() {
  1574. this.$inputs = $.merge( // Consider as input to validate:
  1575. this.$element.find('input').not('[type=submit]'), // * all input fields expect submit
  1576. this.$element.find('textarea, select') // * all textareas and select fields
  1577. );
  1578. const $globalErrors = this.$element.find('[data-abide-error]');
  1579. // Add a11y attributes to all fields
  1580. if (this.options.a11yAttributes) {
  1581. this.$inputs.each((i, input) => this.addA11yAttributes($(input)));
  1582. $globalErrors.each((i, error) => this.addGlobalErrorA11yAttributes($(error)));
  1583. }
  1584. this._events();
  1585. }
  1586. /**
  1587. * Initializes events for Abide.
  1588. * @private
  1589. */
  1590. _events() {
  1591. this.$element.off('.abide')
  1592. .on('reset.zf.abide', () => {
  1593. this.resetForm();
  1594. })
  1595. .on('submit.zf.abide', () => {
  1596. return this.validateForm();
  1597. });
  1598. if (this.options.validateOn === 'fieldChange') {
  1599. this.$inputs
  1600. .off('change.zf.abide')
  1601. .on('change.zf.abide', (e) => {
  1602. this.validateInput($(e.target));
  1603. });
  1604. }
  1605. if (this.options.liveValidate) {
  1606. this.$inputs
  1607. .off('input.zf.abide')
  1608. .on('input.zf.abide', (e) => {
  1609. this.validateInput($(e.target));
  1610. });
  1611. }
  1612. if (this.options.validateOnBlur) {
  1613. this.$inputs
  1614. .off('blur.zf.abide')
  1615. .on('blur.zf.abide', (e) => {
  1616. this.validateInput($(e.target));
  1617. });
  1618. }
  1619. }
  1620. /**
  1621. * Calls necessary functions to update Abide upon DOM change
  1622. * @private
  1623. */
  1624. _reflow() {
  1625. this._init();
  1626. }
  1627. /**
  1628. * Checks whether or not a form element has the required attribute and if it's checked or not
  1629. * @param {Object} element - jQuery object to check for required attribute
  1630. * @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
  1631. */
  1632. requiredCheck($el) {
  1633. if (!$el.attr('required')) return true;
  1634. var isGood = true;
  1635. switch ($el[0].type) {
  1636. case 'checkbox':
  1637. isGood = $el[0].checked;
  1638. break;
  1639. case 'select':
  1640. case 'select-one':
  1641. case 'select-multiple':
  1642. var opt = $el.find('option:selected');
  1643. if (!opt.length || !opt.val()) isGood = false;
  1644. break;
  1645. default:
  1646. if(!$el.val() || !$el.val().length) isGood = false;
  1647. }
  1648. return isGood;
  1649. }
  1650. /**
  1651. * Get:
  1652. * - Based on $el, the first element(s) corresponding to `formErrorSelector` in this order:
  1653. * 1. The element's direct sibling('s).
  1654. * 2. The element's parent's children.
  1655. * - Element(s) with the attribute `[data-form-error-for]` set with the element's id.
  1656. *
  1657. * This allows for multiple form errors per input, though if none are found, no form errors will be shown.
  1658. *
  1659. * @param {Object} $el - jQuery object to use as reference to find the form error selector.
  1660. * @returns {Object} jQuery object with the selector.
  1661. */
  1662. findFormError($el) {
  1663. var id = $el[0].id;
  1664. var $error = $el.siblings(this.options.formErrorSelector);
  1665. if (!$error.length) {
  1666. $error = $el.parent().find(this.options.formErrorSelector);
  1667. }
  1668. if (id) {
  1669. $error = $error.add(this.$element.find(`[data-form-error-for="${id}"]`));
  1670. }
  1671. return $error;
  1672. }
  1673. /**
  1674. * Get the first element in this order:
  1675. * 2. The <label> with the attribute `[for="someInputId"]`
  1676. * 3. The `.closest()` <label>
  1677. *
  1678. * @param {Object} $el - jQuery object to check for required attribute
  1679. * @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
  1680. */
  1681. findLabel($el) {
  1682. var id = $el[0].id;
  1683. var $label = this.$element.find(`label[for="${id}"]`);
  1684. if (!$label.length) {
  1685. return $el.closest('label');
  1686. }
  1687. return $label;
  1688. }
  1689. /**
  1690. * Get the set of labels associated with a set of radio els in this order
  1691. * 2. The <label> with the attribute `[for="someInputId"]`
  1692. * 3. The `.closest()` <label>
  1693. *
  1694. * @param {Object} $el - jQuery object to check for required attribute
  1695. * @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
  1696. */
  1697. findRadioLabels($els) {
  1698. var labels = $els.map((i, el) => {
  1699. var id = el.id;
  1700. var $label = this.$element.find(`label[for="${id}"]`);
  1701. if (!$label.length) {
  1702. $label = $(el).closest('label');
  1703. }
  1704. return $label[0];
  1705. });
  1706. return $(labels);
  1707. }
  1708. /**
  1709. * Adds the CSS error class as specified by the Abide settings to the label, input, and the form
  1710. * @param {Object} $el - jQuery object to add the class to
  1711. */
  1712. addErrorClasses($el) {
  1713. var $label = this.findLabel($el);
  1714. var $formError = this.findFormError($el);
  1715. if ($label.length) {
  1716. $label.addClass(this.options.labelErrorClass);
  1717. }
  1718. if ($formError.length) {
  1719. $formError.addClass(this.options.formErrorClass);
  1720. }
  1721. $el.addClass(this.options.inputErrorClass).attr({
  1722. 'data-invalid': '',
  1723. 'aria-invalid': true
  1724. });
  1725. }
  1726. /**
  1727. * Adds [for] and [role=alert] attributes to all form error targetting $el,
  1728. * and [aria-describedby] attribute to $el toward the first form error.
  1729. * @param {Object} $el - jQuery object
  1730. */
  1731. addA11yAttributes($el) {
  1732. let $errors = this.findFormError($el);
  1733. let $labels = $errors.filter('label');
  1734. let $error = $errors.first();
  1735. if (!$errors.length) return;
  1736. // Set [aria-describedby] on the input toward the first form error if it is not set
  1737. if (typeof $el.attr('aria-describedby') === 'undefined') {
  1738. // Get the first error ID or create one
  1739. let errorId = $error.attr('id');
  1740. if (typeof errorId === 'undefined') {
  1741. errorId = GetYoDigits(6, 'abide-error');
  1742. $error.attr('id', errorId);
  1743. }
  1744. $el.attr('aria-describedby', errorId);
  1745. }
  1746. if ($labels.filter('[for]').length < $labels.length) {
  1747. // Get the input ID or create one
  1748. let elemId = $el.attr('id');
  1749. if (typeof elemId === 'undefined') {
  1750. elemId = GetYoDigits(6, 'abide-input');
  1751. $el.attr('id', elemId);
  1752. }
  1753. // For each label targeting $el, set [for] if it is not set.
  1754. $labels.each((i, label) => {
  1755. const $label = $(label);
  1756. if (typeof $label.attr('for') === 'undefined')
  1757. $label.attr('for', elemId);
  1758. });
  1759. }
  1760. // For each error targeting $el, set [role=alert] if it is not set.
  1761. $errors.each((i, label) => {
  1762. const $label = $(label);
  1763. if (typeof $label.attr('role') === 'undefined')
  1764. $label.attr('role', 'alert');
  1765. }).end();
  1766. }
  1767. /**
  1768. * Adds [aria-live] attribute to the given global form error $el.
  1769. * @param {Object} $el - jQuery object to add the attribute to
  1770. */
  1771. addGlobalErrorA11yAttributes($el) {
  1772. if (typeof $el.attr('aria-live') === 'undefined')
  1773. $el.attr('aria-live', this.options.a11yErrorLevel);
  1774. }
  1775. /**
  1776. * Remove CSS error classes etc from an entire radio button group
  1777. * @param {String} groupName - A string that specifies the name of a radio button group
  1778. *
  1779. */
  1780. removeRadioErrorClasses(groupName) {
  1781. var $els = this.$element.find(`:radio[name="${groupName}"]`);
  1782. var $labels = this.findRadioLabels($els);
  1783. var $formErrors = this.findFormError($els);
  1784. if ($labels.length) {
  1785. $labels.removeClass(this.options.labelErrorClass);
  1786. }
  1787. if ($formErrors.length) {
  1788. $formErrors.removeClass(this.options.formErrorClass);
  1789. }
  1790. $els.removeClass(this.options.inputErrorClass).attr({
  1791. 'data-invalid': null,
  1792. 'aria-invalid': null
  1793. });
  1794. }
  1795. /**
  1796. * Removes CSS error class as specified by the Abide settings from the label, input, and the form
  1797. * @param {Object} $el - jQuery object to remove the class from
  1798. */
  1799. removeErrorClasses($el) {
  1800. // radios need to clear all of the els
  1801. if($el[0].type == 'radio') {
  1802. return this.removeRadioErrorClasses($el.attr('name'));
  1803. }
  1804. var $label = this.findLabel($el);
  1805. var $formError = this.findFormError($el);
  1806. if ($label.length) {
  1807. $label.removeClass(this.options.labelErrorClass);
  1808. }
  1809. if ($formError.length) {
  1810. $formError.removeClass(this.options.formErrorClass);
  1811. }
  1812. $el.removeClass(this.options.inputErrorClass).attr({
  1813. 'data-invalid': null,
  1814. 'aria-invalid': null
  1815. });
  1816. }
  1817. /**
  1818. * Goes through a form to find inputs and proceeds to validate them in ways specific to their type.
  1819. * Ignores inputs with data-abide-ignore, type="hidden" or disabled attributes set
  1820. * @fires Abide#invalid
  1821. * @fires Abide#valid
  1822. * @param {Object} element - jQuery object to validate, should be an HTML input
  1823. * @returns {Boolean} goodToGo - If the input is valid or not.
  1824. */
  1825. validateInput($el) {
  1826. var clearRequire = this.requiredCheck($el),
  1827. validated = false,
  1828. customValidator = true,
  1829. validator = $el.attr('data-validator'),
  1830. equalTo = true;
  1831. // don't validate ignored inputs or hidden inputs or disabled inputs
  1832. if ($el.is('[data-abide-ignore]') || $el.is('[type="hidden"]') || $el.is('[disabled]')) {
  1833. return true;
  1834. }
  1835. switch ($el[0].type) {
  1836. case 'radio':
  1837. validated = this.validateRadio($el.attr('name'));
  1838. break;
  1839. case 'checkbox':
  1840. validated = clearRequire;
  1841. break;
  1842. case 'select':
  1843. case 'select-one':
  1844. case 'select-multiple':
  1845. validated = clearRequire;
  1846. break;
  1847. default:
  1848. validated = this.validateText($el);
  1849. }
  1850. if (validator) {
  1851. customValidator = this.matchValidation($el, validator, $el.attr('required'));
  1852. }
  1853. if ($el.attr('data-equalto')) {
  1854. equalTo = this.options.validators.equalTo($el);
  1855. }
  1856. var goodToGo = [clearRequire, validated, customValidator, equalTo].indexOf(false) === -1;
  1857. var message = (goodToGo ? 'valid' : 'invalid') + '.zf.abide';
  1858. if (goodToGo) {
  1859. // Re-validate inputs that depend on this one with equalto
  1860. const dependentElements = this.$element.find(`[data-equalto="${$el.attr('id')}"]`);
  1861. if (dependentElements.length) {
  1862. let _this = this;
  1863. dependentElements.each(function() {
  1864. if ($(this).val()) {
  1865. _this.validateInput($(this));
  1866. }
  1867. });
  1868. }
  1869. }
  1870. this[goodToGo ? 'removeErrorClasses' : 'addErrorClasses']($el);
  1871. /**
  1872. * Fires when the input is done checking for validation. Event trigger is either `valid.zf.abide` or `invalid.zf.abide`
  1873. * Trigger includes the DOM element of the input.
  1874. * @event Abide#valid
  1875. * @event Abide#invalid
  1876. */
  1877. $el.trigger(message, [$el]);
  1878. return goodToGo;
  1879. }
  1880. /**
  1881. * Goes through a form and if there are any invalid inputs, it will display the form error element
  1882. * @returns {Boolean} noError - true if no errors were detected...
  1883. * @fires Abide#formvalid
  1884. * @fires Abide#forminvalid
  1885. */
  1886. validateForm() {
  1887. var acc = [];
  1888. var _this = this;
  1889. this.$inputs.each(function() {
  1890. acc.push(_this.validateInput($(this)));
  1891. });
  1892. var noError = acc.indexOf(false) === -1;
  1893. this.$element.find('[data-abide-error]').each((i, elem) => {
  1894. const $elem = $(elem);
  1895. // Ensure a11y attributes are set
  1896. if (this.options.a11yAttributes) this.addGlobalErrorA11yAttributes($elem);
  1897. // Show or hide the error
  1898. $elem.css('display', (noError ? 'none' : 'block'));
  1899. });
  1900. /**
  1901. * Fires when the form is finished validating. Event trigger is either `formvalid.zf.abide` or `forminvalid.zf.abide`.
  1902. * Trigger includes the element of the form.
  1903. * @event Abide#formvalid
  1904. * @event Abide#forminvalid
  1905. */
  1906. this.$element.trigger((noError ? 'formvalid' : 'forminvalid') + '.zf.abide', [this.$element]);
  1907. return noError;
  1908. }
  1909. /**
  1910. * Determines whether or a not a text input is valid based on the pattern specified in the attribute. If no matching pattern is found, returns true.
  1911. * @param {Object} $el - jQuery object to validate, should be a text input HTML element
  1912. * @param {String} pattern - string value of one of the RegEx patterns in Abide.options.patterns
  1913. * @returns {Boolean} Boolean value depends on whether or not the input value matches the pattern specified
  1914. */
  1915. validateText($el, pattern) {
  1916. // A pattern can be passed to this function, or it will be infered from the input's "pattern" attribute, or it's "type" attribute
  1917. pattern = (pattern || $el.attr('pattern') || $el.attr('type'));
  1918. var inputText = $el.val();
  1919. var valid = false;
  1920. if (inputText.length) {
  1921. // If the pattern attribute on the element is in Abide's list of patterns, then test that regexp
  1922. if (this.options.patterns.hasOwnProperty(pattern)) {
  1923. valid = this.options.patterns[pattern].test(inputText);
  1924. }
  1925. // If the pattern name isn't also the type attribute of the field, then test it as a regexp
  1926. else if (pattern !== $el.attr('type')) {
  1927. valid = new RegExp(pattern).test(inputText);
  1928. }
  1929. else {
  1930. valid = true;
  1931. }
  1932. }
  1933. // An empty field is valid if it's not required
  1934. else if (!$el.prop('required')) {
  1935. valid = true;
  1936. }
  1937. return valid;
  1938. }
  1939. /**
  1940. * Determines whether or a not a radio input is valid based on whether or not it is required and selected. Although the function targets a single `<input>`, it validates by checking the `required` and `checked` properties of all radio buttons in its group.
  1941. * @param {String} groupName - A string that specifies the name of a radio button group
  1942. * @returns {Boolean} Boolean value depends on whether or not at least one radio input has been selected (if it's required)
  1943. */
  1944. validateRadio(groupName) {
  1945. // If at least one radio in the group has the `required` attribute, the group is considered required
  1946. // Per W3C spec, all radio buttons in a group should have `required`, but we're being nice
  1947. var $group = this.$element.find(`:radio[name="${groupName}"]`);
  1948. var valid = false, required = false;
  1949. // For the group to be required, at least one radio needs to be required
  1950. $group.each((i, e) => {
  1951. if ($(e).attr('required')) {
  1952. required = true;
  1953. }
  1954. });
  1955. if(!required) valid=true;
  1956. if (!valid) {
  1957. // For the group to be valid, at least one radio needs to be checked
  1958. $group.each((i, e) => {
  1959. if ($(e).prop('checked')) {
  1960. valid = true;
  1961. }
  1962. });
  1963. }
  1964. return valid;
  1965. }
  1966. /**
  1967. * Determines if a selected input passes a custom validation function. Multiple validations can be used, if passed to the element with `data-validator="foo bar baz"` in a space separated listed.
  1968. * @param {Object} $el - jQuery input element.
  1969. * @param {String} validators - a string of function names matching functions in the Abide.options.validators object.
  1970. * @param {Boolean} required - self explanatory?
  1971. * @returns {Boolean} - true if validations passed.
  1972. */
  1973. matchValidation($el, validators, required) {
  1974. required = required ? true : false;
  1975. var clear = validators.split(' ').map((v) => {
  1976. return this.options.validators[v]($el, required, $el.parent());
  1977. });
  1978. return clear.indexOf(false) === -1;
  1979. }
  1980. /**
  1981. * Resets form inputs and styles
  1982. * @fires Abide#formreset
  1983. */
  1984. resetForm() {
  1985. var $form = this.$element,
  1986. opts = this.options;
  1987. $(`.${opts.labelErrorClass}`, $form).not('small').removeClass(opts.labelErrorClass);
  1988. $(`.${opts.inputErrorClass}`, $form).not('small').removeClass(opts.inputErrorClass);
  1989. $(`${opts.formErrorSelector}.${opts.formErrorClass}`).removeClass(opts.formErrorClass);
  1990. $form.find('[data-abide-error]').css('display', 'none');
  1991. $(':input', $form).not(':button, :submit, :reset, :hidden, :radio, :checkbox, [data-abide-ignore]').val('').attr({
  1992. 'data-invalid': null,
  1993. 'aria-invalid': null
  1994. });
  1995. $(':input:radio', $form).not('[data-abide-ignore]').prop('checked',false).attr({
  1996. 'data-invalid': null,
  1997. 'aria-invalid': null
  1998. });
  1999. $(':input:checkbox', $form).not('[data-abide-ignore]').prop('checked',false).attr({
  2000. 'data-invalid': null,
  2001. 'aria-invalid': null
  2002. });
  2003. /**
  2004. * Fires when the form has been reset.
  2005. * @event Abide#formreset
  2006. */
  2007. $form.trigger('formreset.zf.abide', [$form]);
  2008. }
  2009. /**
  2010. * Destroys an instance of Abide.
  2011. * Removes error styles and classes from elements, without resetting their values.
  2012. */
  2013. _destroy() {
  2014. var _this = this;
  2015. this.$element
  2016. .off('.abide')
  2017. .find('[data-abide-error]')
  2018. .css('display', 'none');
  2019. this.$inputs
  2020. .off('.abide')
  2021. .each(function() {
  2022. _this.removeErrorClasses($(this));
  2023. });
  2024. }
  2025. }
  2026. /**
  2027. * Default settings for plugin
  2028. */
  2029. Abide.defaults = {
  2030. /**
  2031. * The default event to validate inputs. Checkboxes and radios validate immediately.
  2032. * Remove or change this value for manual validation.
  2033. * @option
  2034. * @type {?string}
  2035. * @default 'fieldChange'
  2036. */
  2037. validateOn: 'fieldChange',
  2038. /**
  2039. * Class to be applied to input labels on failed validation.
  2040. * @option
  2041. * @type {string}
  2042. * @default 'is-invalid-label'
  2043. */
  2044. labelErrorClass: 'is-invalid-label',
  2045. /**
  2046. * Class to be applied to inputs on failed validation.
  2047. * @option
  2048. * @type {string}
  2049. * @default 'is-invalid-input'
  2050. */
  2051. inputErrorClass: 'is-invalid-input',
  2052. /**
  2053. * Class selector to use to target Form Errors for show/hide.
  2054. * @option
  2055. * @type {string}
  2056. * @default '.form-error'
  2057. */
  2058. formErrorSelector: '.form-error',
  2059. /**
  2060. * Class added to Form Errors on failed validation.
  2061. * @option
  2062. * @type {string}
  2063. * @default 'is-visible'
  2064. */
  2065. formErrorClass: 'is-visible',
  2066. /**
  2067. * If true, automatically insert when possible:
  2068. * - `[aria-describedby]` on fields
  2069. * - `[role=alert]` on form errors and `[for]` on form error labels
  2070. * - `[aria-live]` on global errors `[data-abide-error]` (see option `a11yErrorLevel`).
  2071. * @option
  2072. * @type {boolean}
  2073. * @default true
  2074. */
  2075. a11yAttributes: true,
  2076. /**
  2077. * [aria-live] attribute value to be applied on global errors `[data-abide-error]`.
  2078. * Options are: 'assertive', 'polite' and 'off'/null
  2079. * @option
  2080. * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
  2081. * @type {string}
  2082. * @default 'assertive'
  2083. */
  2084. a11yErrorLevel: 'assertive',
  2085. /**
  2086. * Set to true to validate text inputs on any value change.
  2087. * @option
  2088. * @type {boolean}
  2089. * @default false
  2090. */
  2091. liveValidate: false,
  2092. /**
  2093. * Set to true to validate inputs on blur.
  2094. * @option
  2095. * @type {boolean}
  2096. * @default false
  2097. */
  2098. validateOnBlur: false,
  2099. patterns: {
  2100. alpha : /^[a-zA-Z]+$/,
  2101. alpha_numeric : /^[a-zA-Z0-9]+$/,
  2102. integer : /^[-+]?\d+$/,
  2103. number : /^[-+]?\d*(?:[\.\,]\d+)?$/,
  2104. // amex, visa, diners
  2105. card : /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(?:222[1-9]|2[3-6][0-9]{2}|27[0-1][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/,
  2106. cvv : /^([0-9]){3,4}$/,
  2107. // http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
  2108. email : /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/,
  2109. // From CommonRegexJS (@talyssonoc)
  2110. // https://github.com/talyssonoc/CommonRegexJS/blob/e2901b9f57222bc14069dc8f0598d5f412555411/lib/commonregex.js#L76
  2111. // For more restrictive URL Regexs, see https://mathiasbynens.be/demo/url-regex.
  2112. url: /^((?:(https?|ftps?|file|ssh|sftp):\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))$/,
  2113. // abc.de
  2114. domain : /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,8}$/,
  2115. datetime : /^([0-2][0-9]{3})\-([0-1][0-9])\-([0-3][0-9])T([0-5][0-9])\:([0-5][0-9])\:([0-5][0-9])(Z|([\-\+]([0-1][0-9])\:00))$/,
  2116. // YYYY-MM-DD
  2117. date : /(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))$/,
  2118. // HH:MM:SS
  2119. time : /^(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}$/,
  2120. dateISO : /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/,
  2121. // MM/DD/YYYY
  2122. month_day_year : /^(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.]\d{4}$/,
  2123. // DD/MM/YYYY
  2124. day_month_year : /^(0[1-9]|[12][0-9]|3[01])[- \/.](0[1-9]|1[012])[- \/.]\d{4}$/,
  2125. // #FFF or #FFFFFF
  2126. color : /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/,
  2127. // Domain || URL
  2128. website: {
  2129. test: (text) => {
  2130. return Abide.defaults.patterns['domain'].test(text) || Abide.defaults.patterns['url'].test(text);
  2131. }
  2132. }
  2133. },
  2134. /**
  2135. * Optional validation functions to be used. `equalTo` being the only default included function.
  2136. * Functions should return only a boolean if the input is valid or not. Functions are given the following arguments:
  2137. * el : The jQuery element to validate.
  2138. * required : Boolean value of the required attribute be present or not.
  2139. * parent : The direct parent of the input.
  2140. * @option
  2141. */
  2142. validators: {
  2143. equalTo: function (el, required, parent) {
  2144. return $(`#${el.attr('data-equalto')}`).val() === el.val();
  2145. }
  2146. }
  2147. };
  2148. /**
  2149. * Accordion module.
  2150. * @module foundation.accordion
  2151. * @requires foundation.util.keyboard
  2152. */
  2153. class Accordion extends Plugin {
  2154. /**
  2155. * Creates a new instance of an accordion.
  2156. * @class
  2157. * @name Accordion
  2158. * @fires Accordion#init
  2159. * @param {jQuery} element - jQuery object to make into an accordion.
  2160. * @param {Object} options - a plain object with settings to override the default options.
  2161. */
  2162. _setup(element, options) {
  2163. this.$element = element;
  2164. this.options = $.extend({}, Accordion.defaults, this.$element.data(), options);
  2165. this.className = 'Accordion'; // ie9 back compat
  2166. this._init();
  2167. Keyboard.register('Accordion', {
  2168. 'ENTER': 'toggle',
  2169. 'SPACE': 'toggle',
  2170. 'ARROW_DOWN': 'next',
  2171. 'ARROW_UP': 'previous'
  2172. });
  2173. }
  2174. /**
  2175. * Initializes the accordion by animating the preset active pane(s).
  2176. * @private
  2177. */
  2178. _init() {
  2179. this._isInitializing = true;
  2180. this.$element.attr('role', 'tablist');
  2181. this.$tabs = this.$element.children('[data-accordion-item]');
  2182. this.$tabs.each(function(idx, el) {
  2183. var $el = $(el),
  2184. $content = $el.children('[data-tab-content]'),
  2185. id = $content[0].id || GetYoDigits(6, 'accordion'),
  2186. linkId = (el.id) ? `${el.id}-label` : `${id}-label`;
  2187. $el.find('a:first').attr({
  2188. 'aria-controls': id,
  2189. 'role': 'tab',
  2190. 'id': linkId,
  2191. 'aria-expanded': false,
  2192. 'aria-selected': false
  2193. });
  2194. $content.attr({'role': 'tabpanel', 'aria-labelledby': linkId, 'aria-hidden': true, 'id': id});
  2195. });
  2196. var $initActive = this.$element.find('.is-active').children('[data-tab-content]');
  2197. if ($initActive.length) {
  2198. // Save up the initial hash to return to it later when going back in history
  2199. this._initialAnchor = $initActive.prev('a').attr('href');
  2200. this._openSingleTab($initActive);
  2201. }
  2202. this._checkDeepLink = () => {
  2203. var anchor = window.location.hash;
  2204. if (!anchor.length) {
  2205. // If we are still initializing and there is no anchor, then there is nothing to do
  2206. if (this._isInitializing) return;
  2207. // Otherwise, move to the initial anchor
  2208. if (this._initialAnchor) anchor = this._initialAnchor;
  2209. }
  2210. var $anchor = anchor && $(anchor);
  2211. var $link = anchor && this.$element.find(`[href$="${anchor}"]`);
  2212. // Whether the anchor element that has been found is part of this element
  2213. var isOwnAnchor = !!($anchor.length && $link.length);
  2214. // If there is an anchor for the hash, open it (if not already active)
  2215. if ($anchor && $link && $link.length) {
  2216. if (!$link.parent('[data-accordion-item]').hasClass('is-active')) {
  2217. this._openSingleTab($anchor);
  2218. } }
  2219. // Otherwise, close everything
  2220. else {
  2221. this._closeAllTabs();
  2222. }
  2223. if (isOwnAnchor) {
  2224. // Roll up a little to show the titles
  2225. if (this.options.deepLinkSmudge) {
  2226. onLoad($(window), () => {
  2227. var offset = this.$element.offset();
  2228. $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay);
  2229. });
  2230. }
  2231. /**
  2232. * Fires when the plugin has deeplinked at pageload
  2233. * @event Accordion#deeplink
  2234. */
  2235. this.$element.trigger('deeplink.zf.accordion', [$link, $anchor]);
  2236. }
  2237. };
  2238. //use browser to open a tab, if it exists in this tabset
  2239. if (this.options.deepLink) {
  2240. this._checkDeepLink();
  2241. }
  2242. this._events();
  2243. this._isInitializing = false;
  2244. }
  2245. /**
  2246. * Adds event handlers for items within the accordion.
  2247. * @private
  2248. */
  2249. _events() {
  2250. var _this = this;
  2251. this.$tabs.each(function() {
  2252. var $elem = $(this);
  2253. var $tabContent = $elem.children('[data-tab-content]');
  2254. if ($tabContent.length) {
  2255. $elem.children('a').off('click.zf.accordion keydown.zf.accordion')
  2256. .on('click.zf.accordion', function(e) {
  2257. e.preventDefault();
  2258. _this.toggle($tabContent);
  2259. }).on('keydown.zf.accordion', function(e){
  2260. Keyboard.handleKey(e, 'Accordion', {
  2261. toggle: function() {
  2262. _this.toggle($tabContent);
  2263. },
  2264. next: function() {
  2265. var $a = $elem.next().find('a').focus();
  2266. if (!_this.options.multiExpand) {
  2267. $a.trigger('click.zf.accordion');
  2268. }
  2269. },
  2270. previous: function() {
  2271. var $a = $elem.prev().find('a').focus();
  2272. if (!_this.options.multiExpand) {
  2273. $a.trigger('click.zf.accordion');
  2274. }
  2275. },
  2276. handled: function() {
  2277. e.preventDefault();
  2278. e.stopPropagation();
  2279. }
  2280. });
  2281. });
  2282. }
  2283. });
  2284. if(this.options.deepLink) {
  2285. $(window).on('hashchange', this._checkDeepLink);
  2286. }
  2287. }
  2288. /**
  2289. * Toggles the selected content pane's open/close state.
  2290. * @param {jQuery} $target - jQuery object of the pane to toggle (`.accordion-content`).
  2291. * @function
  2292. */
  2293. toggle($target) {
  2294. if ($target.closest('[data-accordion]').is('[disabled]')) {
  2295. console.info('Cannot toggle an accordion that is disabled.');
  2296. return;
  2297. }
  2298. if($target.parent().hasClass('is-active')) {
  2299. this.up($target);
  2300. } else {
  2301. this.down($target);
  2302. }
  2303. //either replace or update browser history
  2304. if (this.options.deepLink) {
  2305. var anchor = $target.prev('a').attr('href');
  2306. if (this.options.updateHistory) {
  2307. history.pushState({}, '', anchor);
  2308. } else {
  2309. history.replaceState({}, '', anchor);
  2310. }
  2311. }
  2312. }
  2313. /**
  2314. * Opens the accordion tab defined by `$target`.
  2315. * @param {jQuery} $target - Accordion pane to open (`.accordion-content`).
  2316. * @fires Accordion#down
  2317. * @function
  2318. */
  2319. down($target) {
  2320. if ($target.closest('[data-accordion]').is('[disabled]')) {
  2321. console.info('Cannot call down on an accordion that is disabled.');
  2322. return;
  2323. }
  2324. if (this.options.multiExpand)
  2325. this._openTab($target);
  2326. else
  2327. this._openSingleTab($target);
  2328. }
  2329. /**
  2330. * Closes the tab defined by `$target`.
  2331. * It may be ignored if the Accordion options don't allow it.
  2332. *
  2333. * @param {jQuery} $target - Accordion tab to close (`.accordion-content`).
  2334. * @fires Accordion#up
  2335. * @function
  2336. */
  2337. up($target) {
  2338. if (this.$element.is('[disabled]')) {
  2339. console.info('Cannot call up on an accordion that is disabled.');
  2340. return;
  2341. }
  2342. // Don't close the item if it is already closed
  2343. const $targetItem = $target.parent();
  2344. if (!$targetItem.hasClass('is-active')) return;
  2345. // Don't close the item if there is no other active item (unless with `allowAllClosed`)
  2346. const $othersItems = $targetItem.siblings();
  2347. if (!this.options.allowAllClosed && !$othersItems.hasClass('is-active')) return;
  2348. this._closeTab($target);
  2349. }
  2350. /**
  2351. * Make the tab defined by `$target` the only opened tab, closing all others tabs.
  2352. * @param {jQuery} $target - Accordion tab to open (`.accordion-content`).
  2353. * @function
  2354. * @private
  2355. */
  2356. _openSingleTab($target) {
  2357. // Close all the others active tabs.
  2358. const $activeContents = this.$element.children('.is-active').children('[data-tab-content]');
  2359. if ($activeContents.length) {
  2360. this._closeTab($activeContents.not($target));
  2361. }
  2362. // Then open the target.
  2363. this._openTab($target);
  2364. }
  2365. /**
  2366. * Opens the tab defined by `$target`.
  2367. * @param {jQuery} $target - Accordion tab to open (`.accordion-content`).
  2368. * @fires Accordion#down
  2369. * @function
  2370. * @private
  2371. */
  2372. _openTab($target) {
  2373. const $targetItem = $target.parent();
  2374. const targetContentId = $target.attr('aria-labelledby');
  2375. $target.attr('aria-hidden', false);
  2376. $targetItem.addClass('is-active');
  2377. $(`#${targetContentId}`).attr({
  2378. 'aria-expanded': true,
  2379. 'aria-selected': true
  2380. });
  2381. $target.slideDown(this.options.slideSpeed, () => {
  2382. /**
  2383. * Fires when the tab is done opening.
  2384. * @event Accordion#down
  2385. */
  2386. this.$element.trigger('down.zf.accordion', [$target]);
  2387. });
  2388. }
  2389. /**
  2390. * Closes the tab defined by `$target`.
  2391. * @param {jQuery} $target - Accordion tab to close (`.accordion-content`).
  2392. * @fires Accordion#up
  2393. * @function
  2394. * @private
  2395. */
  2396. _closeTab($target) {
  2397. const $targetItem = $target.parent();
  2398. const targetContentId = $target.attr('aria-labelledby');
  2399. $target.attr('aria-hidden', true);
  2400. $targetItem.removeClass('is-active');
  2401. $(`#${targetContentId}`).attr({
  2402. 'aria-expanded': false,
  2403. 'aria-selected': false
  2404. });
  2405. $target.slideUp(this.options.slideSpeed, () => {
  2406. /**
  2407. * Fires when the tab is done collapsing up.
  2408. * @event Accordion#up
  2409. */
  2410. this.$element.trigger('up.zf.accordion', [$target]);
  2411. });
  2412. }
  2413. /**
  2414. * Closes all active tabs
  2415. * @fires Accordion#up
  2416. * @function
  2417. * @private
  2418. */
  2419. _closeAllTabs() {
  2420. var $activeTabs = this.$element.children('.is-active').children('[data-tab-content]');
  2421. if ($activeTabs.length) {
  2422. this._closeTab($activeTabs);
  2423. }
  2424. }
  2425. /**
  2426. * Destroys an instance of an accordion.
  2427. * @fires Accordion#destroyed
  2428. * @function
  2429. */
  2430. _destroy() {
  2431. this.$element.find('[data-tab-content]').stop(true).slideUp(0).css('display', '');
  2432. this.$element.find('a').off('.zf.accordion');
  2433. if(this.options.deepLink) {
  2434. $(window).off('hashchange', this._checkDeepLink);
  2435. }
  2436. }
  2437. }
  2438. Accordion.defaults = {
  2439. /**
  2440. * Amount of time to animate the opening of an accordion pane.
  2441. * @option
  2442. * @type {number}
  2443. * @default 250
  2444. */
  2445. slideSpeed: 250,
  2446. /**
  2447. * Allow the accordion to have multiple open panes.
  2448. * @option
  2449. * @type {boolean}
  2450. * @default false
  2451. */
  2452. multiExpand: false,
  2453. /**
  2454. * Allow the accordion to close all panes.
  2455. * @option
  2456. * @type {boolean}
  2457. * @default false
  2458. */
  2459. allowAllClosed: false,
  2460. /**
  2461. * Link the location hash to the open pane.
  2462. * Set the location hash when the opened pane changes, and open and scroll to the corresponding pane when the location changes.
  2463. * @option
  2464. * @type {boolean}
  2465. * @default false
  2466. */
  2467. deepLink: false,
  2468. /**
  2469. * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the accordion panel is visible
  2470. * @option
  2471. * @type {boolean}
  2472. * @default false
  2473. */
  2474. deepLinkSmudge: false,
  2475. /**
  2476. * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment
  2477. * @option
  2478. * @type {number}
  2479. * @default 300
  2480. */
  2481. deepLinkSmudgeDelay: 300,
  2482. /**
  2483. * If `deepLink` is enabled, update the browser history with the open accordion
  2484. * @option
  2485. * @type {boolean}
  2486. * @default false
  2487. */
  2488. updateHistory: false
  2489. };
  2490. /**
  2491. * AccordionMenu module.
  2492. * @module foundation.accordionMenu
  2493. * @requires foundation.util.keyboard
  2494. * @requires foundation.util.nest
  2495. */
  2496. class AccordionMenu extends Plugin {
  2497. /**
  2498. * Creates a new instance of an accordion menu.
  2499. * @class
  2500. * @name AccordionMenu
  2501. * @fires AccordionMenu#init
  2502. * @param {jQuery} element - jQuery object to make into an accordion menu.
  2503. * @param {Object} options - Overrides to the default plugin settings.
  2504. */
  2505. _setup(element, options) {
  2506. this.$element = element;
  2507. this.options = $.extend({}, AccordionMenu.defaults, this.$element.data(), options);
  2508. this.className = 'AccordionMenu'; // ie9 back compat
  2509. this._init();
  2510. Keyboard.register('AccordionMenu', {
  2511. 'ENTER': 'toggle',
  2512. 'SPACE': 'toggle',
  2513. 'ARROW_RIGHT': 'open',
  2514. 'ARROW_UP': 'up',
  2515. 'ARROW_DOWN': 'down',
  2516. 'ARROW_LEFT': 'close',
  2517. 'ESCAPE': 'closeAll'
  2518. });
  2519. }
  2520. /**
  2521. * Initializes the accordion menu by hiding all nested menus.
  2522. * @private
  2523. */
  2524. _init() {
  2525. Nest.Feather(this.$element, 'accordion');
  2526. var _this = this;
  2527. this.$element.find('[data-submenu]').not('.is-active').slideUp(0);//.find('a').css('padding-left', '1rem');
  2528. this.$element.attr({
  2529. 'role': 'tree',
  2530. 'aria-multiselectable': this.options.multiOpen
  2531. });
  2532. this.$menuLinks = this.$element.find('.is-accordion-submenu-parent');
  2533. this.$menuLinks.each(function(){
  2534. var linkId = this.id || GetYoDigits(6, 'acc-menu-link'),
  2535. $elem = $(this),
  2536. $sub = $elem.children('[data-submenu]'),
  2537. subId = $sub[0].id || GetYoDigits(6, 'acc-menu'),
  2538. isActive = $sub.hasClass('is-active');
  2539. if(_this.options.parentLink) {
  2540. let $anchor = $elem.children('a');
  2541. $anchor.clone().prependTo($sub).wrap('<li data-is-parent-link class="is-submenu-parent-item is-submenu-item is-accordion-submenu-item"></li>');
  2542. }
  2543. if(_this.options.submenuToggle) {
  2544. $elem.addClass('has-submenu-toggle');
  2545. $elem.children('a').after('<button id="' + linkId + '" class="submenu-toggle" aria-controls="' + subId + '" aria-expanded="' + isActive + '" title="' + _this.options.submenuToggleText + '"><span class="submenu-toggle-text">' + _this.options.submenuToggleText + '</span></button>');
  2546. } else {
  2547. $elem.attr({
  2548. 'aria-controls': subId,
  2549. 'aria-expanded': isActive,
  2550. 'id': linkId
  2551. });
  2552. }
  2553. $sub.attr({
  2554. 'aria-labelledby': linkId,
  2555. 'aria-hidden': !isActive,
  2556. 'role': 'group',
  2557. 'id': subId
  2558. });
  2559. });
  2560. this.$element.find('li').attr({
  2561. 'role': 'treeitem'
  2562. });
  2563. var initPanes = this.$element.find('.is-active');
  2564. if(initPanes.length){
  2565. var _this = this;
  2566. initPanes.each(function(){
  2567. _this.down($(this));
  2568. });
  2569. }
  2570. this._events();
  2571. }
  2572. /**
  2573. * Adds event handlers for items within the menu.
  2574. * @private
  2575. */
  2576. _events() {
  2577. var _this = this;
  2578. this.$element.find('li').each(function() {
  2579. var $submenu = $(this).children('[data-submenu]');
  2580. if ($submenu.length) {
  2581. if(_this.options.submenuToggle) {
  2582. $(this).children('.submenu-toggle').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) {
  2583. _this.toggle($submenu);
  2584. });
  2585. } else {
  2586. $(this).children('a').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) {
  2587. e.preventDefault();
  2588. _this.toggle($submenu);
  2589. });
  2590. }
  2591. }
  2592. }).on('keydown.zf.accordionmenu', function(e){
  2593. var $element = $(this),
  2594. $elements = $element.parent('ul').children('li'),
  2595. $prevElement,
  2596. $nextElement,
  2597. $target = $element.children('[data-submenu]');
  2598. $elements.each(function(i) {
  2599. if ($(this).is($element)) {
  2600. $prevElement = $elements.eq(Math.max(0, i-1)).find('a').first();
  2601. $nextElement = $elements.eq(Math.min(i+1, $elements.length-1)).find('a').first();
  2602. if ($(this).children('[data-submenu]:visible').length) { // has open sub menu
  2603. $nextElement = $element.find('li:first-child').find('a').first();
  2604. }
  2605. if ($(this).is(':first-child')) { // is first element of sub menu
  2606. $prevElement = $element.parents('li').first().find('a').first();
  2607. } else if ($prevElement.parents('li').first().children('[data-submenu]:visible').length) { // if previous element has open sub menu
  2608. $prevElement = $prevElement.parents('li').find('li:last-child').find('a').first();
  2609. }
  2610. if ($(this).is(':last-child')) { // is last element of sub menu
  2611. $nextElement = $element.parents('li').first().next('li').find('a').first();
  2612. }
  2613. return;
  2614. }
  2615. });
  2616. Keyboard.handleKey(e, 'AccordionMenu', {
  2617. open: function() {
  2618. if ($target.is(':hidden')) {
  2619. _this.down($target);
  2620. $target.find('li').first().find('a').first().focus();
  2621. }
  2622. },
  2623. close: function() {
  2624. if ($target.length && !$target.is(':hidden')) { // close active sub of this item
  2625. _this.up($target);
  2626. } else if ($element.parent('[data-submenu]').length) { // close currently open sub
  2627. _this.up($element.parent('[data-submenu]'));
  2628. $element.parents('li').first().find('a').first().focus();
  2629. }
  2630. },
  2631. up: function() {
  2632. $prevElement.focus();
  2633. return true;
  2634. },
  2635. down: function() {
  2636. $nextElement.focus();
  2637. return true;
  2638. },
  2639. toggle: function() {
  2640. if (_this.options.submenuToggle) {
  2641. return false;
  2642. }
  2643. if ($element.children('[data-submenu]').length) {
  2644. _this.toggle($element.children('[data-submenu]'));
  2645. return true;
  2646. }
  2647. },
  2648. closeAll: function() {
  2649. _this.hideAll();
  2650. },
  2651. handled: function(preventDefault) {
  2652. if (preventDefault) {
  2653. e.preventDefault();
  2654. }
  2655. e.stopImmediatePropagation();
  2656. }
  2657. });
  2658. });//.attr('tabindex', 0);
  2659. }
  2660. /**
  2661. * Closes all panes of the menu.
  2662. * @function
  2663. */
  2664. hideAll() {
  2665. this.up(this.$element.find('[data-submenu]'));
  2666. }
  2667. /**
  2668. * Opens all panes of the menu.
  2669. * @function
  2670. */
  2671. showAll() {
  2672. this.down(this.$element.find('[data-submenu]'));
  2673. }
  2674. /**
  2675. * Toggles the open/close state of a submenu.
  2676. * @function
  2677. * @param {jQuery} $target - the submenu to toggle
  2678. */
  2679. toggle($target){
  2680. if(!$target.is(':animated')) {
  2681. if (!$target.is(':hidden')) {
  2682. this.up($target);
  2683. }
  2684. else {
  2685. this.down($target);
  2686. }
  2687. }
  2688. }
  2689. /**
  2690. * Opens the sub-menu defined by `$target`.
  2691. * @param {jQuery} $target - Sub-menu to open.
  2692. * @fires AccordionMenu#down
  2693. */
  2694. down($target) {
  2695. // If having multiple submenus active is disabled, close all the submenus
  2696. // that are not parents or children of the targeted submenu.
  2697. if (!this.options.multiOpen) {
  2698. // The "branch" of the targetted submenu, from the component root to
  2699. // the active submenus nested in it.
  2700. const $targetBranch = $target.parentsUntil(this.$element)
  2701. .add($target)
  2702. .add($target.find('.is-active'));
  2703. // All the active submenus that are not in the branch.
  2704. const $othersActiveSubmenus = this.$element.find('.is-active').not($targetBranch);
  2705. this.up($othersActiveSubmenus);
  2706. }
  2707. $target
  2708. .addClass('is-active')
  2709. .attr({ 'aria-hidden': false });
  2710. if(this.options.submenuToggle) {
  2711. $target.prev('.submenu-toggle').attr({'aria-expanded': true});
  2712. }
  2713. else {
  2714. $target.parent('.is-accordion-submenu-parent').attr({'aria-expanded': true});
  2715. }
  2716. $target.slideDown(this.options.slideSpeed, () => {
  2717. /**
  2718. * Fires when the menu is done opening.
  2719. * @event AccordionMenu#down
  2720. */
  2721. this.$element.trigger('down.zf.accordionMenu', [$target]);
  2722. });
  2723. }
  2724. /**
  2725. * Closes the sub-menu defined by `$target`. All sub-menus inside the target will be closed as well.
  2726. * @param {jQuery} $target - Sub-menu to close.
  2727. * @fires AccordionMenu#up
  2728. */
  2729. up($target) {
  2730. const $submenus = $target.find('[data-submenu]');
  2731. const $allmenus = $target.add($submenus);
  2732. $submenus.slideUp(0);
  2733. $allmenus
  2734. .removeClass('is-active')
  2735. .attr('aria-hidden', true);
  2736. if(this.options.submenuToggle) {
  2737. $allmenus.prev('.submenu-toggle').attr('aria-expanded', false);
  2738. }
  2739. else {
  2740. $allmenus.parent('.is-accordion-submenu-parent').attr('aria-expanded', false);
  2741. }
  2742. $target.slideUp(this.options.slideSpeed, () => {
  2743. /**
  2744. * Fires when the menu is done collapsing up.
  2745. * @event AccordionMenu#up
  2746. */
  2747. this.$element.trigger('up.zf.accordionMenu', [$target]);
  2748. });
  2749. }
  2750. /**
  2751. * Destroys an instance of accordion menu.
  2752. * @fires AccordionMenu#destroyed
  2753. */
  2754. _destroy() {
  2755. this.$element.find('[data-submenu]').slideDown(0).css('display', '');
  2756. this.$element.find('a').off('click.zf.accordionMenu');
  2757. this.$element.find('[data-is-parent-link]').detach();
  2758. if(this.options.submenuToggle) {
  2759. this.$element.find('.has-submenu-toggle').removeClass('has-submenu-toggle');
  2760. this.$element.find('.submenu-toggle').remove();
  2761. }
  2762. Nest.Burn(this.$element, 'accordion');
  2763. }
  2764. }
  2765. AccordionMenu.defaults = {
  2766. /**
  2767. * Adds the parent link to the submenu.
  2768. * @option
  2769. * @type {boolean}
  2770. * @default false
  2771. */
  2772. parentLink: false,
  2773. /**
  2774. * Amount of time to animate the opening of a submenu in ms.
  2775. * @option
  2776. * @type {number}
  2777. * @default 250
  2778. */
  2779. slideSpeed: 250,
  2780. /**
  2781. * Adds a separate submenu toggle button. This allows the parent item to have a link.
  2782. * @option
  2783. * @example true
  2784. */
  2785. submenuToggle: false,
  2786. /**
  2787. * The text used for the submenu toggle if enabled. This is used for screen readers only.
  2788. * @option
  2789. * @example true
  2790. */
  2791. submenuToggleText: 'Toggle menu',
  2792. /**
  2793. * Allow the menu to have multiple open panes.
  2794. * @option
  2795. * @type {boolean}
  2796. * @default true
  2797. */
  2798. multiOpen: true
  2799. };
  2800. /**
  2801. * Drilldown module.
  2802. * @module foundation.drilldown
  2803. * @requires foundation.util.keyboard
  2804. * @requires foundation.util.nest
  2805. * @requires foundation.util.box
  2806. */
  2807. class Drilldown extends Plugin {
  2808. /**
  2809. * Creates a new instance of a drilldown menu.
  2810. * @class
  2811. * @name Drilldown
  2812. * @param {jQuery} element - jQuery object to make into an accordion menu.
  2813. * @param {Object} options - Overrides to the default plugin settings.
  2814. */
  2815. _setup(element, options) {
  2816. this.$element = element;
  2817. this.options = $.extend({}, Drilldown.defaults, this.$element.data(), options);
  2818. this.className = 'Drilldown'; // ie9 back compat
  2819. this._init();
  2820. Keyboard.register('Drilldown', {
  2821. 'ENTER': 'open',
  2822. 'SPACE': 'open',
  2823. 'ARROW_RIGHT': 'next',
  2824. 'ARROW_UP': 'up',
  2825. 'ARROW_DOWN': 'down',
  2826. 'ARROW_LEFT': 'previous',
  2827. 'ESCAPE': 'close',
  2828. 'TAB': 'down',
  2829. 'SHIFT_TAB': 'up'
  2830. });
  2831. }
  2832. /**
  2833. * Initializes the drilldown by creating jQuery collections of elements
  2834. * @private
  2835. */
  2836. _init() {
  2837. Nest.Feather(this.$element, 'drilldown');
  2838. if(this.options.autoApplyClass) {
  2839. this.$element.addClass('drilldown');
  2840. }
  2841. this.$element.attr({
  2842. 'role': 'tree',
  2843. 'aria-multiselectable': false
  2844. });
  2845. this.$submenuAnchors = this.$element.find('li.is-drilldown-submenu-parent').children('a');
  2846. this.$submenus = this.$submenuAnchors.parent('li').children('[data-submenu]').attr('role', 'group');
  2847. this.$menuItems = this.$element.find('li').not('.js-drilldown-back').attr('role', 'treeitem').find('a');
  2848. // Set the main menu as current by default (unless a submenu is selected)
  2849. // Used to set the wrapper height when the drilldown is closed/reopened from any (sub)menu
  2850. this.$currentMenu = this.$element;
  2851. this.$element.attr('data-mutate', (this.$element.attr('data-drilldown') || GetYoDigits(6, 'drilldown')));
  2852. this._prepareMenu();
  2853. this._registerEvents();
  2854. this._keyboardEvents();
  2855. }
  2856. /**
  2857. * prepares drilldown menu by setting attributes to links and elements
  2858. * sets a min height to prevent content jumping
  2859. * wraps the element if not already wrapped
  2860. * @private
  2861. * @function
  2862. */
  2863. _prepareMenu() {
  2864. var _this = this;
  2865. // if(!this.options.holdOpen){
  2866. // this._menuLinkEvents();
  2867. // }
  2868. this.$submenuAnchors.each(function(){
  2869. var $link = $(this);
  2870. var $sub = $link.parent();
  2871. if(_this.options.parentLink){
  2872. $link.clone().prependTo($sub.children('[data-submenu]')).wrap('<li data-is-parent-link class="is-submenu-parent-item is-submenu-item is-drilldown-submenu-item" role="menuitem"></li>');
  2873. }
  2874. $link.data('savedHref', $link.attr('href')).removeAttr('href').attr('tabindex', 0);
  2875. $link.children('[data-submenu]')
  2876. .attr({
  2877. 'aria-hidden': true,
  2878. 'tabindex': 0,
  2879. 'role': 'group'
  2880. });
  2881. _this._events($link);
  2882. });
  2883. this.$submenus.each(function(){
  2884. var $menu = $(this),
  2885. $back = $menu.find('.js-drilldown-back');
  2886. if(!$back.length){
  2887. switch (_this.options.backButtonPosition) {
  2888. case "bottom":
  2889. $menu.append(_this.options.backButton);
  2890. break;
  2891. case "top":
  2892. $menu.prepend(_this.options.backButton);
  2893. break;
  2894. default:
  2895. console.error("Unsupported backButtonPosition value '" + _this.options.backButtonPosition + "'");
  2896. }
  2897. }
  2898. _this._back($menu);
  2899. });
  2900. this.$submenus.addClass('invisible');
  2901. if(!this.options.autoHeight) {
  2902. this.$submenus.addClass('drilldown-submenu-cover-previous');
  2903. }
  2904. // create a wrapper on element if it doesn't exist.
  2905. if(!this.$element.parent().hasClass('is-drilldown')){
  2906. this.$wrapper = $(this.options.wrapper).addClass('is-drilldown');
  2907. if(this.options.animateHeight) this.$wrapper.addClass('animate-height');
  2908. this.$element.wrap(this.$wrapper);
  2909. }
  2910. // set wrapper
  2911. this.$wrapper = this.$element.parent();
  2912. this.$wrapper.css(this._getMaxDims());
  2913. }
  2914. _resize() {
  2915. this.$wrapper.css({'max-width': 'none', 'min-height': 'none'});
  2916. // _getMaxDims has side effects (boo) but calling it should update all other necessary heights & widths
  2917. this.$wrapper.css(this._getMaxDims());
  2918. }
  2919. /**
  2920. * Adds event handlers to elements in the menu.
  2921. * @function
  2922. * @private
  2923. * @param {jQuery} $elem - the current menu item to add handlers to.
  2924. */
  2925. _events($elem) {
  2926. var _this = this;
  2927. $elem.off('click.zf.drilldown')
  2928. .on('click.zf.drilldown', function(e){
  2929. if($(e.target).parentsUntil('ul', 'li').hasClass('is-drilldown-submenu-parent')){
  2930. e.stopImmediatePropagation();
  2931. e.preventDefault();
  2932. }
  2933. // if(e.target !== e.currentTarget.firstElementChild){
  2934. // return false;
  2935. // }
  2936. _this._show($elem.parent('li'));
  2937. if(_this.options.closeOnClick){
  2938. var $body = $('body');
  2939. $body.off('.zf.drilldown').on('click.zf.drilldown', function(e){
  2940. if (e.target === _this.$element[0] || $.contains(_this.$element[0], e.target)) { return; }
  2941. e.preventDefault();
  2942. _this._hideAll();
  2943. $body.off('.zf.drilldown');
  2944. });
  2945. }
  2946. });
  2947. }
  2948. /**
  2949. * Adds event handlers to the menu element.
  2950. * @function
  2951. * @private
  2952. */
  2953. _registerEvents() {
  2954. if(this.options.scrollTop){
  2955. this._bindHandler = this._scrollTop.bind(this);
  2956. this.$element.on('open.zf.drilldown hide.zf.drilldown closed.zf.drilldown',this._bindHandler);
  2957. }
  2958. this.$element.on('mutateme.zf.trigger', this._resize.bind(this));
  2959. }
  2960. /**
  2961. * Scroll to Top of Element or data-scroll-top-element
  2962. * @function
  2963. * @fires Drilldown#scrollme
  2964. */
  2965. _scrollTop() {
  2966. var _this = this;
  2967. var $scrollTopElement = _this.options.scrollTopElement!=''?$(_this.options.scrollTopElement):_this.$element,
  2968. scrollPos = parseInt($scrollTopElement.offset().top+_this.options.scrollTopOffset, 10);
  2969. $('html, body').stop(true).animate({ scrollTop: scrollPos }, _this.options.animationDuration, _this.options.animationEasing,function(){
  2970. /**
  2971. * Fires after the menu has scrolled
  2972. * @event Drilldown#scrollme
  2973. */
  2974. if(this===$('html')[0])_this.$element.trigger('scrollme.zf.drilldown');
  2975. });
  2976. }
  2977. /**
  2978. * Adds keydown event listener to `li`'s in the menu.
  2979. * @private
  2980. */
  2981. _keyboardEvents() {
  2982. var _this = this;
  2983. this.$menuItems.add(this.$element.find('.js-drilldown-back > a, .is-submenu-parent-item > a')).on('keydown.zf.drilldown', function(e){
  2984. var $element = $(this),
  2985. $elements = $element.parent('li').parent('ul').children('li').children('a'),
  2986. $prevElement,
  2987. $nextElement;
  2988. $elements.each(function(i) {
  2989. if ($(this).is($element)) {
  2990. $prevElement = $elements.eq(Math.max(0, i-1));
  2991. $nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
  2992. return;
  2993. }
  2994. });
  2995. Keyboard.handleKey(e, 'Drilldown', {
  2996. next: function() {
  2997. if ($element.is(_this.$submenuAnchors)) {
  2998. _this._show($element.parent('li'));
  2999. $element.parent('li').one(transitionend($element), function(){
  3000. $element.parent('li').find('ul li a').not('.js-drilldown-back a').first().focus();
  3001. });
  3002. return true;
  3003. }
  3004. },
  3005. previous: function() {
  3006. _this._hide($element.parent('li').parent('ul'));
  3007. $element.parent('li').parent('ul').one(transitionend($element), function(){
  3008. setTimeout(function() {
  3009. $element.parent('li').parent('ul').parent('li').children('a').first().focus();
  3010. }, 1);
  3011. });
  3012. return true;
  3013. },
  3014. up: function() {
  3015. $prevElement.focus();
  3016. // Don't tap focus on first element in root ul
  3017. return !$element.is(_this.$element.find('> li:first-child > a'));
  3018. },
  3019. down: function() {
  3020. $nextElement.focus();
  3021. // Don't tap focus on last element in root ul
  3022. return !$element.is(_this.$element.find('> li:last-child > a'));
  3023. },
  3024. close: function() {
  3025. // Don't close on element in root ul
  3026. if (!$element.is(_this.$element.find('> li > a'))) {
  3027. _this._hide($element.parent().parent());
  3028. $element.parent().parent().siblings('a').focus();
  3029. }
  3030. },
  3031. open: function() {
  3032. if (_this.options.parentLink && $element.attr('href')) { // Link with href
  3033. return false;
  3034. } else if (!$element.is(_this.$menuItems)) { // not menu item means back button
  3035. _this._hide($element.parent('li').parent('ul'));
  3036. $element.parent('li').parent('ul').one(transitionend($element), function(){
  3037. setTimeout(function() {
  3038. $element.parent('li').parent('ul').parent('li').children('a').first().focus();
  3039. }, 1);
  3040. });
  3041. return true;
  3042. } else if ($element.is(_this.$submenuAnchors)) { // Sub menu item
  3043. _this._show($element.parent('li'));
  3044. $element.parent('li').one(transitionend($element), function(){
  3045. $element.parent('li').find('ul li a').not('.js-drilldown-back a').first().focus();
  3046. });
  3047. return true;
  3048. }
  3049. },
  3050. handled: function(preventDefault) {
  3051. if (preventDefault) {
  3052. e.preventDefault();
  3053. }
  3054. e.stopImmediatePropagation();
  3055. }
  3056. });
  3057. }); // end keyboardAccess
  3058. }
  3059. /**
  3060. * Closes all open elements, and returns to root menu.
  3061. * @function
  3062. * @fires Drilldown#closed
  3063. */
  3064. _hideAll() {
  3065. var $elem = this.$element.find('.is-drilldown-submenu.is-active').addClass('is-closing');
  3066. if(this.options.autoHeight) this.$wrapper.css({height:$elem.parent().closest('ul').data('calcHeight')});
  3067. $elem.one(transitionend($elem), function(e){
  3068. $elem.removeClass('is-active is-closing');
  3069. });
  3070. /**
  3071. * Fires when the menu is fully closed.
  3072. * @event Drilldown#closed
  3073. */
  3074. this.$element.trigger('closed.zf.drilldown');
  3075. }
  3076. /**
  3077. * Adds event listener for each `back` button, and closes open menus.
  3078. * @function
  3079. * @fires Drilldown#back
  3080. * @param {jQuery} $elem - the current sub-menu to add `back` event.
  3081. */
  3082. _back($elem) {
  3083. var _this = this;
  3084. $elem.off('click.zf.drilldown');
  3085. $elem.children('.js-drilldown-back')
  3086. .on('click.zf.drilldown', function(e){
  3087. e.stopImmediatePropagation();
  3088. // console.log('mouseup on back');
  3089. _this._hide($elem);
  3090. // If there is a parent submenu, call show
  3091. let parentSubMenu = $elem.parent('li').parent('ul').parent('li');
  3092. if (parentSubMenu.length) {
  3093. _this._show(parentSubMenu);
  3094. }
  3095. });
  3096. }
  3097. /**
  3098. * Adds event listener to menu items w/o submenus to close open menus on click.
  3099. * @function
  3100. * @private
  3101. */
  3102. _menuLinkEvents() {
  3103. var _this = this;
  3104. this.$menuItems.not('.is-drilldown-submenu-parent')
  3105. .off('click.zf.drilldown')
  3106. .on('click.zf.drilldown', function(e){
  3107. // e.stopImmediatePropagation();
  3108. setTimeout(function(){
  3109. _this._hideAll();
  3110. }, 0);
  3111. });
  3112. }
  3113. /**
  3114. * Sets the CSS classes for submenu to show it.
  3115. * @function
  3116. * @private
  3117. * @param {jQuery} $elem - the target submenu (`ul` tag)
  3118. * @param {boolean} trigger - trigger drilldown event
  3119. */
  3120. _setShowSubMenuClasses($elem, trigger) {
  3121. $elem.addClass('is-active').removeClass('invisible').attr('aria-hidden', false);
  3122. $elem.parent('li').attr('aria-expanded', true);
  3123. if (trigger === true) {
  3124. this.$element.trigger('open.zf.drilldown', [$elem]);
  3125. }
  3126. }
  3127. /**
  3128. * Sets the CSS classes for submenu to hide it.
  3129. * @function
  3130. * @private
  3131. * @param {jQuery} $elem - the target submenu (`ul` tag)
  3132. * @param {boolean} trigger - trigger drilldown event
  3133. */
  3134. _setHideSubMenuClasses($elem, trigger) {
  3135. $elem.removeClass('is-active').addClass('invisible').attr('aria-hidden', true);
  3136. $elem.parent('li').attr('aria-expanded', false);
  3137. if (trigger === true) {
  3138. $elem.trigger('hide.zf.drilldown', [$elem]);
  3139. }
  3140. }
  3141. /**
  3142. * Opens a specific drilldown (sub)menu no matter which (sub)menu in it is currently visible.
  3143. * Compared to _show() this lets you jump into any submenu without clicking through every submenu on the way to it.
  3144. * @function
  3145. * @fires Drilldown#open
  3146. * @param {jQuery} $elem - the target (sub)menu (`ul` tag)
  3147. * @param {boolean} autoFocus - if true the first link in the target (sub)menu gets auto focused
  3148. */
  3149. _showMenu($elem, autoFocus) {
  3150. var _this = this;
  3151. // Reset drilldown
  3152. var $expandedSubmenus = this.$element.find('li[aria-expanded="true"] > ul[data-submenu]');
  3153. $expandedSubmenus.each(function(index) {
  3154. _this._setHideSubMenuClasses($(this));
  3155. });
  3156. // Save the menu as the currently displayed one.
  3157. this.$currentMenu = $elem;
  3158. // If target menu is root, focus first link & exit
  3159. if ($elem.is('[data-drilldown]')) {
  3160. if (autoFocus === true) $elem.find('li[role="treeitem"] > a').first().focus();
  3161. if (this.options.autoHeight) this.$wrapper.css('height', $elem.data('calcHeight'));
  3162. return;
  3163. }
  3164. // Find all submenus on way to root incl. the element itself
  3165. var $submenus = $elem.children().first().parentsUntil('[data-drilldown]', '[data-submenu]');
  3166. // Open target menu and all submenus on its way to root
  3167. $submenus.each(function(index) {
  3168. // Update height of first child (target menu) if autoHeight option true
  3169. if (index === 0 && _this.options.autoHeight) {
  3170. _this.$wrapper.css('height', $(this).data('calcHeight'));
  3171. }
  3172. var isLastChild = index == $submenus.length - 1;
  3173. // Add transitionsend listener to last child (root due to reverse order) to open target menu's first link
  3174. // Last child makes sure the event gets always triggered even if going through several menus
  3175. if (isLastChild === true) {
  3176. $(this).one(transitionend($(this)), () => {
  3177. if (autoFocus === true) {
  3178. $elem.find('li[role="treeitem"] > a').first().focus();
  3179. }
  3180. });
  3181. }
  3182. _this._setShowSubMenuClasses($(this), isLastChild);
  3183. });
  3184. }
  3185. /**
  3186. * Opens a submenu.
  3187. * @function
  3188. * @fires Drilldown#open
  3189. * @param {jQuery} $elem - the current element with a submenu to open, i.e. the `li` tag.
  3190. */
  3191. _show($elem) {
  3192. const $submenu = $elem.children('[data-submenu]');
  3193. $elem.attr('aria-expanded', true);
  3194. this.$currentMenu = $submenu;
  3195. $submenu.addClass('is-active').removeClass('invisible').attr('aria-hidden', false);
  3196. if (this.options.autoHeight) {
  3197. this.$wrapper.css({ height: $submenu.data('calcHeight') });
  3198. }
  3199. /**
  3200. * Fires when the submenu has opened.
  3201. * @event Drilldown#open
  3202. */
  3203. this.$element.trigger('open.zf.drilldown', [$elem]);
  3204. }
  3205. /**
  3206. * Hides a submenu
  3207. * @function
  3208. * @fires Drilldown#hide
  3209. * @param {jQuery} $elem - the current sub-menu to hide, i.e. the `ul` tag.
  3210. */
  3211. _hide($elem) {
  3212. if(this.options.autoHeight) this.$wrapper.css({height:$elem.parent().closest('ul').data('calcHeight')});
  3213. $elem.parent('li').attr('aria-expanded', false);
  3214. $elem.attr('aria-hidden', true);
  3215. $elem.addClass('is-closing')
  3216. .one(transitionend($elem), function(){
  3217. $elem.removeClass('is-active is-closing');
  3218. $elem.blur().addClass('invisible');
  3219. });
  3220. /**
  3221. * Fires when the submenu has closed.
  3222. * @event Drilldown#hide
  3223. */
  3224. $elem.trigger('hide.zf.drilldown', [$elem]);
  3225. }
  3226. /**
  3227. * Iterates through the nested menus to calculate the min-height, and max-width for the menu.
  3228. * Prevents content jumping.
  3229. * @function
  3230. * @private
  3231. */
  3232. _getMaxDims() {
  3233. var maxHeight = 0, result = {}, _this = this;
  3234. // Recalculate menu heights and total max height
  3235. this.$submenus.add(this.$element).each(function(){
  3236. var numOfElems = $(this).children('li').length;
  3237. var height = Box.GetDimensions(this).height;
  3238. maxHeight = height > maxHeight ? height : maxHeight;
  3239. if(_this.options.autoHeight) {
  3240. $(this).data('calcHeight',height);
  3241. }
  3242. });
  3243. if (this.options.autoHeight)
  3244. result['height'] = this.$currentMenu.data('calcHeight');
  3245. else
  3246. result['min-height'] = `${maxHeight}px`;
  3247. result['max-width'] = `${this.$element[0].getBoundingClientRect().width}px`;
  3248. return result;
  3249. }
  3250. /**
  3251. * Destroys the Drilldown Menu
  3252. * @function
  3253. */
  3254. _destroy() {
  3255. if(this.options.scrollTop) this.$element.off('.zf.drilldown',this._bindHandler);
  3256. this._hideAll();
  3257. this.$element.off('mutateme.zf.trigger');
  3258. Nest.Burn(this.$element, 'drilldown');
  3259. this.$element.unwrap()
  3260. .find('.js-drilldown-back, .is-submenu-parent-item').remove()
  3261. .end().find('.is-active, .is-closing, .is-drilldown-submenu').removeClass('is-active is-closing is-drilldown-submenu')
  3262. .end().find('[data-submenu]').removeAttr('aria-hidden tabindex role');
  3263. this.$submenuAnchors.each(function() {
  3264. $(this).off('.zf.drilldown');
  3265. });
  3266. this.$element.find('[data-is-parent-link]').detach();
  3267. this.$submenus.removeClass('drilldown-submenu-cover-previous invisible');
  3268. this.$element.find('a').each(function(){
  3269. var $link = $(this);
  3270. $link.removeAttr('tabindex');
  3271. if($link.data('savedHref')){
  3272. $link.attr('href', $link.data('savedHref')).removeData('savedHref');
  3273. }else{ return; }
  3274. });
  3275. };
  3276. }
  3277. Drilldown.defaults = {
  3278. /**
  3279. * Drilldowns depend on styles in order to function properly; in the default build of Foundation these are
  3280. * on the `drilldown` class. This option auto-applies this class to the drilldown upon initialization.
  3281. * @option
  3282. * @type {boolian}
  3283. * @default true
  3284. */
  3285. autoApplyClass: true,
  3286. /**
  3287. * Markup used for JS generated back button. Prepended or appended (see backButtonPosition) to submenu lists and deleted on `destroy` method, 'js-drilldown-back' class required. Remove the backslash (`\`) if copy and pasting.
  3288. * @option
  3289. * @type {string}
  3290. * @default '<li class="js-drilldown-back"><a tabindex="0">Back</a></li>'
  3291. */
  3292. backButton: '<li class="js-drilldown-back"><a tabindex="0">Back</a></li>',
  3293. /**
  3294. * Position the back button either at the top or bottom of drilldown submenus. Can be `'left'` or `'bottom'`.
  3295. * @option
  3296. * @type {string}
  3297. * @default top
  3298. */
  3299. backButtonPosition: 'top',
  3300. /**
  3301. * Markup used to wrap drilldown menu. Use a class name for independent styling; the JS applied class: `is-drilldown` is required. Remove the backslash (`\`) if copy and pasting.
  3302. * @option
  3303. * @type {string}
  3304. * @default '<div></div>'
  3305. */
  3306. wrapper: '<div></div>',
  3307. /**
  3308. * Adds the parent link to the submenu.
  3309. * @option
  3310. * @type {boolean}
  3311. * @default false
  3312. */
  3313. parentLink: false,
  3314. /**
  3315. * Allow the menu to return to root list on body click.
  3316. * @option
  3317. * @type {boolean}
  3318. * @default false
  3319. */
  3320. closeOnClick: false,
  3321. /**
  3322. * Allow the menu to auto adjust height.
  3323. * @option
  3324. * @type {boolean}
  3325. * @default false
  3326. */
  3327. autoHeight: false,
  3328. /**
  3329. * Animate the auto adjust height.
  3330. * @option
  3331. * @type {boolean}
  3332. * @default false
  3333. */
  3334. animateHeight: false,
  3335. /**
  3336. * Scroll to the top of the menu after opening a submenu or navigating back using the menu back button
  3337. * @option
  3338. * @type {boolean}
  3339. * @default false
  3340. */
  3341. scrollTop: false,
  3342. /**
  3343. * String jquery selector (for example 'body') of element to take offset().top from, if empty string the drilldown menu offset().top is taken
  3344. * @option
  3345. * @type {string}
  3346. * @default ''
  3347. */
  3348. scrollTopElement: '',
  3349. /**
  3350. * ScrollTop offset
  3351. * @option
  3352. * @type {number}
  3353. * @default 0
  3354. */
  3355. scrollTopOffset: 0,
  3356. /**
  3357. * Scroll animation duration
  3358. * @option
  3359. * @type {number}
  3360. * @default 500
  3361. */
  3362. animationDuration: 500,
  3363. /**
  3364. * Scroll animation easing. Can be `'swing'` or `'linear'`.
  3365. * @option
  3366. * @type {string}
  3367. * @see {@link https://api.jquery.com/animate|JQuery animate}
  3368. * @default 'swing'
  3369. */
  3370. animationEasing: 'swing'
  3371. // holdOpen: false
  3372. };
  3373. const POSITIONS = ['left', 'right', 'top', 'bottom'];
  3374. const VERTICAL_ALIGNMENTS = ['top', 'bottom', 'center'];
  3375. const HORIZONTAL_ALIGNMENTS = ['left', 'right', 'center'];
  3376. const ALIGNMENTS = {
  3377. 'left': VERTICAL_ALIGNMENTS,
  3378. 'right': VERTICAL_ALIGNMENTS,
  3379. 'top': HORIZONTAL_ALIGNMENTS,
  3380. 'bottom': HORIZONTAL_ALIGNMENTS
  3381. };
  3382. function nextItem(item, array) {
  3383. var currentIdx = array.indexOf(item);
  3384. if(currentIdx === array.length - 1) {
  3385. return array[0];
  3386. } else {
  3387. return array[currentIdx + 1];
  3388. }
  3389. }
  3390. class Positionable extends Plugin {
  3391. /**
  3392. * Abstract class encapsulating the tether-like explicit positioning logic
  3393. * including repositioning based on overlap.
  3394. * Expects classes to define defaults for vOffset, hOffset, position,
  3395. * alignment, allowOverlap, and allowBottomOverlap. They can do this by
  3396. * extending the defaults, or (for now recommended due to the way docs are
  3397. * generated) by explicitly declaring them.
  3398. *
  3399. **/
  3400. _init() {
  3401. this.triedPositions = {};
  3402. this.position = this.options.position === 'auto' ? this._getDefaultPosition() : this.options.position;
  3403. this.alignment = this.options.alignment === 'auto' ? this._getDefaultAlignment() : this.options.alignment;
  3404. this.originalPosition = this.position;
  3405. this.originalAlignment = this.alignment;
  3406. }
  3407. _getDefaultPosition () {
  3408. return 'bottom';
  3409. }
  3410. _getDefaultAlignment() {
  3411. switch(this.position) {
  3412. case 'bottom':
  3413. case 'top':
  3414. return rtl() ? 'right' : 'left';
  3415. case 'left':
  3416. case 'right':
  3417. return 'bottom';
  3418. }
  3419. }
  3420. /**
  3421. * Adjusts the positionable possible positions by iterating through alignments
  3422. * and positions.
  3423. * @function
  3424. * @private
  3425. */
  3426. _reposition() {
  3427. if(this._alignmentsExhausted(this.position)) {
  3428. this.position = nextItem(this.position, POSITIONS);
  3429. this.alignment = ALIGNMENTS[this.position][0];
  3430. } else {
  3431. this._realign();
  3432. }
  3433. }
  3434. /**
  3435. * Adjusts the dropdown pane possible positions by iterating through alignments
  3436. * on the current position.
  3437. * @function
  3438. * @private
  3439. */
  3440. _realign() {
  3441. this._addTriedPosition(this.position, this.alignment);
  3442. this.alignment = nextItem(this.alignment, ALIGNMENTS[this.position]);
  3443. }
  3444. _addTriedPosition(position, alignment) {
  3445. this.triedPositions[position] = this.triedPositions[position] || [];
  3446. this.triedPositions[position].push(alignment);
  3447. }
  3448. _positionsExhausted() {
  3449. var isExhausted = true;
  3450. for(var i = 0; i < POSITIONS.length; i++) {
  3451. isExhausted = isExhausted && this._alignmentsExhausted(POSITIONS[i]);
  3452. }
  3453. return isExhausted;
  3454. }
  3455. _alignmentsExhausted(position) {
  3456. return this.triedPositions[position] && this.triedPositions[position].length == ALIGNMENTS[position].length;
  3457. }
  3458. // When we're trying to center, we don't want to apply offset that's going to
  3459. // take us just off center, so wrap around to return 0 for the appropriate
  3460. // offset in those alignments. TODO: Figure out if we want to make this
  3461. // configurable behavior... it feels more intuitive, especially for tooltips, but
  3462. // it's possible someone might actually want to start from center and then nudge
  3463. // slightly off.
  3464. _getVOffset() {
  3465. return this.options.vOffset;
  3466. }
  3467. _getHOffset() {
  3468. return this.options.hOffset;
  3469. }
  3470. _setPosition($anchor, $element, $parent) {
  3471. if($anchor.attr('aria-expanded') === 'false'){ return false; }
  3472. var $eleDims = Box.GetDimensions($element),
  3473. $anchorDims = Box.GetDimensions($anchor);
  3474. if (!this.options.allowOverlap) {
  3475. // restore original position & alignment before checking overlap
  3476. this.position = this.originalPosition;
  3477. this.alignment = this.originalAlignment;
  3478. }
  3479. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  3480. if(!this.options.allowOverlap) {
  3481. var minOverlap = 100000000;
  3482. // default coordinates to how we start, in case we can't figure out better
  3483. var minCoordinates = {position: this.position, alignment: this.alignment};
  3484. while(!this._positionsExhausted()) {
  3485. let overlap = Box.OverlapArea($element, $parent, false, false, this.options.allowBottomOverlap);
  3486. if(overlap === 0) {
  3487. return;
  3488. }
  3489. if(overlap < minOverlap) {
  3490. minOverlap = overlap;
  3491. minCoordinates = {position: this.position, alignment: this.alignment};
  3492. }
  3493. this._reposition();
  3494. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  3495. }
  3496. // If we get through the entire loop, there was no non-overlapping
  3497. // position available. Pick the version with least overlap.
  3498. this.position = minCoordinates.position;
  3499. this.alignment = minCoordinates.alignment;
  3500. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  3501. }
  3502. }
  3503. }
  3504. Positionable.defaults = {
  3505. /**
  3506. * Position of positionable relative to anchor. Can be left, right, bottom, top, or auto.
  3507. * @option
  3508. * @type {string}
  3509. * @default 'auto'
  3510. */
  3511. position: 'auto',
  3512. /**
  3513. * Alignment of positionable relative to anchor. Can be left, right, bottom, top, center, or auto.
  3514. * @option
  3515. * @type {string}
  3516. * @default 'auto'
  3517. */
  3518. alignment: 'auto',
  3519. /**
  3520. * Allow overlap of container/window. If false, dropdown positionable first
  3521. * try to position as defined by data-position and data-alignment, but
  3522. * reposition if it would cause an overflow.
  3523. * @option
  3524. * @type {boolean}
  3525. * @default false
  3526. */
  3527. allowOverlap: false,
  3528. /**
  3529. * Allow overlap of only the bottom of the container. This is the most common
  3530. * behavior for dropdowns, allowing the dropdown to extend the bottom of the
  3531. * screen but not otherwise influence or break out of the container.
  3532. * @option
  3533. * @type {boolean}
  3534. * @default true
  3535. */
  3536. allowBottomOverlap: true,
  3537. /**
  3538. * Number of pixels the positionable should be separated vertically from anchor
  3539. * @option
  3540. * @type {number}
  3541. * @default 0
  3542. */
  3543. vOffset: 0,
  3544. /**
  3545. * Number of pixels the positionable should be separated horizontally from anchor
  3546. * @option
  3547. * @type {number}
  3548. * @default 0
  3549. */
  3550. hOffset: 0,
  3551. };
  3552. /**
  3553. * Dropdown module.
  3554. * @module foundation.dropdown
  3555. * @requires foundation.util.keyboard
  3556. * @requires foundation.util.box
  3557. * @requires foundation.util.triggers
  3558. */
  3559. class Dropdown extends Positionable {
  3560. /**
  3561. * Creates a new instance of a dropdown.
  3562. * @class
  3563. * @name Dropdown
  3564. * @param {jQuery} element - jQuery object to make into a dropdown.
  3565. * Object should be of the dropdown panel, rather than its anchor.
  3566. * @param {Object} options - Overrides to the default plugin settings.
  3567. */
  3568. _setup(element, options) {
  3569. this.$element = element;
  3570. this.options = $.extend({}, Dropdown.defaults, this.$element.data(), options);
  3571. this.className = 'Dropdown'; // ie9 back compat
  3572. // Triggers init is idempotent, just need to make sure it is initialized
  3573. Triggers.init($);
  3574. this._init();
  3575. Keyboard.register('Dropdown', {
  3576. 'ENTER': 'toggle',
  3577. 'SPACE': 'toggle',
  3578. 'ESCAPE': 'close'
  3579. });
  3580. }
  3581. /**
  3582. * Initializes the plugin by setting/checking options and attributes, adding helper variables, and saving the anchor.
  3583. * @function
  3584. * @private
  3585. */
  3586. _init() {
  3587. var $id = this.$element.attr('id');
  3588. this.$anchors = $(`[data-toggle="${$id}"]`).length ? $(`[data-toggle="${$id}"]`) : $(`[data-open="${$id}"]`);
  3589. this.$anchors.attr({
  3590. 'aria-controls': $id,
  3591. 'data-is-focus': false,
  3592. 'data-yeti-box': $id,
  3593. 'aria-haspopup': true,
  3594. 'aria-expanded': false
  3595. });
  3596. this._setCurrentAnchor(this.$anchors.first());
  3597. if(this.options.parentClass){
  3598. this.$parent = this.$element.parents('.' + this.options.parentClass);
  3599. }else{
  3600. this.$parent = null;
  3601. }
  3602. // Set [aria-labelledby] on the Dropdown if it is not set
  3603. if (typeof this.$element.attr('aria-labelledby') === 'undefined') {
  3604. // Get the anchor ID or create one
  3605. if (typeof this.$currentAnchor.attr('id') === 'undefined') {
  3606. this.$currentAnchor.attr('id', GetYoDigits(6, 'dd-anchor'));
  3607. }
  3608. this.$element.attr('aria-labelledby', this.$currentAnchor.attr('id'));
  3609. }
  3610. this.$element.attr({
  3611. 'aria-hidden': 'true',
  3612. 'data-yeti-box': $id,
  3613. 'data-resize': $id,
  3614. });
  3615. super._init();
  3616. this._events();
  3617. }
  3618. _getDefaultPosition() {
  3619. // handle legacy classnames
  3620. var position = this.$element[0].className.match(/(top|left|right|bottom)/g);
  3621. if(position) {
  3622. return position[0];
  3623. } else {
  3624. return 'bottom'
  3625. }
  3626. }
  3627. _getDefaultAlignment() {
  3628. // handle legacy float approach
  3629. var horizontalPosition = /float-(\S+)/.exec(this.$currentAnchor.attr('class'));
  3630. if(horizontalPosition) {
  3631. return horizontalPosition[1];
  3632. }
  3633. return super._getDefaultAlignment();
  3634. }
  3635. /**
  3636. * Sets the position and orientation of the dropdown pane, checks for collisions if allow-overlap is not true.
  3637. * Recursively calls itself if a collision is detected, with a new position class.
  3638. * @function
  3639. * @private
  3640. */
  3641. _setPosition() {
  3642. this.$element.removeClass(`has-position-${this.position} has-alignment-${this.alignment}`);
  3643. super._setPosition(this.$currentAnchor, this.$element, this.$parent);
  3644. this.$element.addClass(`has-position-${this.position} has-alignment-${this.alignment}`);
  3645. }
  3646. /**
  3647. * Make it a current anchor.
  3648. * Current anchor as the reference for the position of Dropdown panes.
  3649. * @param {HTML} el - DOM element of the anchor.
  3650. * @function
  3651. * @private
  3652. */
  3653. _setCurrentAnchor(el) {
  3654. this.$currentAnchor = $(el);
  3655. }
  3656. /**
  3657. * Adds event listeners to the element utilizing the triggers utility library.
  3658. * @function
  3659. * @private
  3660. */
  3661. _events() {
  3662. var _this = this;
  3663. this.$element.on({
  3664. 'open.zf.trigger': this.open.bind(this),
  3665. 'close.zf.trigger': this.close.bind(this),
  3666. 'toggle.zf.trigger': this.toggle.bind(this),
  3667. 'resizeme.zf.trigger': this._setPosition.bind(this)
  3668. });
  3669. this.$anchors.off('click.zf.trigger')
  3670. .on('click.zf.trigger', function() { _this._setCurrentAnchor(this); });
  3671. if(this.options.hover){
  3672. this.$anchors.off('mouseenter.zf.dropdown mouseleave.zf.dropdown')
  3673. .on('mouseenter.zf.dropdown', function(){
  3674. _this._setCurrentAnchor(this);
  3675. var bodyData = $('body').data();
  3676. if(typeof(bodyData.whatinput) === 'undefined' || bodyData.whatinput === 'mouse') {
  3677. clearTimeout(_this.timeout);
  3678. _this.timeout = setTimeout(function(){
  3679. _this.open();
  3680. _this.$anchors.data('hover', true);
  3681. }, _this.options.hoverDelay);
  3682. }
  3683. }).on('mouseleave.zf.dropdown', ignoreMousedisappear(function(){
  3684. clearTimeout(_this.timeout);
  3685. _this.timeout = setTimeout(function(){
  3686. _this.close();
  3687. _this.$anchors.data('hover', false);
  3688. }, _this.options.hoverDelay);
  3689. }));
  3690. if(this.options.hoverPane){
  3691. this.$element.off('mouseenter.zf.dropdown mouseleave.zf.dropdown')
  3692. .on('mouseenter.zf.dropdown', function(){
  3693. clearTimeout(_this.timeout);
  3694. }).on('mouseleave.zf.dropdown', ignoreMousedisappear(function(){
  3695. clearTimeout(_this.timeout);
  3696. _this.timeout = setTimeout(function(){
  3697. _this.close();
  3698. _this.$anchors.data('hover', false);
  3699. }, _this.options.hoverDelay);
  3700. }));
  3701. }
  3702. }
  3703. this.$anchors.add(this.$element).on('keydown.zf.dropdown', function(e) {
  3704. var $target = $(this),
  3705. visibleFocusableElements = Keyboard.findFocusable(_this.$element);
  3706. Keyboard.handleKey(e, 'Dropdown', {
  3707. open: function() {
  3708. if ($target.is(_this.$anchors) && !$target.is('input, textarea')) {
  3709. _this.open();
  3710. _this.$element.attr('tabindex', -1).focus();
  3711. e.preventDefault();
  3712. }
  3713. },
  3714. close: function() {
  3715. _this.close();
  3716. _this.$anchors.focus();
  3717. }
  3718. });
  3719. });
  3720. }
  3721. /**
  3722. * Adds an event handler to the body to close any dropdowns on a click.
  3723. * @function
  3724. * @private
  3725. */
  3726. _addBodyHandler() {
  3727. var $body = $(document.body).not(this.$element),
  3728. _this = this;
  3729. $body.off('click.zf.dropdown')
  3730. .on('click.zf.dropdown', function(e){
  3731. if(_this.$anchors.is(e.target) || _this.$anchors.find(e.target).length) {
  3732. return;
  3733. }
  3734. if(_this.$element.is(e.target) || _this.$element.find(e.target).length) {
  3735. return;
  3736. }
  3737. _this.close();
  3738. $body.off('click.zf.dropdown');
  3739. });
  3740. }
  3741. /**
  3742. * Opens the dropdown pane, and fires a bubbling event to close other dropdowns.
  3743. * @function
  3744. * @fires Dropdown#closeme
  3745. * @fires Dropdown#show
  3746. */
  3747. open() {
  3748. // var _this = this;
  3749. /**
  3750. * Fires to close other open dropdowns, typically when dropdown is opening
  3751. * @event Dropdown#closeme
  3752. */
  3753. this.$element.trigger('closeme.zf.dropdown', this.$element.attr('id'));
  3754. this.$anchors.addClass('hover')
  3755. .attr({'aria-expanded': true});
  3756. // this.$element/*.show()*/;
  3757. this.$element.addClass('is-opening');
  3758. this._setPosition();
  3759. this.$element.removeClass('is-opening').addClass('is-open')
  3760. .attr({'aria-hidden': false});
  3761. if(this.options.autoFocus){
  3762. var $focusable = Keyboard.findFocusable(this.$element);
  3763. if($focusable.length){
  3764. $focusable.eq(0).focus();
  3765. }
  3766. }
  3767. if(this.options.closeOnClick){ this._addBodyHandler(); }
  3768. if (this.options.trapFocus) {
  3769. Keyboard.trapFocus(this.$element);
  3770. }
  3771. /**
  3772. * Fires once the dropdown is visible.
  3773. * @event Dropdown#show
  3774. */
  3775. this.$element.trigger('show.zf.dropdown', [this.$element]);
  3776. }
  3777. /**
  3778. * Closes the open dropdown pane.
  3779. * @function
  3780. * @fires Dropdown#hide
  3781. */
  3782. close() {
  3783. if(!this.$element.hasClass('is-open')){
  3784. return false;
  3785. }
  3786. this.$element.removeClass('is-open')
  3787. .attr({'aria-hidden': true});
  3788. this.$anchors.removeClass('hover')
  3789. .attr('aria-expanded', false);
  3790. /**
  3791. * Fires once the dropdown is no longer visible.
  3792. * @event Dropdown#hide
  3793. */
  3794. this.$element.trigger('hide.zf.dropdown', [this.$element]);
  3795. if (this.options.trapFocus) {
  3796. Keyboard.releaseFocus(this.$element);
  3797. }
  3798. }
  3799. /**
  3800. * Toggles the dropdown pane's visibility.
  3801. * @function
  3802. */
  3803. toggle() {
  3804. if(this.$element.hasClass('is-open')){
  3805. if(this.$anchors.data('hover')) return;
  3806. this.close();
  3807. }else{
  3808. this.open();
  3809. }
  3810. }
  3811. /**
  3812. * Destroys the dropdown.
  3813. * @function
  3814. */
  3815. _destroy() {
  3816. this.$element.off('.zf.trigger').hide();
  3817. this.$anchors.off('.zf.dropdown');
  3818. $(document.body).off('click.zf.dropdown');
  3819. }
  3820. }
  3821. Dropdown.defaults = {
  3822. /**
  3823. * Class that designates bounding container of Dropdown (default: window)
  3824. * @option
  3825. * @type {?string}
  3826. * @default null
  3827. */
  3828. parentClass: null,
  3829. /**
  3830. * Amount of time to delay opening a submenu on hover event.
  3831. * @option
  3832. * @type {number}
  3833. * @default 250
  3834. */
  3835. hoverDelay: 250,
  3836. /**
  3837. * Allow submenus to open on hover events
  3838. * @option
  3839. * @type {boolean}
  3840. * @default false
  3841. */
  3842. hover: false,
  3843. /**
  3844. * Don't close dropdown when hovering over dropdown pane
  3845. * @option
  3846. * @type {boolean}
  3847. * @default false
  3848. */
  3849. hoverPane: false,
  3850. /**
  3851. * Number of pixels between the dropdown pane and the triggering element on open.
  3852. * @option
  3853. * @type {number}
  3854. * @default 0
  3855. */
  3856. vOffset: 0,
  3857. /**
  3858. * Number of pixels between the dropdown pane and the triggering element on open.
  3859. * @option
  3860. * @type {number}
  3861. * @default 0
  3862. */
  3863. hOffset: 0,
  3864. /**
  3865. * Position of dropdown. Can be left, right, bottom, top, or auto.
  3866. * @option
  3867. * @type {string}
  3868. * @default 'auto'
  3869. */
  3870. position: 'auto',
  3871. /**
  3872. * Alignment of dropdown relative to anchor. Can be left, right, bottom, top, center, or auto.
  3873. * @option
  3874. * @type {string}
  3875. * @default 'auto'
  3876. */
  3877. alignment: 'auto',
  3878. /**
  3879. * Allow overlap of container/window. If false, dropdown will first try to position as defined by data-position and data-alignment, but reposition if it would cause an overflow.
  3880. * @option
  3881. * @type {boolean}
  3882. * @default false
  3883. */
  3884. allowOverlap: false,
  3885. /**
  3886. * Allow overlap of only the bottom of the container. This is the most common
  3887. * behavior for dropdowns, allowing the dropdown to extend the bottom of the
  3888. * screen but not otherwise influence or break out of the container.
  3889. * @option
  3890. * @type {boolean}
  3891. * @default true
  3892. */
  3893. allowBottomOverlap: true,
  3894. /**
  3895. * Allow the plugin to trap focus to the dropdown pane if opened with keyboard commands.
  3896. * @option
  3897. * @type {boolean}
  3898. * @default false
  3899. */
  3900. trapFocus: false,
  3901. /**
  3902. * Allow the plugin to set focus to the first focusable element within the pane, regardless of method of opening.
  3903. * @option
  3904. * @type {boolean}
  3905. * @default false
  3906. */
  3907. autoFocus: false,
  3908. /**
  3909. * Allows a click on the body to close the dropdown.
  3910. * @option
  3911. * @type {boolean}
  3912. * @default false
  3913. */
  3914. closeOnClick: false
  3915. };
  3916. /**
  3917. * DropdownMenu module.
  3918. * @module foundation.dropdown-menu
  3919. * @requires foundation.util.keyboard
  3920. * @requires foundation.util.box
  3921. * @requires foundation.util.nest
  3922. */
  3923. class DropdownMenu extends Plugin {
  3924. /**
  3925. * Creates a new instance of DropdownMenu.
  3926. * @class
  3927. * @name DropdownMenu
  3928. * @fires DropdownMenu#init
  3929. * @param {jQuery} element - jQuery object to make into a dropdown menu.
  3930. * @param {Object} options - Overrides to the default plugin settings.
  3931. */
  3932. _setup(element, options) {
  3933. this.$element = element;
  3934. this.options = $.extend({}, DropdownMenu.defaults, this.$element.data(), options);
  3935. this.className = 'DropdownMenu'; // ie9 back compat
  3936. this._init();
  3937. Keyboard.register('DropdownMenu', {
  3938. 'ENTER': 'open',
  3939. 'SPACE': 'open',
  3940. 'ARROW_RIGHT': 'next',
  3941. 'ARROW_UP': 'up',
  3942. 'ARROW_DOWN': 'down',
  3943. 'ARROW_LEFT': 'previous',
  3944. 'ESCAPE': 'close'
  3945. });
  3946. }
  3947. /**
  3948. * Initializes the plugin, and calls _prepareMenu
  3949. * @private
  3950. * @function
  3951. */
  3952. _init() {
  3953. Nest.Feather(this.$element, 'dropdown');
  3954. var subs = this.$element.find('li.is-dropdown-submenu-parent');
  3955. this.$element.children('.is-dropdown-submenu-parent').children('.is-dropdown-submenu').addClass('first-sub');
  3956. this.$menuItems = this.$element.find('[role="menuitem"]');
  3957. this.$tabs = this.$element.children('[role="menuitem"]');
  3958. this.$tabs.find('ul.is-dropdown-submenu').addClass(this.options.verticalClass);
  3959. if (this.options.alignment === 'auto') {
  3960. if (this.$element.hasClass(this.options.rightClass) || rtl() || this.$element.parents('.top-bar-right').is('*')) {
  3961. this.options.alignment = 'right';
  3962. subs.addClass('opens-left');
  3963. } else {
  3964. this.options.alignment = 'left';
  3965. subs.addClass('opens-right');
  3966. }
  3967. } else {
  3968. if (this.options.alignment === 'right') {
  3969. subs.addClass('opens-left');
  3970. } else {
  3971. subs.addClass('opens-right');
  3972. }
  3973. }
  3974. this.changed = false;
  3975. this._events();
  3976. };
  3977. _isVertical() {
  3978. return this.$tabs.css('display') === 'block' || this.$element.css('flex-direction') === 'column';
  3979. }
  3980. _isRtl() {
  3981. return this.$element.hasClass('align-right') || (rtl() && !this.$element.hasClass('align-left'));
  3982. }
  3983. /**
  3984. * Adds event listeners to elements within the menu
  3985. * @private
  3986. * @function
  3987. */
  3988. _events() {
  3989. var _this = this,
  3990. hasTouch = 'ontouchstart' in window || (typeof window.ontouchstart !== 'undefined'),
  3991. parClass = 'is-dropdown-submenu-parent';
  3992. // used for onClick and in the keyboard handlers
  3993. var handleClickFn = function(e) {
  3994. var $elem = $(e.target).parentsUntil('ul', `.${parClass}`),
  3995. hasSub = $elem.hasClass(parClass),
  3996. hasClicked = $elem.attr('data-is-click') === 'true',
  3997. $sub = $elem.children('.is-dropdown-submenu');
  3998. if (hasSub) {
  3999. if (hasClicked) {
  4000. if (!_this.options.closeOnClick || (!_this.options.clickOpen && !hasTouch) || (_this.options.forceFollow && hasTouch)) { return; }
  4001. else {
  4002. e.stopImmediatePropagation();
  4003. e.preventDefault();
  4004. _this._hide($elem);
  4005. }
  4006. } else {
  4007. e.preventDefault();
  4008. e.stopImmediatePropagation();
  4009. _this._show($sub);
  4010. $elem.add($elem.parentsUntil(_this.$element, `.${parClass}`)).attr('data-is-click', true);
  4011. }
  4012. }
  4013. };
  4014. if (this.options.clickOpen || hasTouch) {
  4015. this.$menuItems.on('click.zf.dropdownmenu touchstart.zf.dropdownmenu', handleClickFn);
  4016. }
  4017. // Handle Leaf element Clicks
  4018. if(_this.options.closeOnClickInside){
  4019. this.$menuItems.on('click.zf.dropdownmenu', function(e) {
  4020. var $elem = $(this),
  4021. hasSub = $elem.hasClass(parClass);
  4022. if(!hasSub){
  4023. _this._hide();
  4024. }
  4025. });
  4026. }
  4027. if (!this.options.disableHover) {
  4028. this.$menuItems.on('mouseenter.zf.dropdownmenu', function (e) {
  4029. var $elem = $(this),
  4030. hasSub = $elem.hasClass(parClass);
  4031. if (hasSub) {
  4032. clearTimeout($elem.data('_delay'));
  4033. $elem.data('_delay', setTimeout(function () {
  4034. _this._show($elem.children('.is-dropdown-submenu'));
  4035. }, _this.options.hoverDelay));
  4036. }
  4037. }).on('mouseleave.zf.dropdownMenu', ignoreMousedisappear(function (e) {
  4038. var $elem = $(this),
  4039. hasSub = $elem.hasClass(parClass);
  4040. if (hasSub && _this.options.autoclose) {
  4041. if ($elem.attr('data-is-click') === 'true' && _this.options.clickOpen) { return false; }
  4042. clearTimeout($elem.data('_delay'));
  4043. $elem.data('_delay', setTimeout(function () {
  4044. _this._hide($elem);
  4045. }, _this.options.closingTime));
  4046. }
  4047. }));
  4048. }
  4049. this.$menuItems.on('keydown.zf.dropdownmenu', function(e) {
  4050. var $element = $(e.target).parentsUntil('ul', '[role="menuitem"]'),
  4051. isTab = _this.$tabs.index($element) > -1,
  4052. $elements = isTab ? _this.$tabs : $element.siblings('li').add($element),
  4053. $prevElement,
  4054. $nextElement;
  4055. $elements.each(function(i) {
  4056. if ($(this).is($element)) {
  4057. $prevElement = $elements.eq(i-1);
  4058. $nextElement = $elements.eq(i+1);
  4059. return;
  4060. }
  4061. });
  4062. var nextSibling = function() {
  4063. $nextElement.children('a:first').focus();
  4064. e.preventDefault();
  4065. }, prevSibling = function() {
  4066. $prevElement.children('a:first').focus();
  4067. e.preventDefault();
  4068. }, openSub = function() {
  4069. var $sub = $element.children('ul.is-dropdown-submenu');
  4070. if ($sub.length) {
  4071. _this._show($sub);
  4072. $element.find('li > a:first').focus();
  4073. e.preventDefault();
  4074. } else { return; }
  4075. }, closeSub = function() {
  4076. //if ($element.is(':first-child')) {
  4077. var close = $element.parent('ul').parent('li');
  4078. close.children('a:first').focus();
  4079. _this._hide(close);
  4080. e.preventDefault();
  4081. //}
  4082. };
  4083. var functions = {
  4084. open: openSub,
  4085. close: function() {
  4086. _this._hide(_this.$element);
  4087. _this.$menuItems.eq(0).children('a').focus(); // focus to first element
  4088. e.preventDefault();
  4089. },
  4090. handled: function() {
  4091. e.stopImmediatePropagation();
  4092. }
  4093. };
  4094. if (isTab) {
  4095. if (_this._isVertical()) { // vertical menu
  4096. if (_this._isRtl()) { // right aligned
  4097. $.extend(functions, {
  4098. down: nextSibling,
  4099. up: prevSibling,
  4100. next: closeSub,
  4101. previous: openSub
  4102. });
  4103. } else { // left aligned
  4104. $.extend(functions, {
  4105. down: nextSibling,
  4106. up: prevSibling,
  4107. next: openSub,
  4108. previous: closeSub
  4109. });
  4110. }
  4111. } else { // horizontal menu
  4112. if (_this._isRtl()) { // right aligned
  4113. $.extend(functions, {
  4114. next: prevSibling,
  4115. previous: nextSibling,
  4116. down: openSub,
  4117. up: closeSub
  4118. });
  4119. } else { // left aligned
  4120. $.extend(functions, {
  4121. next: nextSibling,
  4122. previous: prevSibling,
  4123. down: openSub,
  4124. up: closeSub
  4125. });
  4126. }
  4127. }
  4128. } else { // not tabs -> one sub
  4129. if (_this._isRtl()) { // right aligned
  4130. $.extend(functions, {
  4131. next: closeSub,
  4132. previous: openSub,
  4133. down: nextSibling,
  4134. up: prevSibling
  4135. });
  4136. } else { // left aligned
  4137. $.extend(functions, {
  4138. next: openSub,
  4139. previous: closeSub,
  4140. down: nextSibling,
  4141. up: prevSibling
  4142. });
  4143. }
  4144. }
  4145. Keyboard.handleKey(e, 'DropdownMenu', functions);
  4146. });
  4147. }
  4148. /**
  4149. * Adds an event handler to the body to close any dropdowns on a click.
  4150. * @function
  4151. * @private
  4152. */
  4153. _addBodyHandler() {
  4154. var $body = $(document.body),
  4155. _this = this;
  4156. $body.off('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu')
  4157. .on('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu', function(e) {
  4158. var $link = _this.$element.find(e.target);
  4159. if ($link.length) { return; }
  4160. _this._hide();
  4161. $body.off('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu');
  4162. });
  4163. }
  4164. /**
  4165. * Opens a dropdown pane, and checks for collisions first.
  4166. * @param {jQuery} $sub - ul element that is a submenu to show
  4167. * @function
  4168. * @private
  4169. * @fires Dropdownmenu#show
  4170. */
  4171. _show($sub) {
  4172. var idx = this.$tabs.index(this.$tabs.filter(function(i, el) {
  4173. return $(el).find($sub).length > 0;
  4174. }));
  4175. var $sibs = $sub.parent('li.is-dropdown-submenu-parent').siblings('li.is-dropdown-submenu-parent');
  4176. this._hide($sibs, idx);
  4177. $sub.css('visibility', 'hidden').addClass('js-dropdown-active')
  4178. .parent('li.is-dropdown-submenu-parent').addClass('is-active');
  4179. var clear = Box.ImNotTouchingYou($sub, null, true);
  4180. if (!clear) {
  4181. var oldClass = this.options.alignment === 'left' ? '-right' : '-left',
  4182. $parentLi = $sub.parent('.is-dropdown-submenu-parent');
  4183. $parentLi.removeClass(`opens${oldClass}`).addClass(`opens-${this.options.alignment}`);
  4184. clear = Box.ImNotTouchingYou($sub, null, true);
  4185. if (!clear) {
  4186. $parentLi.removeClass(`opens-${this.options.alignment}`).addClass('opens-inner');
  4187. }
  4188. this.changed = true;
  4189. }
  4190. $sub.css('visibility', '');
  4191. if (this.options.closeOnClick) { this._addBodyHandler(); }
  4192. /**
  4193. * Fires when the new dropdown pane is visible.
  4194. * @event Dropdownmenu#show
  4195. */
  4196. this.$element.trigger('show.zf.dropdownmenu', [$sub]);
  4197. }
  4198. /**
  4199. * Hides a single, currently open dropdown pane, if passed a parameter, otherwise, hides everything.
  4200. * @function
  4201. * @param {jQuery} $elem - element with a submenu to hide
  4202. * @param {Number} idx - index of the $tabs collection to hide
  4203. * @private
  4204. */
  4205. _hide($elem, idx) {
  4206. var $toClose;
  4207. if ($elem && $elem.length) {
  4208. $toClose = $elem;
  4209. } else if (typeof idx !== 'undefined') {
  4210. $toClose = this.$tabs.not(function(i, el) {
  4211. return i === idx;
  4212. });
  4213. }
  4214. else {
  4215. $toClose = this.$element;
  4216. }
  4217. var somethingToClose = $toClose.hasClass('is-active') || $toClose.find('.is-active').length > 0;
  4218. if (somethingToClose) {
  4219. $toClose.find('li.is-active').add($toClose).attr({
  4220. 'data-is-click': false
  4221. }).removeClass('is-active');
  4222. $toClose.find('ul.js-dropdown-active').removeClass('js-dropdown-active');
  4223. if (this.changed || $toClose.find('opens-inner').length) {
  4224. var oldClass = this.options.alignment === 'left' ? 'right' : 'left';
  4225. $toClose.find('li.is-dropdown-submenu-parent').add($toClose)
  4226. .removeClass(`opens-inner opens-${this.options.alignment}`)
  4227. .addClass(`opens-${oldClass}`);
  4228. this.changed = false;
  4229. }
  4230. /**
  4231. * Fires when the open menus are closed.
  4232. * @event Dropdownmenu#hide
  4233. */
  4234. this.$element.trigger('hide.zf.dropdownmenu', [$toClose]);
  4235. }
  4236. }
  4237. /**
  4238. * Destroys the plugin.
  4239. * @function
  4240. */
  4241. _destroy() {
  4242. this.$menuItems.off('.zf.dropdownmenu').removeAttr('data-is-click')
  4243. .removeClass('is-right-arrow is-left-arrow is-down-arrow opens-right opens-left opens-inner');
  4244. $(document.body).off('.zf.dropdownmenu');
  4245. Nest.Burn(this.$element, 'dropdown');
  4246. }
  4247. }
  4248. /**
  4249. * Default settings for plugin
  4250. */
  4251. DropdownMenu.defaults = {
  4252. /**
  4253. * Disallows hover events from opening submenus
  4254. * @option
  4255. * @type {boolean}
  4256. * @default false
  4257. */
  4258. disableHover: false,
  4259. /**
  4260. * Allow a submenu to automatically close on a mouseleave event, if not clicked open.
  4261. * @option
  4262. * @type {boolean}
  4263. * @default true
  4264. */
  4265. autoclose: true,
  4266. /**
  4267. * Amount of time to delay opening a submenu on hover event.
  4268. * @option
  4269. * @type {number}
  4270. * @default 50
  4271. */
  4272. hoverDelay: 50,
  4273. /**
  4274. * Allow a submenu to open/remain open on parent click event. Allows cursor to move away from menu.
  4275. * @option
  4276. * @type {boolean}
  4277. * @default false
  4278. */
  4279. clickOpen: false,
  4280. /**
  4281. * Amount of time to delay closing a submenu on a mouseleave event.
  4282. * @option
  4283. * @type {number}
  4284. * @default 500
  4285. */
  4286. closingTime: 500,
  4287. /**
  4288. * Position of the menu relative to what direction the submenus should open. Handled by JS. Can be `'auto'`, `'left'` or `'right'`.
  4289. * @option
  4290. * @type {string}
  4291. * @default 'auto'
  4292. */
  4293. alignment: 'auto',
  4294. /**
  4295. * Allow clicks on the body to close any open submenus.
  4296. * @option
  4297. * @type {boolean}
  4298. * @default true
  4299. */
  4300. closeOnClick: true,
  4301. /**
  4302. * Allow clicks on leaf anchor links to close any open submenus.
  4303. * @option
  4304. * @type {boolean}
  4305. * @default true
  4306. */
  4307. closeOnClickInside: true,
  4308. /**
  4309. * Class applied to vertical oriented menus, Foundation default is `vertical`. Update this if using your own class.
  4310. * @option
  4311. * @type {string}
  4312. * @default 'vertical'
  4313. */
  4314. verticalClass: 'vertical',
  4315. /**
  4316. * Class applied to right-side oriented menus, Foundation default is `align-right`. Update this if using your own class.
  4317. * @option
  4318. * @type {string}
  4319. * @default 'align-right'
  4320. */
  4321. rightClass: 'align-right',
  4322. /**
  4323. * Boolean to force overide the clicking of links to perform default action, on second touch event for mobile.
  4324. * @option
  4325. * @type {boolean}
  4326. * @default true
  4327. */
  4328. forceFollow: true
  4329. };
  4330. /**
  4331. * Equalizer module.
  4332. * @module foundation.equalizer
  4333. * @requires foundation.util.mediaQuery
  4334. * @requires foundation.util.imageLoader if equalizer contains images
  4335. */
  4336. class Equalizer extends Plugin {
  4337. /**
  4338. * Creates a new instance of Equalizer.
  4339. * @class
  4340. * @name Equalizer
  4341. * @fires Equalizer#init
  4342. * @param {Object} element - jQuery object to add the trigger to.
  4343. * @param {Object} options - Overrides to the default plugin settings.
  4344. */
  4345. _setup(element, options){
  4346. this.$element = element;
  4347. this.options = $.extend({}, Equalizer.defaults, this.$element.data(), options);
  4348. this.className = 'Equalizer'; // ie9 back compat
  4349. this._init();
  4350. }
  4351. /**
  4352. * Initializes the Equalizer plugin and calls functions to get equalizer functioning on load.
  4353. * @private
  4354. */
  4355. _init() {
  4356. var eqId = this.$element.attr('data-equalizer') || '';
  4357. var $watched = this.$element.find(`[data-equalizer-watch="${eqId}"]`);
  4358. MediaQuery._init();
  4359. this.$watched = $watched.length ? $watched : this.$element.find('[data-equalizer-watch]');
  4360. this.$element.attr('data-resize', (eqId || GetYoDigits(6, 'eq')));
  4361. this.$element.attr('data-mutate', (eqId || GetYoDigits(6, 'eq')));
  4362. this.hasNested = this.$element.find('[data-equalizer]').length > 0;
  4363. this.isNested = this.$element.parentsUntil(document.body, '[data-equalizer]').length > 0;
  4364. this.isOn = false;
  4365. this._bindHandler = {
  4366. onResizeMeBound: this._onResizeMe.bind(this),
  4367. onPostEqualizedBound: this._onPostEqualized.bind(this)
  4368. };
  4369. var imgs = this.$element.find('img');
  4370. var tooSmall;
  4371. if(this.options.equalizeOn){
  4372. tooSmall = this._checkMQ();
  4373. $(window).on('changed.zf.mediaquery', this._checkMQ.bind(this));
  4374. }else{
  4375. this._events();
  4376. }
  4377. if((typeof tooSmall !== 'undefined' && tooSmall === false) || typeof tooSmall === 'undefined'){
  4378. if(imgs.length){
  4379. onImagesLoaded(imgs, this._reflow.bind(this));
  4380. }else{
  4381. this._reflow();
  4382. }
  4383. }
  4384. }
  4385. /**
  4386. * Removes event listeners if the breakpoint is too small.
  4387. * @private
  4388. */
  4389. _pauseEvents() {
  4390. this.isOn = false;
  4391. this.$element.off({
  4392. '.zf.equalizer': this._bindHandler.onPostEqualizedBound,
  4393. 'resizeme.zf.trigger': this._bindHandler.onResizeMeBound,
  4394. 'mutateme.zf.trigger': this._bindHandler.onResizeMeBound
  4395. });
  4396. }
  4397. /**
  4398. * function to handle $elements resizeme.zf.trigger, with bound this on _bindHandler.onResizeMeBound
  4399. * @private
  4400. */
  4401. _onResizeMe(e) {
  4402. this._reflow();
  4403. }
  4404. /**
  4405. * function to handle $elements postequalized.zf.equalizer, with bound this on _bindHandler.onPostEqualizedBound
  4406. * @private
  4407. */
  4408. _onPostEqualized(e) {
  4409. if(e.target !== this.$element[0]){ this._reflow(); }
  4410. }
  4411. /**
  4412. * Initializes events for Equalizer.
  4413. * @private
  4414. */
  4415. _events() {
  4416. this._pauseEvents();
  4417. if(this.hasNested){
  4418. this.$element.on('postequalized.zf.equalizer', this._bindHandler.onPostEqualizedBound);
  4419. }else{
  4420. this.$element.on('resizeme.zf.trigger', this._bindHandler.onResizeMeBound);
  4421. this.$element.on('mutateme.zf.trigger', this._bindHandler.onResizeMeBound);
  4422. }
  4423. this.isOn = true;
  4424. }
  4425. /**
  4426. * Checks the current breakpoint to the minimum required size.
  4427. * @private
  4428. */
  4429. _checkMQ() {
  4430. var tooSmall = !MediaQuery.is(this.options.equalizeOn);
  4431. if(tooSmall){
  4432. if(this.isOn){
  4433. this._pauseEvents();
  4434. this.$watched.css('height', 'auto');
  4435. }
  4436. }else{
  4437. if(!this.isOn){
  4438. this._events();
  4439. }
  4440. }
  4441. return tooSmall;
  4442. }
  4443. /**
  4444. * A noop version for the plugin
  4445. * @private
  4446. */
  4447. _killswitch() {
  4448. return;
  4449. }
  4450. /**
  4451. * Calls necessary functions to update Equalizer upon DOM change
  4452. * @private
  4453. */
  4454. _reflow() {
  4455. if(!this.options.equalizeOnStack){
  4456. if(this._isStacked()){
  4457. this.$watched.css('height', 'auto');
  4458. return false;
  4459. }
  4460. }
  4461. if (this.options.equalizeByRow) {
  4462. this.getHeightsByRow(this.applyHeightByRow.bind(this));
  4463. }else{
  4464. this.getHeights(this.applyHeight.bind(this));
  4465. }
  4466. }
  4467. /**
  4468. * Manually determines if the first 2 elements are *NOT* stacked.
  4469. * @private
  4470. */
  4471. _isStacked() {
  4472. if (!this.$watched[0] || !this.$watched[1]) {
  4473. return true;
  4474. }
  4475. return this.$watched[0].getBoundingClientRect().top !== this.$watched[1].getBoundingClientRect().top;
  4476. }
  4477. /**
  4478. * Finds the outer heights of children contained within an Equalizer parent and returns them in an array
  4479. * @param {Function} cb - A non-optional callback to return the heights array to.
  4480. * @returns {Array} heights - An array of heights of children within Equalizer container
  4481. */
  4482. getHeights(cb) {
  4483. var heights = [];
  4484. for(var i = 0, len = this.$watched.length; i < len; i++){
  4485. this.$watched[i].style.height = 'auto';
  4486. heights.push(this.$watched[i].offsetHeight);
  4487. }
  4488. cb(heights);
  4489. }
  4490. /**
  4491. * Finds the outer heights of children contained within an Equalizer parent and returns them in an array
  4492. * @param {Function} cb - A non-optional callback to return the heights array to.
  4493. * @returns {Array} groups - An array of heights of children within Equalizer container grouped by row with element,height and max as last child
  4494. */
  4495. getHeightsByRow(cb) {
  4496. var lastElTopOffset = (this.$watched.length ? this.$watched.first().offset().top : 0),
  4497. groups = [],
  4498. group = 0;
  4499. //group by Row
  4500. groups[group] = [];
  4501. for(var i = 0, len = this.$watched.length; i < len; i++){
  4502. this.$watched[i].style.height = 'auto';
  4503. //maybe could use this.$watched[i].offsetTop
  4504. var elOffsetTop = $(this.$watched[i]).offset().top;
  4505. if (elOffsetTop!=lastElTopOffset) {
  4506. group++;
  4507. groups[group] = [];
  4508. lastElTopOffset=elOffsetTop;
  4509. }
  4510. groups[group].push([this.$watched[i],this.$watched[i].offsetHeight]);
  4511. }
  4512. for (var j = 0, ln = groups.length; j < ln; j++) {
  4513. var heights = $(groups[j]).map(function(){ return this[1]; }).get();
  4514. var max = Math.max.apply(null, heights);
  4515. groups[j].push(max);
  4516. }
  4517. cb(groups);
  4518. }
  4519. /**
  4520. * Changes the CSS height property of each child in an Equalizer parent to match the tallest
  4521. * @param {array} heights - An array of heights of children within Equalizer container
  4522. * @fires Equalizer#preequalized
  4523. * @fires Equalizer#postequalized
  4524. */
  4525. applyHeight(heights) {
  4526. var max = Math.max.apply(null, heights);
  4527. /**
  4528. * Fires before the heights are applied
  4529. * @event Equalizer#preequalized
  4530. */
  4531. this.$element.trigger('preequalized.zf.equalizer');
  4532. this.$watched.css('height', max);
  4533. /**
  4534. * Fires when the heights have been applied
  4535. * @event Equalizer#postequalized
  4536. */
  4537. this.$element.trigger('postequalized.zf.equalizer');
  4538. }
  4539. /**
  4540. * Changes the CSS height property of each child in an Equalizer parent to match the tallest by row
  4541. * @param {array} groups - An array of heights of children within Equalizer container grouped by row with element,height and max as last child
  4542. * @fires Equalizer#preequalized
  4543. * @fires Equalizer#preequalizedrow
  4544. * @fires Equalizer#postequalizedrow
  4545. * @fires Equalizer#postequalized
  4546. */
  4547. applyHeightByRow(groups) {
  4548. /**
  4549. * Fires before the heights are applied
  4550. */
  4551. this.$element.trigger('preequalized.zf.equalizer');
  4552. for (var i = 0, len = groups.length; i < len ; i++) {
  4553. var groupsILength = groups[i].length,
  4554. max = groups[i][groupsILength - 1];
  4555. if (groupsILength<=2) {
  4556. $(groups[i][0][0]).css({'height':'auto'});
  4557. continue;
  4558. }
  4559. /**
  4560. * Fires before the heights per row are applied
  4561. * @event Equalizer#preequalizedrow
  4562. */
  4563. this.$element.trigger('preequalizedrow.zf.equalizer');
  4564. for (var j = 0, lenJ = (groupsILength-1); j < lenJ ; j++) {
  4565. $(groups[i][j][0]).css({'height':max});
  4566. }
  4567. /**
  4568. * Fires when the heights per row have been applied
  4569. * @event Equalizer#postequalizedrow
  4570. */
  4571. this.$element.trigger('postequalizedrow.zf.equalizer');
  4572. }
  4573. /**
  4574. * Fires when the heights have been applied
  4575. */
  4576. this.$element.trigger('postequalized.zf.equalizer');
  4577. }
  4578. /**
  4579. * Destroys an instance of Equalizer.
  4580. * @function
  4581. */
  4582. _destroy() {
  4583. this._pauseEvents();
  4584. this.$watched.css('height', 'auto');
  4585. }
  4586. }
  4587. /**
  4588. * Default settings for plugin
  4589. */
  4590. Equalizer.defaults = {
  4591. /**
  4592. * Enable height equalization when stacked on smaller screens.
  4593. * @option
  4594. * @type {boolean}
  4595. * @default false
  4596. */
  4597. equalizeOnStack: false,
  4598. /**
  4599. * Enable height equalization row by row.
  4600. * @option
  4601. * @type {boolean}
  4602. * @default false
  4603. */
  4604. equalizeByRow: false,
  4605. /**
  4606. * String representing the minimum breakpoint size the plugin should equalize heights on.
  4607. * @option
  4608. * @type {string}
  4609. * @default ''
  4610. */
  4611. equalizeOn: ''
  4612. };
  4613. /**
  4614. * Interchange module.
  4615. * @module foundation.interchange
  4616. * @requires foundation.util.mediaQuery
  4617. */
  4618. class Interchange extends Plugin {
  4619. /**
  4620. * Creates a new instance of Interchange.
  4621. * @class
  4622. * @name Interchange
  4623. * @fires Interchange#init
  4624. * @param {Object} element - jQuery object to add the trigger to.
  4625. * @param {Object} options - Overrides to the default plugin settings.
  4626. */
  4627. _setup(element, options) {
  4628. this.$element = element;
  4629. this.options = $.extend({}, Interchange.defaults, options);
  4630. this.rules = [];
  4631. this.currentPath = '';
  4632. this.className = 'Interchange'; // ie9 back compat
  4633. this._init();
  4634. this._events();
  4635. }
  4636. /**
  4637. * Initializes the Interchange plugin and calls functions to get interchange functioning on load.
  4638. * @function
  4639. * @private
  4640. */
  4641. _init() {
  4642. MediaQuery._init();
  4643. var id = this.$element[0].id || GetYoDigits(6, 'interchange');
  4644. this.$element.attr({
  4645. 'data-resize': id,
  4646. 'id': id
  4647. });
  4648. this._addBreakpoints();
  4649. this._generateRules();
  4650. this._reflow();
  4651. }
  4652. /**
  4653. * Initializes events for Interchange.
  4654. * @function
  4655. * @private
  4656. */
  4657. _events() {
  4658. this.$element.off('resizeme.zf.trigger').on('resizeme.zf.trigger', () => this._reflow());
  4659. }
  4660. /**
  4661. * Calls necessary functions to update Interchange upon DOM change
  4662. * @function
  4663. * @private
  4664. */
  4665. _reflow() {
  4666. var match;
  4667. // Iterate through each rule, but only save the last match
  4668. for (var i in this.rules) {
  4669. if(this.rules.hasOwnProperty(i)) {
  4670. var rule = this.rules[i];
  4671. if (window.matchMedia(rule.query).matches) {
  4672. match = rule;
  4673. }
  4674. }
  4675. }
  4676. if (match) {
  4677. this.replace(match.path);
  4678. }
  4679. }
  4680. /**
  4681. * Gets the Foundation breakpoints and adds them to the Interchange.SPECIAL_QUERIES object.
  4682. * @function
  4683. * @private
  4684. */
  4685. _addBreakpoints() {
  4686. for (var i in MediaQuery.queries) {
  4687. if (MediaQuery.queries.hasOwnProperty(i)) {
  4688. var query = MediaQuery.queries[i];
  4689. Interchange.SPECIAL_QUERIES[query.name] = query.value;
  4690. }
  4691. }
  4692. }
  4693. /**
  4694. * Checks the Interchange element for the provided media query + content pairings
  4695. * @function
  4696. * @private
  4697. * @param {Object} element - jQuery object that is an Interchange instance
  4698. * @returns {Array} scenarios - Array of objects that have 'mq' and 'path' keys with corresponding keys
  4699. */
  4700. _generateRules(element) {
  4701. var rulesList = [];
  4702. var rules;
  4703. if (this.options.rules) {
  4704. rules = this.options.rules;
  4705. }
  4706. else {
  4707. rules = this.$element.data('interchange');
  4708. }
  4709. rules = typeof rules === 'string' ? rules.match(/\[.*?, .*?\]/g) : rules;
  4710. for (var i in rules) {
  4711. if(rules.hasOwnProperty(i)) {
  4712. var rule = rules[i].slice(1, -1).split(', ');
  4713. var path = rule.slice(0, -1).join('');
  4714. var query = rule[rule.length - 1];
  4715. if (Interchange.SPECIAL_QUERIES[query]) {
  4716. query = Interchange.SPECIAL_QUERIES[query];
  4717. }
  4718. rulesList.push({
  4719. path: path,
  4720. query: query
  4721. });
  4722. }
  4723. }
  4724. this.rules = rulesList;
  4725. }
  4726. /**
  4727. * Update the `src` property of an image, or change the HTML of a container, to the specified path.
  4728. * @function
  4729. * @param {String} path - Path to the image or HTML partial.
  4730. * @fires Interchange#replaced
  4731. */
  4732. replace(path) {
  4733. if (this.currentPath === path) return;
  4734. var _this = this,
  4735. trigger = 'replaced.zf.interchange';
  4736. // Replacing images
  4737. if (this.$element[0].nodeName === 'IMG') {
  4738. this.$element.attr('src', path).on('load', function() {
  4739. _this.currentPath = path;
  4740. })
  4741. .trigger(trigger);
  4742. }
  4743. // Replacing background images
  4744. else if (path.match(/\.(gif|jpg|jpeg|png|svg|tiff)([?#].*)?/i)) {
  4745. path = path.replace(/\(/g, '%28').replace(/\)/g, '%29');
  4746. this.$element.css({ 'background-image': 'url('+path+')' })
  4747. .trigger(trigger);
  4748. }
  4749. // Replacing HTML
  4750. else {
  4751. $.get(path, function(response) {
  4752. _this.$element.html(response)
  4753. .trigger(trigger);
  4754. $(response).foundation();
  4755. _this.currentPath = path;
  4756. });
  4757. }
  4758. /**
  4759. * Fires when content in an Interchange element is done being loaded.
  4760. * @event Interchange#replaced
  4761. */
  4762. // this.$element.trigger('replaced.zf.interchange');
  4763. }
  4764. /**
  4765. * Destroys an instance of interchange.
  4766. * @function
  4767. */
  4768. _destroy() {
  4769. this.$element.off('resizeme.zf.trigger');
  4770. }
  4771. }
  4772. /**
  4773. * Default settings for plugin
  4774. */
  4775. Interchange.defaults = {
  4776. /**
  4777. * Rules to be applied to Interchange elements. Set with the `data-interchange` array notation.
  4778. * @option
  4779. * @type {?array}
  4780. * @default null
  4781. */
  4782. rules: null
  4783. };
  4784. Interchange.SPECIAL_QUERIES = {
  4785. 'landscape': 'screen and (orientation: landscape)',
  4786. 'portrait': 'screen and (orientation: portrait)',
  4787. 'retina': 'only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)'
  4788. };
  4789. /**
  4790. * SmoothScroll module.
  4791. * @module foundation.smooth-scroll
  4792. */
  4793. class SmoothScroll extends Plugin {
  4794. /**
  4795. * Creates a new instance of SmoothScroll.
  4796. * @class
  4797. * @name SmoothScroll
  4798. * @fires SmoothScroll#init
  4799. * @param {Object} element - jQuery object to add the trigger to.
  4800. * @param {Object} options - Overrides to the default plugin settings.
  4801. */
  4802. _setup(element, options) {
  4803. this.$element = element;
  4804. this.options = $.extend({}, SmoothScroll.defaults, this.$element.data(), options);
  4805. this.className = 'SmoothScroll'; // ie9 back compat
  4806. this._init();
  4807. }
  4808. /**
  4809. * Initialize the SmoothScroll plugin
  4810. * @private
  4811. */
  4812. _init() {
  4813. const id = this.$element[0].id || GetYoDigits(6, 'smooth-scroll');
  4814. this.$element.attr({ id });
  4815. this._events();
  4816. }
  4817. /**
  4818. * Initializes events for SmoothScroll.
  4819. * @private
  4820. */
  4821. _events() {
  4822. this._linkClickListener = this._handleLinkClick.bind(this);
  4823. this.$element.on('click.zf.smoothScroll', this._linkClickListener);
  4824. this.$element.on('click.zf.smoothScroll', 'a[href^="#"]', this._linkClickListener);
  4825. }
  4826. /**
  4827. * Handle the given event to smoothly scroll to the anchor pointed by the event target.
  4828. * @param {*} e - event
  4829. * @function
  4830. * @private
  4831. */
  4832. _handleLinkClick(e) {
  4833. // Follow the link if it does not point to an anchor.
  4834. if (!$(e.currentTarget).is('a[href^="#"]')) return;
  4835. const arrival = e.currentTarget.getAttribute('href');
  4836. this._inTransition = true;
  4837. SmoothScroll.scrollToLoc(arrival, this.options, () => {
  4838. this._inTransition = false;
  4839. });
  4840. e.preventDefault();
  4841. };
  4842. /**
  4843. * Function to scroll to a given location on the page.
  4844. * @param {String} loc - A properly formatted jQuery id selector. Example: '#foo'
  4845. * @param {Object} options - The options to use.
  4846. * @param {Function} callback - The callback function.
  4847. * @static
  4848. * @function
  4849. */
  4850. static scrollToLoc(loc, options = SmoothScroll.defaults, callback) {
  4851. const $loc = $(loc);
  4852. // Do nothing if target does not exist to prevent errors
  4853. if (!$loc.length) return false;
  4854. var scrollPos = Math.round($loc.offset().top - options.threshold / 2 - options.offset);
  4855. $('html, body').stop(true).animate(
  4856. { scrollTop: scrollPos },
  4857. options.animationDuration,
  4858. options.animationEasing,
  4859. () => {
  4860. if (typeof callback === 'function'){
  4861. callback();
  4862. }
  4863. }
  4864. );
  4865. }
  4866. /**
  4867. * Destroys the SmoothScroll instance.
  4868. * @function
  4869. */
  4870. _destroy() {
  4871. this.$element.off('click.zf.smoothScroll', this._linkClickListener);
  4872. this.$element.off('click.zf.smoothScroll', 'a[href^="#"]', this._linkClickListener);
  4873. }
  4874. }
  4875. /**
  4876. * Default settings for plugin.
  4877. */
  4878. SmoothScroll.defaults = {
  4879. /**
  4880. * Amount of time, in ms, the animated scrolling should take between locations.
  4881. * @option
  4882. * @type {number}
  4883. * @default 500
  4884. */
  4885. animationDuration: 500,
  4886. /**
  4887. * Animation style to use when scrolling between locations. Can be `'swing'` or `'linear'`.
  4888. * @option
  4889. * @type {string}
  4890. * @default 'linear'
  4891. * @see {@link https://api.jquery.com/animate|Jquery animate}
  4892. */
  4893. animationEasing: 'linear',
  4894. /**
  4895. * Number of pixels to use as a marker for location changes.
  4896. * @option
  4897. * @type {number}
  4898. * @default 50
  4899. */
  4900. threshold: 50,
  4901. /**
  4902. * Number of pixels to offset the scroll of the page on item click if using a sticky nav bar.
  4903. * @option
  4904. * @type {number}
  4905. * @default 0
  4906. */
  4907. offset: 0
  4908. };
  4909. /**
  4910. * Magellan module.
  4911. * @module foundation.magellan
  4912. * @requires foundation.smoothScroll
  4913. */
  4914. class Magellan extends Plugin {
  4915. /**
  4916. * Creates a new instance of Magellan.
  4917. * @class
  4918. * @name Magellan
  4919. * @fires Magellan#init
  4920. * @param {Object} element - jQuery object to add the trigger to.
  4921. * @param {Object} options - Overrides to the default plugin settings.
  4922. */
  4923. _setup(element, options) {
  4924. this.$element = element;
  4925. this.options = $.extend({}, Magellan.defaults, this.$element.data(), options);
  4926. this.className = 'Magellan'; // ie9 back compat
  4927. this._init();
  4928. this.calcPoints();
  4929. }
  4930. /**
  4931. * Initializes the Magellan plugin and calls functions to get equalizer functioning on load.
  4932. * @private
  4933. */
  4934. _init() {
  4935. var id = this.$element[0].id || GetYoDigits(6, 'magellan');
  4936. this.$targets = $('[data-magellan-target]');
  4937. this.$links = this.$element.find('a');
  4938. this.$element.attr({
  4939. 'data-resize': id,
  4940. 'data-scroll': id,
  4941. 'id': id
  4942. });
  4943. this.$active = $();
  4944. this.scrollPos = parseInt(window.pageYOffset, 10);
  4945. this._events();
  4946. }
  4947. /**
  4948. * Calculates an array of pixel values that are the demarcation lines between locations on the page.
  4949. * Can be invoked if new elements are added or the size of a location changes.
  4950. * @function
  4951. */
  4952. calcPoints() {
  4953. var _this = this,
  4954. body = document.body,
  4955. html = document.documentElement;
  4956. this.points = [];
  4957. this.winHeight = Math.round(Math.max(window.innerHeight, html.clientHeight));
  4958. this.docHeight = Math.round(Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight));
  4959. this.$targets.each(function(){
  4960. var $tar = $(this),
  4961. pt = Math.round($tar.offset().top - _this.options.threshold);
  4962. $tar.targetPoint = pt;
  4963. _this.points.push(pt);
  4964. });
  4965. }
  4966. /**
  4967. * Initializes events for Magellan.
  4968. * @private
  4969. */
  4970. _events() {
  4971. var _this = this,
  4972. $body = $('html, body'),
  4973. opts = {
  4974. duration: _this.options.animationDuration,
  4975. easing: _this.options.animationEasing
  4976. };
  4977. $(window).one('load', function(){
  4978. if(_this.options.deepLinking){
  4979. if(location.hash){
  4980. _this.scrollToLoc(location.hash);
  4981. }
  4982. }
  4983. _this.calcPoints();
  4984. _this._updateActive();
  4985. });
  4986. _this.onLoadListener = onLoad($(window), function () {
  4987. _this.$element
  4988. .on({
  4989. 'resizeme.zf.trigger': _this.reflow.bind(_this),
  4990. 'scrollme.zf.trigger': _this._updateActive.bind(_this)
  4991. })
  4992. .on('click.zf.magellan', 'a[href^="#"]', function (e) {
  4993. e.preventDefault();
  4994. var arrival = this.getAttribute('href');
  4995. _this.scrollToLoc(arrival);
  4996. });
  4997. });
  4998. this._deepLinkScroll = function(e) {
  4999. if(_this.options.deepLinking) {
  5000. _this.scrollToLoc(window.location.hash);
  5001. }
  5002. };
  5003. $(window).on('hashchange', this._deepLinkScroll);
  5004. }
  5005. /**
  5006. * Function to scroll to a given location on the page.
  5007. * @param {String} loc - a properly formatted jQuery id selector. Example: '#foo'
  5008. * @function
  5009. */
  5010. scrollToLoc(loc) {
  5011. this._inTransition = true;
  5012. var _this = this;
  5013. var options = {
  5014. animationEasing: this.options.animationEasing,
  5015. animationDuration: this.options.animationDuration,
  5016. threshold: this.options.threshold,
  5017. offset: this.options.offset
  5018. };
  5019. SmoothScroll.scrollToLoc(loc, options, function() {
  5020. _this._inTransition = false;
  5021. });
  5022. }
  5023. /**
  5024. * Calls necessary functions to update Magellan upon DOM change
  5025. * @function
  5026. */
  5027. reflow() {
  5028. this.calcPoints();
  5029. this._updateActive();
  5030. }
  5031. /**
  5032. * Updates the visibility of an active location link, and updates the url hash for the page, if deepLinking enabled.
  5033. * @private
  5034. * @function
  5035. * @fires Magellan#update
  5036. */
  5037. _updateActive(/*evt, elem, scrollPos*/) {
  5038. if(this._inTransition) return;
  5039. const newScrollPos = parseInt(window.pageYOffset, 10);
  5040. const isScrollingUp = this.scrollPos > newScrollPos;
  5041. this.scrollPos = newScrollPos;
  5042. let activeIdx;
  5043. // Before the first point: no link
  5044. if(newScrollPos < this.points[0]);
  5045. // At the bottom of the page: last link
  5046. else if(newScrollPos + this.winHeight === this.docHeight){ activeIdx = this.points.length - 1; }
  5047. // Otherwhise, use the last visible link
  5048. else{
  5049. const visibleLinks = this.points.filter((p, i) => {
  5050. return (p - this.options.offset - (isScrollingUp ? this.options.threshold : 0)) <= newScrollPos;
  5051. });
  5052. activeIdx = visibleLinks.length ? visibleLinks.length - 1 : 0;
  5053. }
  5054. // Get the new active link
  5055. const $oldActive = this.$active;
  5056. let activeHash = '';
  5057. if(typeof activeIdx !== 'undefined'){
  5058. this.$active = this.$links.filter('[href="#' + this.$targets.eq(activeIdx).data('magellan-target') + '"]');
  5059. if (this.$active.length) activeHash = this.$active[0].getAttribute('href');
  5060. }else{
  5061. this.$active = $();
  5062. }
  5063. const isNewActive = !(!this.$active.length && !$oldActive.length) && !this.$active.is($oldActive);
  5064. const isNewHash = activeHash !== window.location.hash;
  5065. // Update the active link element
  5066. if(isNewActive) {
  5067. $oldActive.removeClass(this.options.activeClass);
  5068. this.$active.addClass(this.options.activeClass);
  5069. }
  5070. // Update the hash (it may have changed with the same active link)
  5071. if(this.options.deepLinking && isNewHash){
  5072. if(window.history.pushState){
  5073. // Set or remove the hash (see: https://stackoverflow.com/a/5298684/4317384
  5074. const url = activeHash ? activeHash : window.location.pathname + window.location.search;
  5075. window.history.pushState(null, null, url);
  5076. }else{
  5077. window.location.hash = activeHash;
  5078. }
  5079. }
  5080. if (isNewActive) {
  5081. /**
  5082. * Fires when magellan is finished updating to the new active element.
  5083. * @event Magellan#update
  5084. */
  5085. this.$element.trigger('update.zf.magellan', [this.$active]);
  5086. }
  5087. }
  5088. /**
  5089. * Destroys an instance of Magellan and resets the url of the window.
  5090. * @function
  5091. */
  5092. _destroy() {
  5093. this.$element.off('.zf.trigger .zf.magellan')
  5094. .find(`.${this.options.activeClass}`).removeClass(this.options.activeClass);
  5095. if(this.options.deepLinking){
  5096. var hash = this.$active[0].getAttribute('href');
  5097. window.location.hash.replace(hash, '');
  5098. }
  5099. $(window).off('hashchange', this._deepLinkScroll);
  5100. if (this.onLoadListener) $(window).off(this.onLoadListener);
  5101. }
  5102. }
  5103. /**
  5104. * Default settings for plugin
  5105. */
  5106. Magellan.defaults = {
  5107. /**
  5108. * Amount of time, in ms, the animated scrolling should take between locations.
  5109. * @option
  5110. * @type {number}
  5111. * @default 500
  5112. */
  5113. animationDuration: 500,
  5114. /**
  5115. * Animation style to use when scrolling between locations. Can be `'swing'` or `'linear'`.
  5116. * @option
  5117. * @type {string}
  5118. * @default 'linear'
  5119. * @see {@link https://api.jquery.com/animate|Jquery animate}
  5120. */
  5121. animationEasing: 'linear',
  5122. /**
  5123. * Number of pixels to use as a marker for location changes.
  5124. * @option
  5125. * @type {number}
  5126. * @default 50
  5127. */
  5128. threshold: 50,
  5129. /**
  5130. * Class applied to the active locations link on the magellan container.
  5131. * @option
  5132. * @type {string}
  5133. * @default 'is-active'
  5134. */
  5135. activeClass: 'is-active',
  5136. /**
  5137. * Allows the script to manipulate the url of the current page, and if supported, alter the history.
  5138. * @option
  5139. * @type {boolean}
  5140. * @default false
  5141. */
  5142. deepLinking: false,
  5143. /**
  5144. * Number of pixels to offset the scroll of the page on item click if using a sticky nav bar.
  5145. * @option
  5146. * @type {number}
  5147. * @default 0
  5148. */
  5149. offset: 0
  5150. };
  5151. /**
  5152. * OffCanvas module.
  5153. * @module foundation.offcanvas
  5154. * @requires foundation.util.keyboard
  5155. * @requires foundation.util.mediaQuery
  5156. * @requires foundation.util.triggers
  5157. */
  5158. class OffCanvas extends Plugin {
  5159. /**
  5160. * Creates a new instance of an off-canvas wrapper.
  5161. * @class
  5162. * @name OffCanvas
  5163. * @fires OffCanvas#init
  5164. * @param {Object} element - jQuery object to initialize.
  5165. * @param {Object} options - Overrides to the default plugin settings.
  5166. */
  5167. _setup(element, options) {
  5168. this.className = 'OffCanvas'; // ie9 back compat
  5169. this.$element = element;
  5170. this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options);
  5171. this.contentClasses = { base: [], reveal: [] };
  5172. this.$lastTrigger = $();
  5173. this.$triggers = $();
  5174. this.position = 'left';
  5175. this.$content = $();
  5176. this.nested = !!(this.options.nested);
  5177. // Defines the CSS transition/position classes of the off-canvas content container.
  5178. $(['push', 'overlap']).each((index, val) => {
  5179. this.contentClasses.base.push('has-transition-'+val);
  5180. });
  5181. $(['left', 'right', 'top', 'bottom']).each((index, val) => {
  5182. this.contentClasses.base.push('has-position-'+val);
  5183. this.contentClasses.reveal.push('has-reveal-'+val);
  5184. });
  5185. // Triggers init is idempotent, just need to make sure it is initialized
  5186. Triggers.init($);
  5187. MediaQuery._init();
  5188. this._init();
  5189. this._events();
  5190. Keyboard.register('OffCanvas', {
  5191. 'ESCAPE': 'close'
  5192. });
  5193. }
  5194. /**
  5195. * Initializes the off-canvas wrapper by adding the exit overlay (if needed).
  5196. * @function
  5197. * @private
  5198. */
  5199. _init() {
  5200. var id = this.$element.attr('id');
  5201. this.$element.attr('aria-hidden', 'true');
  5202. // Find off-canvas content, either by ID (if specified), by siblings or by closest selector (fallback)
  5203. if (this.options.contentId) {
  5204. this.$content = $('#'+this.options.contentId);
  5205. } else if (this.$element.siblings('[data-off-canvas-content]').length) {
  5206. this.$content = this.$element.siblings('[data-off-canvas-content]').first();
  5207. } else {
  5208. this.$content = this.$element.closest('[data-off-canvas-content]').first();
  5209. }
  5210. if (!this.options.contentId) {
  5211. // Assume that the off-canvas element is nested if it isn't a sibling of the content
  5212. this.nested = this.$element.siblings('[data-off-canvas-content]').length === 0;
  5213. } else if (this.options.contentId && this.options.nested === null) {
  5214. // Warning if using content ID without setting the nested option
  5215. // Once the element is nested it is required to work properly in this case
  5216. console.warn('Remember to use the nested option if using the content ID option!');
  5217. }
  5218. if (this.nested === true) {
  5219. // Force transition overlap if nested
  5220. this.options.transition = 'overlap';
  5221. // Remove appropriate classes if already assigned in markup
  5222. this.$element.removeClass('is-transition-push');
  5223. }
  5224. this.$element.addClass(`is-transition-${this.options.transition} is-closed`);
  5225. // Find triggers that affect this element and add aria-expanded to them
  5226. this.$triggers = $(document)
  5227. .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]')
  5228. .attr('aria-expanded', 'false')
  5229. .attr('aria-controls', id);
  5230. // Get position by checking for related CSS class
  5231. this.position = this.$element.is('.position-left, .position-top, .position-right, .position-bottom') ? this.$element.attr('class').match(/position\-(left|top|right|bottom)/)[1] : this.position;
  5232. // Add an overlay over the content if necessary
  5233. if (this.options.contentOverlay === true) {
  5234. var overlay = document.createElement('div');
  5235. var overlayPosition = $(this.$element).css("position") === 'fixed' ? 'is-overlay-fixed' : 'is-overlay-absolute';
  5236. overlay.setAttribute('class', 'js-off-canvas-overlay ' + overlayPosition);
  5237. this.$overlay = $(overlay);
  5238. if(overlayPosition === 'is-overlay-fixed') {
  5239. $(this.$overlay).insertAfter(this.$element);
  5240. } else {
  5241. this.$content.append(this.$overlay);
  5242. }
  5243. }
  5244. // Get the revealOn option from the class.
  5245. var revealOnRegExp = new RegExp(RegExpEscape(this.options.revealClass) + '([^\\s]+)', 'g');
  5246. var revealOnClass = revealOnRegExp.exec(this.$element[0].className);
  5247. if (revealOnClass) {
  5248. this.options.isRevealed = true;
  5249. this.options.revealOn = this.options.revealOn || revealOnClass[1];
  5250. }
  5251. // Ensure the `reveal-on-*` class is set.
  5252. if (this.options.isRevealed === true && this.options.revealOn) {
  5253. this.$element.first().addClass(`${this.options.revealClass}${this.options.revealOn}`);
  5254. this._setMQChecker();
  5255. }
  5256. if (this.options.transitionTime) {
  5257. this.$element.css('transition-duration', this.options.transitionTime);
  5258. }
  5259. // Initally remove all transition/position CSS classes from off-canvas content container.
  5260. this._removeContentClasses();
  5261. }
  5262. /**
  5263. * Adds event handlers to the off-canvas wrapper and the exit overlay.
  5264. * @function
  5265. * @private
  5266. */
  5267. _events() {
  5268. this.$element.off('.zf.trigger .zf.offcanvas').on({
  5269. 'open.zf.trigger': this.open.bind(this),
  5270. 'close.zf.trigger': this.close.bind(this),
  5271. 'toggle.zf.trigger': this.toggle.bind(this),
  5272. 'keydown.zf.offcanvas': this._handleKeyboard.bind(this)
  5273. });
  5274. if (this.options.closeOnClick === true) {
  5275. var $target = this.options.contentOverlay ? this.$overlay : this.$content;
  5276. $target.on({'click.zf.offcanvas': this.close.bind(this)});
  5277. }
  5278. }
  5279. /**
  5280. * Applies event listener for elements that will reveal at certain breakpoints.
  5281. * @private
  5282. */
  5283. _setMQChecker() {
  5284. var _this = this;
  5285. this.onLoadListener = onLoad($(window), function () {
  5286. if (MediaQuery.atLeast(_this.options.revealOn)) {
  5287. _this.reveal(true);
  5288. }
  5289. });
  5290. $(window).on('changed.zf.mediaquery', function () {
  5291. if (MediaQuery.atLeast(_this.options.revealOn)) {
  5292. _this.reveal(true);
  5293. } else {
  5294. _this.reveal(false);
  5295. }
  5296. });
  5297. }
  5298. /**
  5299. * Removes the CSS transition/position classes of the off-canvas content container.
  5300. * Removing the classes is important when another off-canvas gets opened that uses the same content container.
  5301. * @param {Boolean} hasReveal - true if related off-canvas element is revealed.
  5302. * @private
  5303. */
  5304. _removeContentClasses(hasReveal) {
  5305. if (typeof hasReveal !== 'boolean') {
  5306. this.$content.removeClass(this.contentClasses.base.join(' '));
  5307. } else if (hasReveal === false) {
  5308. this.$content.removeClass(`has-reveal-${this.position}`);
  5309. }
  5310. }
  5311. /**
  5312. * Adds the CSS transition/position classes of the off-canvas content container, based on the opening off-canvas element.
  5313. * Beforehand any transition/position class gets removed.
  5314. * @param {Boolean} hasReveal - true if related off-canvas element is revealed.
  5315. * @private
  5316. */
  5317. _addContentClasses(hasReveal) {
  5318. this._removeContentClasses(hasReveal);
  5319. if (typeof hasReveal !== 'boolean') {
  5320. this.$content.addClass(`has-transition-${this.options.transition} has-position-${this.position}`);
  5321. } else if (hasReveal === true) {
  5322. this.$content.addClass(`has-reveal-${this.position}`);
  5323. }
  5324. }
  5325. /**
  5326. * Handles the revealing/hiding the off-canvas at breakpoints, not the same as open.
  5327. * @param {Boolean} isRevealed - true if element should be revealed.
  5328. * @function
  5329. */
  5330. reveal(isRevealed) {
  5331. if (isRevealed) {
  5332. this.close();
  5333. this.isRevealed = true;
  5334. this.$element.attr('aria-hidden', 'false');
  5335. this.$element.off('open.zf.trigger toggle.zf.trigger');
  5336. this.$element.removeClass('is-closed');
  5337. } else {
  5338. this.isRevealed = false;
  5339. this.$element.attr('aria-hidden', 'true');
  5340. this.$element.off('open.zf.trigger toggle.zf.trigger').on({
  5341. 'open.zf.trigger': this.open.bind(this),
  5342. 'toggle.zf.trigger': this.toggle.bind(this)
  5343. });
  5344. this.$element.addClass('is-closed');
  5345. }
  5346. this._addContentClasses(isRevealed);
  5347. }
  5348. /**
  5349. * Stops scrolling of the body when offcanvas is open on mobile Safari and other troublesome browsers.
  5350. * @private
  5351. */
  5352. _stopScrolling(event) {
  5353. return false;
  5354. }
  5355. // Taken and adapted from http://stackoverflow.com/questions/16889447/prevent-full-page-scrolling-ios
  5356. // Only really works for y, not sure how to extend to x or if we need to.
  5357. _recordScrollable(event) {
  5358. let elem = this; // called from event handler context with this as elem
  5359. // If the element is scrollable (content overflows), then...
  5360. if (elem.scrollHeight !== elem.clientHeight) {
  5361. // If we're at the top, scroll down one pixel to allow scrolling up
  5362. if (elem.scrollTop === 0) {
  5363. elem.scrollTop = 1;
  5364. }
  5365. // If we're at the bottom, scroll up one pixel to allow scrolling down
  5366. if (elem.scrollTop === elem.scrollHeight - elem.clientHeight) {
  5367. elem.scrollTop = elem.scrollHeight - elem.clientHeight - 1;
  5368. }
  5369. }
  5370. elem.allowUp = elem.scrollTop > 0;
  5371. elem.allowDown = elem.scrollTop < (elem.scrollHeight - elem.clientHeight);
  5372. elem.lastY = event.originalEvent.pageY;
  5373. }
  5374. _stopScrollPropagation(event) {
  5375. let elem = this; // called from event handler context with this as elem
  5376. let up = event.pageY < elem.lastY;
  5377. let down = !up;
  5378. elem.lastY = event.pageY;
  5379. if((up && elem.allowUp) || (down && elem.allowDown)) {
  5380. event.stopPropagation();
  5381. } else {
  5382. event.preventDefault();
  5383. }
  5384. }
  5385. /**
  5386. * Opens the off-canvas menu.
  5387. * @function
  5388. * @param {Object} event - Event object passed from listener.
  5389. * @param {jQuery} trigger - element that triggered the off-canvas to open.
  5390. * @fires Offcanvas#opened
  5391. * @todo also trigger 'open' event?
  5392. */
  5393. open(event, trigger) {
  5394. if (this.$element.hasClass('is-open') || this.isRevealed) { return; }
  5395. var _this = this;
  5396. if (trigger) {
  5397. this.$lastTrigger = trigger;
  5398. }
  5399. if (this.options.forceTo === 'top') {
  5400. window.scrollTo(0, 0);
  5401. } else if (this.options.forceTo === 'bottom') {
  5402. window.scrollTo(0,document.body.scrollHeight);
  5403. }
  5404. if (this.options.transitionTime && this.options.transition !== 'overlap') {
  5405. this.$element.siblings('[data-off-canvas-content]').css('transition-duration', this.options.transitionTime);
  5406. } else {
  5407. this.$element.siblings('[data-off-canvas-content]').css('transition-duration', '');
  5408. }
  5409. this.$element.addClass('is-open').removeClass('is-closed');
  5410. this.$triggers.attr('aria-expanded', 'true');
  5411. this.$element.attr('aria-hidden', 'false');
  5412. this.$content.addClass('is-open-' + this.position);
  5413. // If `contentScroll` is set to false, add class and disable scrolling on touch devices.
  5414. if (this.options.contentScroll === false) {
  5415. $('body').addClass('is-off-canvas-open').on('touchmove', this._stopScrolling);
  5416. this.$element.on('touchstart', this._recordScrollable);
  5417. this.$element.on('touchmove', this._stopScrollPropagation);
  5418. }
  5419. if (this.options.contentOverlay === true) {
  5420. this.$overlay.addClass('is-visible');
  5421. }
  5422. if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
  5423. this.$overlay.addClass('is-closable');
  5424. }
  5425. if (this.options.autoFocus === true) {
  5426. this.$element.one(transitionend(this.$element), function() {
  5427. if (!_this.$element.hasClass('is-open')) {
  5428. return; // exit if prematurely closed
  5429. }
  5430. var canvasFocus = _this.$element.find('[data-autofocus]');
  5431. if (canvasFocus.length) {
  5432. canvasFocus.eq(0).focus();
  5433. } else {
  5434. _this.$element.find('a, button').eq(0).focus();
  5435. }
  5436. });
  5437. }
  5438. if (this.options.trapFocus === true) {
  5439. this.$content.attr('tabindex', '-1');
  5440. Keyboard.trapFocus(this.$element);
  5441. }
  5442. this._addContentClasses();
  5443. /**
  5444. * Fires when the off-canvas menu opens.
  5445. * @event Offcanvas#opened
  5446. */
  5447. this.$element.trigger('opened.zf.offcanvas');
  5448. }
  5449. /**
  5450. * Closes the off-canvas menu.
  5451. * @function
  5452. * @param {Function} cb - optional cb to fire after closure.
  5453. * @fires Offcanvas#closed
  5454. */
  5455. close(cb) {
  5456. if (!this.$element.hasClass('is-open') || this.isRevealed) { return; }
  5457. var _this = this;
  5458. this.$element.removeClass('is-open');
  5459. this.$element.attr('aria-hidden', 'true')
  5460. /**
  5461. * Fires when the off-canvas menu opens.
  5462. * @event Offcanvas#closed
  5463. */
  5464. .trigger('closed.zf.offcanvas');
  5465. this.$content.removeClass('is-open-left is-open-top is-open-right is-open-bottom');
  5466. // If `contentScroll` is set to false, remove class and re-enable scrolling on touch devices.
  5467. if (this.options.contentScroll === false) {
  5468. $('body').removeClass('is-off-canvas-open').off('touchmove', this._stopScrolling);
  5469. this.$element.off('touchstart', this._recordScrollable);
  5470. this.$element.off('touchmove', this._stopScrollPropagation);
  5471. }
  5472. if (this.options.contentOverlay === true) {
  5473. this.$overlay.removeClass('is-visible');
  5474. }
  5475. if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
  5476. this.$overlay.removeClass('is-closable');
  5477. }
  5478. this.$triggers.attr('aria-expanded', 'false');
  5479. if (this.options.trapFocus === true) {
  5480. this.$content.removeAttr('tabindex');
  5481. Keyboard.releaseFocus(this.$element);
  5482. }
  5483. // Listen to transitionEnd and add class when done.
  5484. this.$element.one(transitionend(this.$element), function(e) {
  5485. _this.$element.addClass('is-closed');
  5486. _this._removeContentClasses();
  5487. });
  5488. }
  5489. /**
  5490. * Toggles the off-canvas menu open or closed.
  5491. * @function
  5492. * @param {Object} event - Event object passed from listener.
  5493. * @param {jQuery} trigger - element that triggered the off-canvas to open.
  5494. */
  5495. toggle(event, trigger) {
  5496. if (this.$element.hasClass('is-open')) {
  5497. this.close(event, trigger);
  5498. }
  5499. else {
  5500. this.open(event, trigger);
  5501. }
  5502. }
  5503. /**
  5504. * Handles keyboard input when detected. When the escape key is pressed, the off-canvas menu closes, and focus is restored to the element that opened the menu.
  5505. * @function
  5506. * @private
  5507. */
  5508. _handleKeyboard(e) {
  5509. Keyboard.handleKey(e, 'OffCanvas', {
  5510. close: () => {
  5511. this.close();
  5512. this.$lastTrigger.focus();
  5513. return true;
  5514. },
  5515. handled: () => {
  5516. e.stopPropagation();
  5517. e.preventDefault();
  5518. }
  5519. });
  5520. }
  5521. /**
  5522. * Destroys the offcanvas plugin.
  5523. * @function
  5524. */
  5525. _destroy() {
  5526. this.close();
  5527. this.$element.off('.zf.trigger .zf.offcanvas');
  5528. this.$overlay.off('.zf.offcanvas');
  5529. if (this.onLoadListener) $(window).off(this.onLoadListener);
  5530. }
  5531. }
  5532. OffCanvas.defaults = {
  5533. /**
  5534. * Allow the user to click outside of the menu to close it.
  5535. * @option
  5536. * @type {boolean}
  5537. * @default true
  5538. */
  5539. closeOnClick: true,
  5540. /**
  5541. * Adds an overlay on top of `[data-off-canvas-content]`.
  5542. * @option
  5543. * @type {boolean}
  5544. * @default true
  5545. */
  5546. contentOverlay: true,
  5547. /**
  5548. * Target an off-canvas content container by ID that may be placed anywhere. If null the closest content container will be taken.
  5549. * @option
  5550. * @type {?string}
  5551. * @default null
  5552. */
  5553. contentId: null,
  5554. /**
  5555. * Define the off-canvas element is nested in an off-canvas content. This is required when using the contentId option for a nested element.
  5556. * @option
  5557. * @type {boolean}
  5558. * @default null
  5559. */
  5560. nested: null,
  5561. /**
  5562. * Enable/disable scrolling of the main content when an off canvas panel is open.
  5563. * @option
  5564. * @type {boolean}
  5565. * @default true
  5566. */
  5567. contentScroll: true,
  5568. /**
  5569. * Amount of time in ms the open and close transition requires. If none selected, pulls from body style.
  5570. * @option
  5571. * @type {number}
  5572. * @default null
  5573. */
  5574. transitionTime: null,
  5575. /**
  5576. * Type of transition for the offcanvas menu. Options are 'push', 'detached' or 'slide'.
  5577. * @option
  5578. * @type {string}
  5579. * @default push
  5580. */
  5581. transition: 'push',
  5582. /**
  5583. * Force the page to scroll to top or bottom on open.
  5584. * @option
  5585. * @type {?string}
  5586. * @default null
  5587. */
  5588. forceTo: null,
  5589. /**
  5590. * Allow the offcanvas to remain open for certain breakpoints.
  5591. * @option
  5592. * @type {boolean}
  5593. * @default false
  5594. */
  5595. isRevealed: false,
  5596. /**
  5597. * Breakpoint at which to reveal. JS will use a RegExp to target standard classes, if changing classnames, pass your class with the `revealClass` option.
  5598. * @option
  5599. * @type {?string}
  5600. * @default null
  5601. */
  5602. revealOn: null,
  5603. /**
  5604. * Force focus to the offcanvas on open. If true, will focus the opening trigger on close.
  5605. * @option
  5606. * @type {boolean}
  5607. * @default true
  5608. */
  5609. autoFocus: true,
  5610. /**
  5611. * Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`.
  5612. * @option
  5613. * @type {string}
  5614. * @default reveal-for-
  5615. * @todo improve the regex testing for this.
  5616. */
  5617. revealClass: 'reveal-for-',
  5618. /**
  5619. * Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes.
  5620. * @option
  5621. * @type {boolean}
  5622. * @default false
  5623. */
  5624. trapFocus: false
  5625. };
  5626. /**
  5627. * Orbit module.
  5628. * @module foundation.orbit
  5629. * @requires foundation.util.keyboard
  5630. * @requires foundation.util.motion
  5631. * @requires foundation.util.timer
  5632. * @requires foundation.util.imageLoader
  5633. * @requires foundation.util.touch
  5634. */
  5635. class Orbit extends Plugin {
  5636. /**
  5637. * Creates a new instance of an orbit carousel.
  5638. * @class
  5639. * @name Orbit
  5640. * @param {jQuery} element - jQuery object to make into an Orbit Carousel.
  5641. * @param {Object} options - Overrides to the default plugin settings.
  5642. */
  5643. _setup(element, options){
  5644. this.$element = element;
  5645. this.options = $.extend({}, Orbit.defaults, this.$element.data(), options);
  5646. this.className = 'Orbit'; // ie9 back compat
  5647. Touch.init($); // Touch init is idempotent, we just need to make sure it's initialied.
  5648. this._init();
  5649. Keyboard.register('Orbit', {
  5650. 'ltr': {
  5651. 'ARROW_RIGHT': 'next',
  5652. 'ARROW_LEFT': 'previous'
  5653. },
  5654. 'rtl': {
  5655. 'ARROW_LEFT': 'next',
  5656. 'ARROW_RIGHT': 'previous'
  5657. }
  5658. });
  5659. }
  5660. /**
  5661. * Initializes the plugin by creating jQuery collections, setting attributes, and starting the animation.
  5662. * @function
  5663. * @private
  5664. */
  5665. _init() {
  5666. // @TODO: consider discussion on PR #9278 about DOM pollution by changeSlide
  5667. this._reset();
  5668. this.$wrapper = this.$element.find(`.${this.options.containerClass}`);
  5669. this.$slides = this.$element.find(`.${this.options.slideClass}`);
  5670. var $images = this.$element.find('img'),
  5671. initActive = this.$slides.filter('.is-active'),
  5672. id = this.$element[0].id || GetYoDigits(6, 'orbit');
  5673. this.$element.attr({
  5674. 'data-resize': id,
  5675. 'id': id
  5676. });
  5677. if (!initActive.length) {
  5678. this.$slides.eq(0).addClass('is-active');
  5679. }
  5680. if (!this.options.useMUI) {
  5681. this.$slides.addClass('no-motionui');
  5682. }
  5683. if ($images.length) {
  5684. onImagesLoaded($images, this._prepareForOrbit.bind(this));
  5685. } else {
  5686. this._prepareForOrbit();//hehe
  5687. }
  5688. if (this.options.bullets) {
  5689. this._loadBullets();
  5690. }
  5691. this._events();
  5692. if (this.options.autoPlay && this.$slides.length > 1) {
  5693. this.geoSync();
  5694. }
  5695. if (this.options.accessible) { // allow wrapper to be focusable to enable arrow navigation
  5696. this.$wrapper.attr('tabindex', 0);
  5697. }
  5698. }
  5699. /**
  5700. * Creates a jQuery collection of bullets, if they are being used.
  5701. * @function
  5702. * @private
  5703. */
  5704. _loadBullets() {
  5705. this.$bullets = this.$element.find(`.${this.options.boxOfBullets}`).find('button');
  5706. }
  5707. /**
  5708. * Sets a `timer` object on the orbit, and starts the counter for the next slide.
  5709. * @function
  5710. */
  5711. geoSync() {
  5712. var _this = this;
  5713. this.timer = new Timer(
  5714. this.$element,
  5715. {
  5716. duration: this.options.timerDelay,
  5717. infinite: false
  5718. },
  5719. function() {
  5720. _this.changeSlide(true);
  5721. });
  5722. this.timer.start();
  5723. }
  5724. /**
  5725. * Sets wrapper and slide heights for the orbit.
  5726. * @function
  5727. * @private
  5728. */
  5729. _prepareForOrbit() {
  5730. this._setWrapperHeight();
  5731. }
  5732. /**
  5733. * Calulates the height of each slide in the collection, and uses the tallest one for the wrapper height.
  5734. * @function
  5735. * @private
  5736. * @param {Function} cb - a callback function to fire when complete.
  5737. */
  5738. _setWrapperHeight(cb) {//rewrite this to `for` loop
  5739. var max = 0, temp, counter = 0, _this = this;
  5740. this.$slides.each(function() {
  5741. temp = this.getBoundingClientRect().height;
  5742. $(this).attr('data-slide', counter);
  5743. // hide all slides but the active one
  5744. if (!/mui/g.test($(this)[0].className) && _this.$slides.filter('.is-active')[0] !== _this.$slides.eq(counter)[0]) {
  5745. $(this).css({'display': 'none'});
  5746. }
  5747. max = temp > max ? temp : max;
  5748. counter++;
  5749. });
  5750. if (counter === this.$slides.length) {
  5751. this.$wrapper.css({'height': max}); //only change the wrapper height property once.
  5752. if(cb) {cb(max);} //fire callback with max height dimension.
  5753. }
  5754. }
  5755. /**
  5756. * Sets the max-height of each slide.
  5757. * @function
  5758. * @private
  5759. */
  5760. _setSlideHeight(height) {
  5761. this.$slides.each(function() {
  5762. $(this).css('max-height', height);
  5763. });
  5764. }
  5765. /**
  5766. * Adds event listeners to basically everything within the element.
  5767. * @function
  5768. * @private
  5769. */
  5770. _events() {
  5771. var _this = this;
  5772. //***************************************
  5773. //**Now using custom event - thanks to:**
  5774. //** Yohai Ararat of Toronto **
  5775. //***************************************
  5776. //
  5777. this.$element.off('.resizeme.zf.trigger').on({
  5778. 'resizeme.zf.trigger': this._prepareForOrbit.bind(this)
  5779. });
  5780. if (this.$slides.length > 1) {
  5781. if (this.options.swipe) {
  5782. this.$slides.off('swipeleft.zf.orbit swiperight.zf.orbit')
  5783. .on('swipeleft.zf.orbit', function(e){
  5784. e.preventDefault();
  5785. _this.changeSlide(true);
  5786. }).on('swiperight.zf.orbit', function(e){
  5787. e.preventDefault();
  5788. _this.changeSlide(false);
  5789. });
  5790. }
  5791. //***************************************
  5792. if (this.options.autoPlay) {
  5793. this.$slides.on('click.zf.orbit', function() {
  5794. _this.$element.data('clickedOn', _this.$element.data('clickedOn') ? false : true);
  5795. _this.timer[_this.$element.data('clickedOn') ? 'pause' : 'start']();
  5796. });
  5797. if (this.options.pauseOnHover) {
  5798. this.$element.on('mouseenter.zf.orbit', function() {
  5799. _this.timer.pause();
  5800. }).on('mouseleave.zf.orbit', function() {
  5801. if (!_this.$element.data('clickedOn')) {
  5802. _this.timer.start();
  5803. }
  5804. });
  5805. }
  5806. }
  5807. if (this.options.navButtons) {
  5808. var $controls = this.$element.find(`.${this.options.nextClass}, .${this.options.prevClass}`);
  5809. $controls.attr('tabindex', 0)
  5810. //also need to handle enter/return and spacebar key presses
  5811. .on('click.zf.orbit touchend.zf.orbit', function(e){
  5812. e.preventDefault();
  5813. _this.changeSlide($(this).hasClass(_this.options.nextClass));
  5814. });
  5815. }
  5816. if (this.options.bullets) {
  5817. this.$bullets.on('click.zf.orbit touchend.zf.orbit', function() {
  5818. if (/is-active/g.test(this.className)) { return false; }//if this is active, kick out of function.
  5819. var idx = $(this).data('slide'),
  5820. ltr = idx > _this.$slides.filter('.is-active').data('slide'),
  5821. $slide = _this.$slides.eq(idx);
  5822. _this.changeSlide(ltr, $slide, idx);
  5823. });
  5824. }
  5825. if (this.options.accessible) {
  5826. this.$wrapper.add(this.$bullets).on('keydown.zf.orbit', function(e) {
  5827. // handle keyboard event with keyboard util
  5828. Keyboard.handleKey(e, 'Orbit', {
  5829. next: function() {
  5830. _this.changeSlide(true);
  5831. },
  5832. previous: function() {
  5833. _this.changeSlide(false);
  5834. },
  5835. handled: function() { // if bullet is focused, make sure focus moves
  5836. if ($(e.target).is(_this.$bullets)) {
  5837. _this.$bullets.filter('.is-active').focus();
  5838. }
  5839. }
  5840. });
  5841. });
  5842. }
  5843. }
  5844. }
  5845. /**
  5846. * Resets Orbit so it can be reinitialized
  5847. */
  5848. _reset() {
  5849. // Don't do anything if there are no slides (first run)
  5850. if (typeof this.$slides == 'undefined') {
  5851. return;
  5852. }
  5853. if (this.$slides.length > 1) {
  5854. // Remove old events
  5855. this.$element.off('.zf.orbit').find('*').off('.zf.orbit');
  5856. // Restart timer if autoPlay is enabled
  5857. if (this.options.autoPlay) {
  5858. this.timer.restart();
  5859. }
  5860. // Reset all sliddes
  5861. this.$slides.each(function(el) {
  5862. $(el).removeClass('is-active is-active is-in')
  5863. .removeAttr('aria-live')
  5864. .hide();
  5865. });
  5866. // Show the first slide
  5867. this.$slides.first().addClass('is-active').show();
  5868. // Triggers when the slide has finished animating
  5869. this.$element.trigger('slidechange.zf.orbit', [this.$slides.first()]);
  5870. // Select first bullet if bullets are present
  5871. if (this.options.bullets) {
  5872. this._updateBullets(0);
  5873. }
  5874. }
  5875. }
  5876. /**
  5877. * Changes the current slide to a new one.
  5878. * @function
  5879. * @param {Boolean} isLTR - if true the slide moves from right to left, if false the slide moves from left to right.
  5880. * @param {jQuery} chosenSlide - the jQuery element of the slide to show next, if one is selected.
  5881. * @param {Number} idx - the index of the new slide in its collection, if one chosen.
  5882. * @fires Orbit#slidechange
  5883. */
  5884. changeSlide(isLTR, chosenSlide, idx) {
  5885. if (!this.$slides) {return; } // Don't freak out if we're in the middle of cleanup
  5886. var $curSlide = this.$slides.filter('.is-active').eq(0);
  5887. if (/mui/g.test($curSlide[0].className)) { return false; } //if the slide is currently animating, kick out of the function
  5888. var $firstSlide = this.$slides.first(),
  5889. $lastSlide = this.$slides.last(),
  5890. dirIn = isLTR ? 'Right' : 'Left',
  5891. dirOut = isLTR ? 'Left' : 'Right',
  5892. _this = this,
  5893. $newSlide;
  5894. if (!chosenSlide) { //most of the time, this will be auto played or clicked from the navButtons.
  5895. $newSlide = isLTR ? //if wrapping enabled, check to see if there is a `next` or `prev` sibling, if not, select the first or last slide to fill in. if wrapping not enabled, attempt to select `next` or `prev`, if there's nothing there, the function will kick out on next step. CRAZY NESTED TERNARIES!!!!!
  5896. (this.options.infiniteWrap ? $curSlide.next(`.${this.options.slideClass}`).length ? $curSlide.next(`.${this.options.slideClass}`) : $firstSlide : $curSlide.next(`.${this.options.slideClass}`))//pick next slide if moving left to right
  5897. :
  5898. (this.options.infiniteWrap ? $curSlide.prev(`.${this.options.slideClass}`).length ? $curSlide.prev(`.${this.options.slideClass}`) : $lastSlide : $curSlide.prev(`.${this.options.slideClass}`));//pick prev slide if moving right to left
  5899. } else {
  5900. $newSlide = chosenSlide;
  5901. }
  5902. if ($newSlide.length) {
  5903. /**
  5904. * Triggers before the next slide starts animating in and only if a next slide has been found.
  5905. * @event Orbit#beforeslidechange
  5906. */
  5907. this.$element.trigger('beforeslidechange.zf.orbit', [$curSlide, $newSlide]);
  5908. if (this.options.bullets) {
  5909. idx = idx || this.$slides.index($newSlide); //grab index to update bullets
  5910. this._updateBullets(idx);
  5911. }
  5912. if (this.options.useMUI && !this.$element.is(':hidden')) {
  5913. Motion.animateIn(
  5914. $newSlide.addClass('is-active'),
  5915. this.options[`animInFrom${dirIn}`],
  5916. function(){
  5917. $newSlide.css({'display': 'block'}).attr('aria-live', 'polite');
  5918. });
  5919. Motion.animateOut(
  5920. $curSlide.removeClass('is-active'),
  5921. this.options[`animOutTo${dirOut}`],
  5922. function(){
  5923. $curSlide.removeAttr('aria-live');
  5924. if(_this.options.autoPlay && !_this.timer.isPaused){
  5925. _this.timer.restart();
  5926. }
  5927. //do stuff?
  5928. });
  5929. } else {
  5930. $curSlide.removeClass('is-active is-in').removeAttr('aria-live').hide();
  5931. $newSlide.addClass('is-active is-in').attr('aria-live', 'polite').show();
  5932. if (this.options.autoPlay && !this.timer.isPaused) {
  5933. this.timer.restart();
  5934. }
  5935. }
  5936. /**
  5937. * Triggers when the slide has finished animating in.
  5938. * @event Orbit#slidechange
  5939. */
  5940. this.$element.trigger('slidechange.zf.orbit', [$newSlide]);
  5941. }
  5942. }
  5943. /**
  5944. * Updates the active state of the bullets, if displayed.
  5945. * @function
  5946. * @private
  5947. * @param {Number} idx - the index of the current slide.
  5948. */
  5949. _updateBullets(idx) {
  5950. var $oldBullet = this.$element.find(`.${this.options.boxOfBullets}`)
  5951. .find('.is-active').removeClass('is-active').blur(),
  5952. span = $oldBullet.find('span:last').detach(),
  5953. $newBullet = this.$bullets.eq(idx).addClass('is-active').append(span);
  5954. }
  5955. /**
  5956. * Destroys the carousel and hides the element.
  5957. * @function
  5958. */
  5959. _destroy() {
  5960. this.$element.off('.zf.orbit').find('*').off('.zf.orbit').end().hide();
  5961. }
  5962. }
  5963. Orbit.defaults = {
  5964. /**
  5965. * Tells the JS to look for and loadBullets.
  5966. * @option
  5967. * @type {boolean}
  5968. * @default true
  5969. */
  5970. bullets: true,
  5971. /**
  5972. * Tells the JS to apply event listeners to nav buttons
  5973. * @option
  5974. * @type {boolean}
  5975. * @default true
  5976. */
  5977. navButtons: true,
  5978. /**
  5979. * motion-ui animation class to apply
  5980. * @option
  5981. * @type {string}
  5982. * @default 'slide-in-right'
  5983. */
  5984. animInFromRight: 'slide-in-right',
  5985. /**
  5986. * motion-ui animation class to apply
  5987. * @option
  5988. * @type {string}
  5989. * @default 'slide-out-right'
  5990. */
  5991. animOutToRight: 'slide-out-right',
  5992. /**
  5993. * motion-ui animation class to apply
  5994. * @option
  5995. * @type {string}
  5996. * @default 'slide-in-left'
  5997. *
  5998. */
  5999. animInFromLeft: 'slide-in-left',
  6000. /**
  6001. * motion-ui animation class to apply
  6002. * @option
  6003. * @type {string}
  6004. * @default 'slide-out-left'
  6005. */
  6006. animOutToLeft: 'slide-out-left',
  6007. /**
  6008. * Allows Orbit to automatically animate on page load.
  6009. * @option
  6010. * @type {boolean}
  6011. * @default true
  6012. */
  6013. autoPlay: true,
  6014. /**
  6015. * Amount of time, in ms, between slide transitions
  6016. * @option
  6017. * @type {number}
  6018. * @default 5000
  6019. */
  6020. timerDelay: 5000,
  6021. /**
  6022. * Allows Orbit to infinitely loop through the slides
  6023. * @option
  6024. * @type {boolean}
  6025. * @default true
  6026. */
  6027. infiniteWrap: true,
  6028. /**
  6029. * Allows the Orbit slides to bind to swipe events for mobile, requires an additional util library
  6030. * @option
  6031. * @type {boolean}
  6032. * @default true
  6033. */
  6034. swipe: true,
  6035. /**
  6036. * Allows the timing function to pause animation on hover.
  6037. * @option
  6038. * @type {boolean}
  6039. * @default true
  6040. */
  6041. pauseOnHover: true,
  6042. /**
  6043. * Allows Orbit to bind keyboard events to the slider, to animate frames with arrow keys
  6044. * @option
  6045. * @type {boolean}
  6046. * @default true
  6047. */
  6048. accessible: true,
  6049. /**
  6050. * Class applied to the container of Orbit
  6051. * @option
  6052. * @type {string}
  6053. * @default 'orbit-container'
  6054. */
  6055. containerClass: 'orbit-container',
  6056. /**
  6057. * Class applied to individual slides.
  6058. * @option
  6059. * @type {string}
  6060. * @default 'orbit-slide'
  6061. */
  6062. slideClass: 'orbit-slide',
  6063. /**
  6064. * Class applied to the bullet container. You're welcome.
  6065. * @option
  6066. * @type {string}
  6067. * @default 'orbit-bullets'
  6068. */
  6069. boxOfBullets: 'orbit-bullets',
  6070. /**
  6071. * Class applied to the `next` navigation button.
  6072. * @option
  6073. * @type {string}
  6074. * @default 'orbit-next'
  6075. */
  6076. nextClass: 'orbit-next',
  6077. /**
  6078. * Class applied to the `previous` navigation button.
  6079. * @option
  6080. * @type {string}
  6081. * @default 'orbit-previous'
  6082. */
  6083. prevClass: 'orbit-previous',
  6084. /**
  6085. * Boolean to flag the js to use motion ui classes or not. Default to true for backwards compatibility.
  6086. * @option
  6087. * @type {boolean}
  6088. * @default true
  6089. */
  6090. useMUI: true
  6091. };
  6092. let MenuPlugins = {
  6093. dropdown: {
  6094. cssClass: 'dropdown',
  6095. plugin: DropdownMenu
  6096. },
  6097. drilldown: {
  6098. cssClass: 'drilldown',
  6099. plugin: Drilldown
  6100. },
  6101. accordion: {
  6102. cssClass: 'accordion-menu',
  6103. plugin: AccordionMenu
  6104. }
  6105. };
  6106. // import "foundation.util.triggers.js";
  6107. /**
  6108. * ResponsiveMenu module.
  6109. * @module foundation.responsiveMenu
  6110. * @requires foundation.util.triggers
  6111. * @requires foundation.util.mediaQuery
  6112. */
  6113. class ResponsiveMenu extends Plugin {
  6114. /**
  6115. * Creates a new instance of a responsive menu.
  6116. * @class
  6117. * @name ResponsiveMenu
  6118. * @fires ResponsiveMenu#init
  6119. * @param {jQuery} element - jQuery object to make into a dropdown menu.
  6120. * @param {Object} options - Overrides to the default plugin settings.
  6121. */
  6122. _setup(element, options) {
  6123. this.$element = $(element);
  6124. this.rules = this.$element.data('responsive-menu');
  6125. this.currentMq = null;
  6126. this.currentPlugin = null;
  6127. this.className = 'ResponsiveMenu'; // ie9 back compat
  6128. this._init();
  6129. this._events();
  6130. }
  6131. /**
  6132. * Initializes the Menu by parsing the classes from the 'data-ResponsiveMenu' attribute on the element.
  6133. * @function
  6134. * @private
  6135. */
  6136. _init() {
  6137. MediaQuery._init();
  6138. // The first time an Interchange plugin is initialized, this.rules is converted from a string of "classes" to an object of rules
  6139. if (typeof this.rules === 'string') {
  6140. let rulesTree = {};
  6141. // Parse rules from "classes" pulled from data attribute
  6142. let rules = this.rules.split(' ');
  6143. // Iterate through every rule found
  6144. for (let i = 0; i < rules.length; i++) {
  6145. let rule = rules[i].split('-');
  6146. let ruleSize = rule.length > 1 ? rule[0] : 'small';
  6147. let rulePlugin = rule.length > 1 ? rule[1] : rule[0];
  6148. if (MenuPlugins[rulePlugin] !== null) {
  6149. rulesTree[ruleSize] = MenuPlugins[rulePlugin];
  6150. }
  6151. }
  6152. this.rules = rulesTree;
  6153. }
  6154. if (!$.isEmptyObject(this.rules)) {
  6155. this._checkMediaQueries();
  6156. }
  6157. // Add data-mutate since children may need it.
  6158. this.$element.attr('data-mutate', (this.$element.attr('data-mutate') || GetYoDigits(6, 'responsive-menu')));
  6159. }
  6160. /**
  6161. * Initializes events for the Menu.
  6162. * @function
  6163. * @private
  6164. */
  6165. _events() {
  6166. var _this = this;
  6167. $(window).on('changed.zf.mediaquery', function() {
  6168. _this._checkMediaQueries();
  6169. });
  6170. // $(window).on('resize.zf.ResponsiveMenu', function() {
  6171. // _this._checkMediaQueries();
  6172. // });
  6173. }
  6174. /**
  6175. * Checks the current screen width against available media queries. If the media query has changed, and the plugin needed has changed, the plugins will swap out.
  6176. * @function
  6177. * @private
  6178. */
  6179. _checkMediaQueries() {
  6180. var matchedMq, _this = this;
  6181. // Iterate through each rule and find the last matching rule
  6182. $.each(this.rules, function(key) {
  6183. if (MediaQuery.atLeast(key)) {
  6184. matchedMq = key;
  6185. }
  6186. });
  6187. // No match? No dice
  6188. if (!matchedMq) return;
  6189. // Plugin already initialized? We good
  6190. if (this.currentPlugin instanceof this.rules[matchedMq].plugin) return;
  6191. // Remove existing plugin-specific CSS classes
  6192. $.each(MenuPlugins, function(key, value) {
  6193. _this.$element.removeClass(value.cssClass);
  6194. });
  6195. // Add the CSS class for the new plugin
  6196. this.$element.addClass(this.rules[matchedMq].cssClass);
  6197. // Create an instance of the new plugin
  6198. if (this.currentPlugin) this.currentPlugin.destroy();
  6199. this.currentPlugin = new this.rules[matchedMq].plugin(this.$element, {});
  6200. }
  6201. /**
  6202. * Destroys the instance of the current plugin on this element, as well as the window resize handler that switches the plugins out.
  6203. * @function
  6204. */
  6205. _destroy() {
  6206. this.currentPlugin.destroy();
  6207. $(window).off('.zf.ResponsiveMenu');
  6208. }
  6209. }
  6210. ResponsiveMenu.defaults = {};
  6211. /**
  6212. * ResponsiveToggle module.
  6213. * @module foundation.responsiveToggle
  6214. * @requires foundation.util.mediaQuery
  6215. * @requires foundation.util.motion
  6216. */
  6217. class ResponsiveToggle extends Plugin {
  6218. /**
  6219. * Creates a new instance of Tab Bar.
  6220. * @class
  6221. * @name ResponsiveToggle
  6222. * @fires ResponsiveToggle#init
  6223. * @param {jQuery} element - jQuery object to attach tab bar functionality to.
  6224. * @param {Object} options - Overrides to the default plugin settings.
  6225. */
  6226. _setup(element, options) {
  6227. this.$element = $(element);
  6228. this.options = $.extend({}, ResponsiveToggle.defaults, this.$element.data(), options);
  6229. this.className = 'ResponsiveToggle'; // ie9 back compat
  6230. this._init();
  6231. this._events();
  6232. }
  6233. /**
  6234. * Initializes the tab bar by finding the target element, toggling element, and running update().
  6235. * @function
  6236. * @private
  6237. */
  6238. _init() {
  6239. MediaQuery._init();
  6240. var targetID = this.$element.data('responsive-toggle');
  6241. if (!targetID) {
  6242. console.error('Your tab bar needs an ID of a Menu as the value of data-tab-bar.');
  6243. }
  6244. this.$targetMenu = $(`#${targetID}`);
  6245. this.$toggler = this.$element.find('[data-toggle]').filter(function() {
  6246. var target = $(this).data('toggle');
  6247. return (target === targetID || target === "");
  6248. });
  6249. this.options = $.extend({}, this.options, this.$targetMenu.data());
  6250. // If they were set, parse the animation classes
  6251. if(this.options.animate) {
  6252. let input = this.options.animate.split(' ');
  6253. this.animationIn = input[0];
  6254. this.animationOut = input[1] || null;
  6255. }
  6256. this._update();
  6257. }
  6258. /**
  6259. * Adds necessary event handlers for the tab bar to work.
  6260. * @function
  6261. * @private
  6262. */
  6263. _events() {
  6264. this._updateMqHandler = this._update.bind(this);
  6265. $(window).on('changed.zf.mediaquery', this._updateMqHandler);
  6266. this.$toggler.on('click.zf.responsiveToggle', this.toggleMenu.bind(this));
  6267. }
  6268. /**
  6269. * Checks the current media query to determine if the tab bar should be visible or hidden.
  6270. * @function
  6271. * @private
  6272. */
  6273. _update() {
  6274. // Mobile
  6275. if (!MediaQuery.atLeast(this.options.hideFor)) {
  6276. this.$element.show();
  6277. this.$targetMenu.hide();
  6278. }
  6279. // Desktop
  6280. else {
  6281. this.$element.hide();
  6282. this.$targetMenu.show();
  6283. }
  6284. }
  6285. /**
  6286. * Toggles the element attached to the tab bar. The toggle only happens if the screen is small enough to allow it.
  6287. * @function
  6288. * @fires ResponsiveToggle#toggled
  6289. */
  6290. toggleMenu() {
  6291. if (!MediaQuery.atLeast(this.options.hideFor)) {
  6292. /**
  6293. * Fires when the element attached to the tab bar toggles.
  6294. * @event ResponsiveToggle#toggled
  6295. */
  6296. if(this.options.animate) {
  6297. if (this.$targetMenu.is(':hidden')) {
  6298. Motion.animateIn(this.$targetMenu, this.animationIn, () => {
  6299. this.$element.trigger('toggled.zf.responsiveToggle');
  6300. this.$targetMenu.find('[data-mutate]').triggerHandler('mutateme.zf.trigger');
  6301. });
  6302. }
  6303. else {
  6304. Motion.animateOut(this.$targetMenu, this.animationOut, () => {
  6305. this.$element.trigger('toggled.zf.responsiveToggle');
  6306. });
  6307. }
  6308. }
  6309. else {
  6310. this.$targetMenu.toggle(0);
  6311. this.$targetMenu.find('[data-mutate]').trigger('mutateme.zf.trigger');
  6312. this.$element.trigger('toggled.zf.responsiveToggle');
  6313. }
  6314. }
  6315. };
  6316. _destroy() {
  6317. this.$element.off('.zf.responsiveToggle');
  6318. this.$toggler.off('.zf.responsiveToggle');
  6319. $(window).off('changed.zf.mediaquery', this._updateMqHandler);
  6320. }
  6321. }
  6322. ResponsiveToggle.defaults = {
  6323. /**
  6324. * The breakpoint after which the menu is always shown, and the tab bar is hidden.
  6325. * @option
  6326. * @type {string}
  6327. * @default 'medium'
  6328. */
  6329. hideFor: 'medium',
  6330. /**
  6331. * To decide if the toggle should be animated or not.
  6332. * @option
  6333. * @type {boolean}
  6334. * @default false
  6335. */
  6336. animate: false
  6337. };
  6338. /**
  6339. * Reveal module.
  6340. * @module foundation.reveal
  6341. * @requires foundation.util.keyboard
  6342. * @requires foundation.util.triggers
  6343. * @requires foundation.util.mediaQuery
  6344. * @requires foundation.util.motion if using animations
  6345. */
  6346. class Reveal extends Plugin {
  6347. /**
  6348. * Creates a new instance of Reveal.
  6349. * @class
  6350. * @name Reveal
  6351. * @param {jQuery} element - jQuery object to use for the modal.
  6352. * @param {Object} options - optional parameters.
  6353. */
  6354. _setup(element, options) {
  6355. this.$element = element;
  6356. this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
  6357. this.className = 'Reveal'; // ie9 back compat
  6358. this._init();
  6359. // Triggers init is idempotent, just need to make sure it is initialized
  6360. Triggers.init($);
  6361. Keyboard.register('Reveal', {
  6362. 'ESCAPE': 'close',
  6363. });
  6364. }
  6365. /**
  6366. * Initializes the modal by adding the overlay and close buttons, (if selected).
  6367. * @private
  6368. */
  6369. _init() {
  6370. MediaQuery._init();
  6371. this.id = this.$element.attr('id');
  6372. this.isActive = false;
  6373. this.cached = {mq: MediaQuery.current};
  6374. this.$anchor = $(`[data-open="${this.id}"]`).length ? $(`[data-open="${this.id}"]`) : $(`[data-toggle="${this.id}"]`);
  6375. this.$anchor.attr({
  6376. 'aria-controls': this.id,
  6377. 'aria-haspopup': true,
  6378. 'tabindex': 0
  6379. });
  6380. if (this.options.fullScreen || this.$element.hasClass('full')) {
  6381. this.options.fullScreen = true;
  6382. this.options.overlay = false;
  6383. }
  6384. if (this.options.overlay && !this.$overlay) {
  6385. this.$overlay = this._makeOverlay(this.id);
  6386. }
  6387. this.$element.attr({
  6388. 'role': 'dialog',
  6389. 'aria-hidden': true,
  6390. 'data-yeti-box': this.id,
  6391. 'data-resize': this.id
  6392. });
  6393. if(this.$overlay) {
  6394. this.$element.detach().appendTo(this.$overlay);
  6395. } else {
  6396. this.$element.detach().appendTo($(this.options.appendTo));
  6397. this.$element.addClass('without-overlay');
  6398. }
  6399. this._events();
  6400. if (this.options.deepLink && window.location.hash === ( `#${this.id}`)) {
  6401. this.onLoadListener = onLoad($(window), () => this.open());
  6402. }
  6403. }
  6404. /**
  6405. * Creates an overlay div to display behind the modal.
  6406. * @private
  6407. */
  6408. _makeOverlay() {
  6409. var additionalOverlayClasses = '';
  6410. if (this.options.additionalOverlayClasses) {
  6411. additionalOverlayClasses = ' ' + this.options.additionalOverlayClasses;
  6412. }
  6413. return $('<div></div>')
  6414. .addClass('reveal-overlay' + additionalOverlayClasses)
  6415. .appendTo(this.options.appendTo);
  6416. }
  6417. /**
  6418. * Updates position of modal
  6419. * TODO: Figure out if we actually need to cache these values or if it doesn't matter
  6420. * @private
  6421. */
  6422. _updatePosition() {
  6423. var width = this.$element.outerWidth();
  6424. var outerWidth = $(window).width();
  6425. var height = this.$element.outerHeight();
  6426. var outerHeight = $(window).height();
  6427. var left, top = null;
  6428. if (this.options.hOffset === 'auto') {
  6429. left = parseInt((outerWidth - width) / 2, 10);
  6430. } else {
  6431. left = parseInt(this.options.hOffset, 10);
  6432. }
  6433. if (this.options.vOffset === 'auto') {
  6434. if (height > outerHeight) {
  6435. top = parseInt(Math.min(100, outerHeight / 10), 10);
  6436. } else {
  6437. top = parseInt((outerHeight - height) / 4, 10);
  6438. }
  6439. } else if (this.options.vOffset !== null) {
  6440. top = parseInt(this.options.vOffset, 10);
  6441. }
  6442. if (top !== null) {
  6443. this.$element.css({top: top + 'px'});
  6444. }
  6445. // only worry about left if we don't have an overlay or we have a horizontal offset,
  6446. // otherwise we're perfectly in the middle
  6447. if (!this.$overlay || (this.options.hOffset !== 'auto')) {
  6448. this.$element.css({left: left + 'px'});
  6449. this.$element.css({margin: '0px'});
  6450. }
  6451. }
  6452. /**
  6453. * Adds event handlers for the modal.
  6454. * @private
  6455. */
  6456. _events() {
  6457. var _this = this;
  6458. this.$element.on({
  6459. 'open.zf.trigger': this.open.bind(this),
  6460. 'close.zf.trigger': (event, $element) => {
  6461. if ((event.target === _this.$element[0]) ||
  6462. ($(event.target).parents('[data-closable]')[0] === $element)) { // only close reveal when it's explicitly called
  6463. return this.close.apply(this);
  6464. }
  6465. },
  6466. 'toggle.zf.trigger': this.toggle.bind(this),
  6467. 'resizeme.zf.trigger': function() {
  6468. _this._updatePosition();
  6469. }
  6470. });
  6471. if (this.options.closeOnClick && this.options.overlay) {
  6472. this.$overlay.off('.zf.reveal').on('click.zf.reveal', function(e) {
  6473. if (e.target === _this.$element[0] ||
  6474. $.contains(_this.$element[0], e.target) ||
  6475. !$.contains(document, e.target)) {
  6476. return;
  6477. }
  6478. _this.close();
  6479. });
  6480. }
  6481. if (this.options.deepLink) {
  6482. $(window).on(`hashchange.zf.reveal:${this.id}`, this._handleState.bind(this));
  6483. }
  6484. }
  6485. /**
  6486. * Handles modal methods on back/forward button clicks or any other event that triggers hashchange.
  6487. * @private
  6488. */
  6489. _handleState(e) {
  6490. if(window.location.hash === ( '#' + this.id) && !this.isActive){ this.open(); }
  6491. else{ this.close(); }
  6492. }
  6493. /**
  6494. * Disables the scroll when Reveal is shown to prevent the background from shifting
  6495. * @param {number} scrollTop - Scroll to visually apply, window current scroll by default
  6496. */
  6497. _disableScroll(scrollTop) {
  6498. scrollTop = scrollTop || $(window).scrollTop();
  6499. if ($(document).height() > $(window).height()) {
  6500. $("html")
  6501. .css("top", -scrollTop);
  6502. }
  6503. }
  6504. /**
  6505. * Reenables the scroll when Reveal closes
  6506. * @param {number} scrollTop - Scroll to restore, html "top" property by default (as set by `_disableScroll`)
  6507. */
  6508. _enableScroll(scrollTop) {
  6509. scrollTop = scrollTop || parseInt($("html").css("top"));
  6510. if ($(document).height() > $(window).height()) {
  6511. $("html")
  6512. .css("top", "");
  6513. $(window).scrollTop(-scrollTop);
  6514. }
  6515. }
  6516. /**
  6517. * Opens the modal controlled by `this.$anchor`, and closes all others by default.
  6518. * @function
  6519. * @fires Reveal#closeme
  6520. * @fires Reveal#open
  6521. */
  6522. open() {
  6523. // either update or replace browser history
  6524. const hash = `#${this.id}`;
  6525. if (this.options.deepLink && window.location.hash !== hash) {
  6526. if (window.history.pushState) {
  6527. if (this.options.updateHistory) {
  6528. window.history.pushState({}, '', hash);
  6529. } else {
  6530. window.history.replaceState({}, '', hash);
  6531. }
  6532. } else {
  6533. window.location.hash = hash;
  6534. }
  6535. }
  6536. // Remember anchor that opened it to set focus back later, have general anchors as fallback
  6537. this.$activeAnchor = $(document.activeElement).is(this.$anchor) ? $(document.activeElement) : this.$anchor;
  6538. this.isActive = true;
  6539. // Make elements invisible, but remove display: none so we can get size and positioning
  6540. this.$element
  6541. .css({ 'visibility': 'hidden' })
  6542. .show()
  6543. .scrollTop(0);
  6544. if (this.options.overlay) {
  6545. this.$overlay.css({'visibility': 'hidden'}).show();
  6546. }
  6547. this._updatePosition();
  6548. this.$element
  6549. .hide()
  6550. .css({ 'visibility': '' });
  6551. if(this.$overlay) {
  6552. this.$overlay.css({'visibility': ''}).hide();
  6553. if(this.$element.hasClass('fast')) {
  6554. this.$overlay.addClass('fast');
  6555. } else if (this.$element.hasClass('slow')) {
  6556. this.$overlay.addClass('slow');
  6557. }
  6558. }
  6559. if (!this.options.multipleOpened) {
  6560. /**
  6561. * Fires immediately before the modal opens.
  6562. * Closes any other modals that are currently open
  6563. * @event Reveal#closeme
  6564. */
  6565. this.$element.trigger('closeme.zf.reveal', this.id);
  6566. }
  6567. this._disableScroll();
  6568. var _this = this;
  6569. // Motion UI method of reveal
  6570. if (this.options.animationIn) {
  6571. function afterAnimation(){
  6572. _this.$element
  6573. .attr({
  6574. 'aria-hidden': false,
  6575. 'tabindex': -1
  6576. })
  6577. .focus();
  6578. _this._addGlobalClasses();
  6579. Keyboard.trapFocus(_this.$element);
  6580. }
  6581. if (this.options.overlay) {
  6582. Motion.animateIn(this.$overlay, 'fade-in');
  6583. }
  6584. Motion.animateIn(this.$element, this.options.animationIn, () => {
  6585. if(this.$element) { // protect against object having been removed
  6586. this.focusableElements = Keyboard.findFocusable(this.$element);
  6587. afterAnimation();
  6588. }
  6589. });
  6590. }
  6591. // jQuery method of reveal
  6592. else {
  6593. if (this.options.overlay) {
  6594. this.$overlay.show(0);
  6595. }
  6596. this.$element.show(this.options.showDelay);
  6597. }
  6598. // handle accessibility
  6599. this.$element
  6600. .attr({
  6601. 'aria-hidden': false,
  6602. 'tabindex': -1
  6603. })
  6604. .focus();
  6605. Keyboard.trapFocus(this.$element);
  6606. this._addGlobalClasses();
  6607. this._addGlobalListeners();
  6608. /**
  6609. * Fires when the modal has successfully opened.
  6610. * @event Reveal#open
  6611. */
  6612. this.$element.trigger('open.zf.reveal');
  6613. }
  6614. /**
  6615. * Adds classes and listeners on document required by open modals.
  6616. *
  6617. * The following classes are added and updated:
  6618. * - `.is-reveal-open` - Prevents the scroll on document
  6619. * - `.zf-has-scroll` - Displays a disabled scrollbar on document if required like if the
  6620. * scroll was not disabled. This prevent a "shift" of the page content due
  6621. * the scrollbar disappearing when the modal opens.
  6622. *
  6623. * @private
  6624. */
  6625. _addGlobalClasses() {
  6626. const updateScrollbarClass = () => {
  6627. $('html').toggleClass('zf-has-scroll', !!($(document).height() > $(window).height()));
  6628. };
  6629. this.$element.on('resizeme.zf.trigger.revealScrollbarListener', () => updateScrollbarClass());
  6630. updateScrollbarClass();
  6631. $('html').addClass('is-reveal-open');
  6632. }
  6633. /**
  6634. * Removes classes and listeners on document that were required by open modals.
  6635. * @private
  6636. */
  6637. _removeGlobalClasses() {
  6638. this.$element.off('resizeme.zf.trigger.revealScrollbarListener');
  6639. $('html').removeClass('is-reveal-open');
  6640. $('html').removeClass('zf-has-scroll');
  6641. }
  6642. /**
  6643. * Adds extra event handlers for the body and window if necessary.
  6644. * @private
  6645. */
  6646. _addGlobalListeners() {
  6647. var _this = this;
  6648. if(!this.$element) { return; } // If we're in the middle of cleanup, don't freak out
  6649. this.focusableElements = Keyboard.findFocusable(this.$element);
  6650. if (!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen) {
  6651. $('body').on('click.zf.reveal', function(e) {
  6652. if (e.target === _this.$element[0] ||
  6653. $.contains(_this.$element[0], e.target) ||
  6654. !$.contains(document, e.target)) { return; }
  6655. _this.close();
  6656. });
  6657. }
  6658. if (this.options.closeOnEsc) {
  6659. $(window).on('keydown.zf.reveal', function(e) {
  6660. Keyboard.handleKey(e, 'Reveal', {
  6661. close: function() {
  6662. if (_this.options.closeOnEsc) {
  6663. _this.close();
  6664. }
  6665. }
  6666. });
  6667. });
  6668. }
  6669. }
  6670. /**
  6671. * Closes the modal.
  6672. * @function
  6673. * @fires Reveal#closed
  6674. */
  6675. close() {
  6676. if (!this.isActive || !this.$element.is(':visible')) {
  6677. return false;
  6678. }
  6679. var _this = this;
  6680. // Motion UI method of hiding
  6681. if (this.options.animationOut) {
  6682. if (this.options.overlay) {
  6683. Motion.animateOut(this.$overlay, 'fade-out');
  6684. }
  6685. Motion.animateOut(this.$element, this.options.animationOut, finishUp);
  6686. }
  6687. // jQuery method of hiding
  6688. else {
  6689. this.$element.hide(this.options.hideDelay);
  6690. if (this.options.overlay) {
  6691. this.$overlay.hide(0, finishUp);
  6692. }
  6693. else {
  6694. finishUp();
  6695. }
  6696. }
  6697. // Conditionals to remove extra event listeners added on open
  6698. if (this.options.closeOnEsc) {
  6699. $(window).off('keydown.zf.reveal');
  6700. }
  6701. if (!this.options.overlay && this.options.closeOnClick) {
  6702. $('body').off('click.zf.reveal');
  6703. }
  6704. this.$element.off('keydown.zf.reveal');
  6705. function finishUp() {
  6706. // Get the current top before the modal is closed and restore the scroll after.
  6707. // TODO: use component properties instead of HTML properties
  6708. // See https://github.com/zurb/foundation-sites/pull/10786
  6709. var scrollTop = parseInt($("html").css("top"));
  6710. if ($('.reveal:visible').length === 0) {
  6711. _this._removeGlobalClasses(); // also remove .is-reveal-open from the html element when there is no opened reveal
  6712. }
  6713. Keyboard.releaseFocus(_this.$element);
  6714. _this.$element.attr('aria-hidden', true);
  6715. _this._enableScroll(scrollTop);
  6716. /**
  6717. * Fires when the modal is done closing.
  6718. * @event Reveal#closed
  6719. */
  6720. _this.$element.trigger('closed.zf.reveal');
  6721. }
  6722. /**
  6723. * Resets the modal content
  6724. * This prevents a running video to keep going in the background
  6725. */
  6726. if (this.options.resetOnClose) {
  6727. this.$element.html(this.$element.html());
  6728. }
  6729. this.isActive = false;
  6730. // If deepLink and we did not switched to an other modal...
  6731. if (_this.options.deepLink && window.location.hash === `#${this.id}`) {
  6732. // Remove the history hash
  6733. if (window.history.replaceState) {
  6734. const urlWithoutHash = window.location.pathname + window.location.search;
  6735. if (this.options.updateHistory) {
  6736. window.history.pushState({}, '', urlWithoutHash); // remove the hash
  6737. } else {
  6738. window.history.replaceState('', document.title, urlWithoutHash);
  6739. }
  6740. } else {
  6741. window.location.hash = '';
  6742. }
  6743. }
  6744. this.$activeAnchor.focus();
  6745. }
  6746. /**
  6747. * Toggles the open/closed state of a modal.
  6748. * @function
  6749. */
  6750. toggle() {
  6751. if (this.isActive) {
  6752. this.close();
  6753. } else {
  6754. this.open();
  6755. }
  6756. };
  6757. /**
  6758. * Destroys an instance of a modal.
  6759. * @function
  6760. */
  6761. _destroy() {
  6762. if (this.options.overlay) {
  6763. this.$element.appendTo($(this.options.appendTo)); // move $element outside of $overlay to prevent error unregisterPlugin()
  6764. this.$overlay.hide().off().remove();
  6765. }
  6766. this.$element.hide().off();
  6767. this.$anchor.off('.zf');
  6768. $(window).off(`.zf.reveal:${this.id}`);
  6769. if (this.onLoadListener) $(window).off(this.onLoadListener);
  6770. if ($('.reveal:visible').length === 0) {
  6771. this._removeGlobalClasses(); // also remove .is-reveal-open from the html element when there is no opened reveal
  6772. }
  6773. };
  6774. }
  6775. Reveal.defaults = {
  6776. /**
  6777. * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
  6778. * @option
  6779. * @type {string}
  6780. * @default ''
  6781. */
  6782. animationIn: '',
  6783. /**
  6784. * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
  6785. * @option
  6786. * @type {string}
  6787. * @default ''
  6788. */
  6789. animationOut: '',
  6790. /**
  6791. * Time, in ms, to delay the opening of a modal after a click if no animation used.
  6792. * @option
  6793. * @type {number}
  6794. * @default 0
  6795. */
  6796. showDelay: 0,
  6797. /**
  6798. * Time, in ms, to delay the closing of a modal after a click if no animation used.
  6799. * @option
  6800. * @type {number}
  6801. * @default 0
  6802. */
  6803. hideDelay: 0,
  6804. /**
  6805. * Allows a click on the body/overlay to close the modal.
  6806. * @option
  6807. * @type {boolean}
  6808. * @default true
  6809. */
  6810. closeOnClick: true,
  6811. /**
  6812. * Allows the modal to close if the user presses the `ESCAPE` key.
  6813. * @option
  6814. * @type {boolean}
  6815. * @default true
  6816. */
  6817. closeOnEsc: true,
  6818. /**
  6819. * If true, allows multiple modals to be displayed at once.
  6820. * @option
  6821. * @type {boolean}
  6822. * @default false
  6823. */
  6824. multipleOpened: false,
  6825. /**
  6826. * Distance, in pixels, the modal should push down from the top of the screen.
  6827. * @option
  6828. * @type {number|string}
  6829. * @default auto
  6830. */
  6831. vOffset: 'auto',
  6832. /**
  6833. * Distance, in pixels, the modal should push in from the side of the screen.
  6834. * @option
  6835. * @type {number|string}
  6836. * @default auto
  6837. */
  6838. hOffset: 'auto',
  6839. /**
  6840. * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
  6841. * @option
  6842. * @type {boolean}
  6843. * @default false
  6844. */
  6845. fullScreen: false,
  6846. /**
  6847. * Allows the modal to generate an overlay div, which will cover the view when modal opens.
  6848. * @option
  6849. * @type {boolean}
  6850. * @default true
  6851. */
  6852. overlay: true,
  6853. /**
  6854. * Allows the modal to remove and reinject markup on close. Should be true if using video elements w/o using provider's api, otherwise, videos will continue to play in the background.
  6855. * @option
  6856. * @type {boolean}
  6857. * @default false
  6858. */
  6859. resetOnClose: false,
  6860. /**
  6861. * Link the location hash to the modal.
  6862. * Set the location hash when the modal is opened/closed, and open/close the modal when the location changes.
  6863. * @option
  6864. * @type {boolean}
  6865. * @default false
  6866. */
  6867. deepLink: false,
  6868. /**
  6869. * If `deepLink` is enabled, update the browser history with the open modal
  6870. * @option
  6871. * @default false
  6872. */
  6873. updateHistory: false,
  6874. /**
  6875. * Allows the modal to append to custom div.
  6876. * @option
  6877. * @type {string}
  6878. * @default "body"
  6879. */
  6880. appendTo: "body",
  6881. /**
  6882. * Allows adding additional class names to the reveal overlay.
  6883. * @option
  6884. * @type {string}
  6885. * @default ''
  6886. */
  6887. additionalOverlayClasses: ''
  6888. };
  6889. /**
  6890. * Slider module.
  6891. * @module foundation.slider
  6892. * @requires foundation.util.motion
  6893. * @requires foundation.util.triggers
  6894. * @requires foundation.util.keyboard
  6895. * @requires foundation.util.touch
  6896. */
  6897. class Slider extends Plugin {
  6898. /**
  6899. * Creates a new instance of a slider control.
  6900. * @class
  6901. * @name Slider
  6902. * @param {jQuery} element - jQuery object to make into a slider control.
  6903. * @param {Object} options - Overrides to the default plugin settings.
  6904. */
  6905. _setup(element, options) {
  6906. this.$element = element;
  6907. this.options = $.extend({}, Slider.defaults, this.$element.data(), options);
  6908. this.className = 'Slider'; // ie9 back compat
  6909. // Touch and Triggers inits are idempotent, we just need to make sure it's initialied.
  6910. Touch.init($);
  6911. Triggers.init($);
  6912. this._init();
  6913. Keyboard.register('Slider', {
  6914. 'ltr': {
  6915. 'ARROW_RIGHT': 'increase',
  6916. 'ARROW_UP': 'increase',
  6917. 'ARROW_DOWN': 'decrease',
  6918. 'ARROW_LEFT': 'decrease',
  6919. 'SHIFT_ARROW_RIGHT': 'increase_fast',
  6920. 'SHIFT_ARROW_UP': 'increase_fast',
  6921. 'SHIFT_ARROW_DOWN': 'decrease_fast',
  6922. 'SHIFT_ARROW_LEFT': 'decrease_fast',
  6923. 'HOME': 'min',
  6924. 'END': 'max'
  6925. },
  6926. 'rtl': {
  6927. 'ARROW_LEFT': 'increase',
  6928. 'ARROW_RIGHT': 'decrease',
  6929. 'SHIFT_ARROW_LEFT': 'increase_fast',
  6930. 'SHIFT_ARROW_RIGHT': 'decrease_fast'
  6931. }
  6932. });
  6933. }
  6934. /**
  6935. * Initilizes the plugin by reading/setting attributes, creating collections and setting the initial position of the handle(s).
  6936. * @function
  6937. * @private
  6938. */
  6939. _init() {
  6940. this.inputs = this.$element.find('input');
  6941. this.handles = this.$element.find('[data-slider-handle]');
  6942. this.$handle = this.handles.eq(0);
  6943. this.$input = this.inputs.length ? this.inputs.eq(0) : $(`#${this.$handle.attr('aria-controls')}`);
  6944. this.$fill = this.$element.find('[data-slider-fill]').css(this.options.vertical ? 'height' : 'width', 0);
  6945. if (this.options.disabled || this.$element.hasClass(this.options.disabledClass)) {
  6946. this.options.disabled = true;
  6947. this.$element.addClass(this.options.disabledClass);
  6948. }
  6949. if (!this.inputs.length) {
  6950. this.inputs = $().add(this.$input);
  6951. this.options.binding = true;
  6952. }
  6953. this._setInitAttr(0);
  6954. if (this.handles[1]) {
  6955. this.options.doubleSided = true;
  6956. this.$handle2 = this.handles.eq(1);
  6957. this.$input2 = this.inputs.length > 1 ? this.inputs.eq(1) : $(`#${this.$handle2.attr('aria-controls')}`);
  6958. if (!this.inputs[1]) {
  6959. this.inputs = this.inputs.add(this.$input2);
  6960. }
  6961. // this.$handle.triggerHandler('click.zf.slider');
  6962. this._setInitAttr(1);
  6963. }
  6964. // Set handle positions
  6965. this.setHandles();
  6966. this._events();
  6967. }
  6968. setHandles() {
  6969. if(this.handles[1]) {
  6970. this._setHandlePos(this.$handle, this.inputs.eq(0).val(), true, () => {
  6971. this._setHandlePos(this.$handle2, this.inputs.eq(1).val(), true);
  6972. });
  6973. } else {
  6974. this._setHandlePos(this.$handle, this.inputs.eq(0).val(), true);
  6975. }
  6976. }
  6977. _reflow() {
  6978. this.setHandles();
  6979. }
  6980. /**
  6981. * @function
  6982. * @private
  6983. * @param {Number} value - floating point (the value) to be transformed using to a relative position on the slider (the inverse of _value)
  6984. */
  6985. _pctOfBar(value) {
  6986. var pctOfBar = percent(value - this.options.start, this.options.end - this.options.start);
  6987. switch(this.options.positionValueFunction) {
  6988. case "pow":
  6989. pctOfBar = this._logTransform(pctOfBar);
  6990. break;
  6991. case "log":
  6992. pctOfBar = this._powTransform(pctOfBar);
  6993. break;
  6994. }
  6995. return pctOfBar.toFixed(2)
  6996. }
  6997. /**
  6998. * @function
  6999. * @private
  7000. * @param {Number} pctOfBar - floating point, the relative position of the slider (typically between 0-1) to be transformed to a value
  7001. */
  7002. _value(pctOfBar) {
  7003. switch(this.options.positionValueFunction) {
  7004. case "pow":
  7005. pctOfBar = this._powTransform(pctOfBar);
  7006. break;
  7007. case "log":
  7008. pctOfBar = this._logTransform(pctOfBar);
  7009. break;
  7010. }
  7011. var value = (this.options.end - this.options.start) * pctOfBar + parseFloat(this.options.start);
  7012. return value
  7013. }
  7014. /**
  7015. * @function
  7016. * @private
  7017. * @param {Number} value - floating point (typically between 0-1) to be transformed using the log function
  7018. */
  7019. _logTransform(value) {
  7020. return baseLog(this.options.nonLinearBase, ((value*(this.options.nonLinearBase-1))+1))
  7021. }
  7022. /**
  7023. * @function
  7024. * @private
  7025. * @param {Number} value - floating point (typically between 0-1) to be transformed using the power function
  7026. */
  7027. _powTransform(value) {
  7028. return (Math.pow(this.options.nonLinearBase, value) - 1) / (this.options.nonLinearBase - 1)
  7029. }
  7030. /**
  7031. * Sets the position of the selected handle and fill bar.
  7032. * @function
  7033. * @private
  7034. * @param {jQuery} $hndl - the selected handle to move.
  7035. * @param {Number} location - floating point between the start and end values of the slider bar.
  7036. * @param {Function} cb - callback function to fire on completion.
  7037. * @fires Slider#moved
  7038. * @fires Slider#changed
  7039. */
  7040. _setHandlePos($hndl, location, noInvert, cb) {
  7041. // don't move if the slider has been disabled since its initialization
  7042. if (this.$element.hasClass(this.options.disabledClass)) {
  7043. return;
  7044. }
  7045. //might need to alter that slightly for bars that will have odd number selections.
  7046. location = parseFloat(location);//on input change events, convert string to number...grumble.
  7047. // prevent slider from running out of bounds, if value exceeds the limits set through options, override the value to min/max
  7048. if (location < this.options.start) { location = this.options.start; }
  7049. else if (location > this.options.end) { location = this.options.end; }
  7050. var isDbl = this.options.doubleSided;
  7051. //this is for single-handled vertical sliders, it adjusts the value to account for the slider being "upside-down"
  7052. //for click and drag events, it's weird due to the scale(-1, 1) css property
  7053. if (this.options.vertical && !noInvert) {
  7054. location = this.options.end - location;
  7055. }
  7056. if (isDbl) { //this block is to prevent 2 handles from crossing eachother. Could/should be improved.
  7057. if (this.handles.index($hndl) === 0) {
  7058. var h2Val = parseFloat(this.$handle2.attr('aria-valuenow'));
  7059. location = location >= h2Val ? h2Val - this.options.step : location;
  7060. } else {
  7061. var h1Val = parseFloat(this.$handle.attr('aria-valuenow'));
  7062. location = location <= h1Val ? h1Val + this.options.step : location;
  7063. }
  7064. }
  7065. var _this = this,
  7066. vert = this.options.vertical,
  7067. hOrW = vert ? 'height' : 'width',
  7068. lOrT = vert ? 'top' : 'left',
  7069. handleDim = $hndl[0].getBoundingClientRect()[hOrW],
  7070. elemDim = this.$element[0].getBoundingClientRect()[hOrW],
  7071. //percentage of bar min/max value based on click or drag point
  7072. pctOfBar = this._pctOfBar(location),
  7073. //number of actual pixels to shift the handle, based on the percentage obtained above
  7074. pxToMove = (elemDim - handleDim) * pctOfBar,
  7075. //percentage of bar to shift the handle
  7076. movement = (percent(pxToMove, elemDim) * 100).toFixed(this.options.decimal);
  7077. //fixing the decimal value for the location number, is passed to other methods as a fixed floating-point value
  7078. location = parseFloat(location.toFixed(this.options.decimal));
  7079. // declare empty object for css adjustments, only used with 2 handled-sliders
  7080. var css = {};
  7081. this._setValues($hndl, location);
  7082. // TODO update to calculate based on values set to respective inputs??
  7083. if (isDbl) {
  7084. var isLeftHndl = this.handles.index($hndl) === 0,
  7085. //empty variable, will be used for min-height/width for fill bar
  7086. dim,
  7087. //percentage w/h of the handle compared to the slider bar
  7088. handlePct = ~~(percent(handleDim, elemDim) * 100);
  7089. //if left handle, the math is slightly different than if it's the right handle, and the left/top property needs to be changed for the fill bar
  7090. if (isLeftHndl) {
  7091. //left or top percentage value to apply to the fill bar.
  7092. css[lOrT] = `${movement}%`;
  7093. //calculate the new min-height/width for the fill bar.
  7094. dim = parseFloat(this.$handle2[0].style[lOrT]) - movement + handlePct;
  7095. //this callback is necessary to prevent errors and allow the proper placement and initialization of a 2-handled slider
  7096. //plus, it means we don't care if 'dim' isNaN on init, it won't be in the future.
  7097. if (cb && typeof cb === 'function') { cb(); }//this is only needed for the initialization of 2 handled sliders
  7098. } else {
  7099. //just caching the value of the left/bottom handle's left/top property
  7100. var handlePos = parseFloat(this.$handle[0].style[lOrT]);
  7101. //calculate the new min-height/width for the fill bar. Use isNaN to prevent false positives for numbers <= 0
  7102. //based on the percentage of movement of the handle being manipulated, less the opposing handle's left/top position, plus the percentage w/h of the handle itself
  7103. dim = movement - (isNaN(handlePos) ? (this.options.initialStart - this.options.start)/((this.options.end-this.options.start)/100) : handlePos) + handlePct;
  7104. }
  7105. // assign the min-height/width to our css object
  7106. css[`min-${hOrW}`] = `${dim}%`;
  7107. }
  7108. this.$element.one('finished.zf.animate', function() {
  7109. /**
  7110. * Fires when the handle is done moving.
  7111. * @event Slider#moved
  7112. */
  7113. _this.$element.trigger('moved.zf.slider', [$hndl]);
  7114. });
  7115. //because we don't know exactly how the handle will be moved, check the amount of time it should take to move.
  7116. var moveTime = this.$element.data('dragging') ? 1000/60 : this.options.moveTime;
  7117. Move(moveTime, $hndl, function() {
  7118. // adjusting the left/top property of the handle, based on the percentage calculated above
  7119. // if movement isNaN, that is because the slider is hidden and we cannot determine handle width,
  7120. // fall back to next best guess.
  7121. if (isNaN(movement)) {
  7122. $hndl.css(lOrT, `${pctOfBar * 100}%`);
  7123. }
  7124. else {
  7125. $hndl.css(lOrT, `${movement}%`);
  7126. }
  7127. if (!_this.options.doubleSided) {
  7128. //if single-handled, a simple method to expand the fill bar
  7129. _this.$fill.css(hOrW, `${pctOfBar * 100}%`);
  7130. } else {
  7131. //otherwise, use the css object we created above
  7132. _this.$fill.css(css);
  7133. }
  7134. });
  7135. /**
  7136. * Fires when the value has not been change for a given time.
  7137. * @event Slider#changed
  7138. */
  7139. clearTimeout(_this.timeout);
  7140. _this.timeout = setTimeout(function(){
  7141. _this.$element.trigger('changed.zf.slider', [$hndl]);
  7142. }, _this.options.changedDelay);
  7143. }
  7144. /**
  7145. * Sets the initial attribute for the slider element.
  7146. * @function
  7147. * @private
  7148. * @param {Number} idx - index of the current handle/input to use.
  7149. */
  7150. _setInitAttr(idx) {
  7151. var initVal = (idx === 0 ? this.options.initialStart : this.options.initialEnd);
  7152. var id = this.inputs.eq(idx).attr('id') || GetYoDigits(6, 'slider');
  7153. this.inputs.eq(idx).attr({
  7154. 'id': id,
  7155. 'max': this.options.end,
  7156. 'min': this.options.start,
  7157. 'step': this.options.step
  7158. });
  7159. this.inputs.eq(idx).val(initVal);
  7160. this.handles.eq(idx).attr({
  7161. 'role': 'slider',
  7162. 'aria-controls': id,
  7163. 'aria-valuemax': this.options.end,
  7164. 'aria-valuemin': this.options.start,
  7165. 'aria-valuenow': initVal,
  7166. 'aria-orientation': this.options.vertical ? 'vertical' : 'horizontal',
  7167. 'tabindex': 0
  7168. });
  7169. }
  7170. /**
  7171. * Sets the input and `aria-valuenow` values for the slider element.
  7172. * @function
  7173. * @private
  7174. * @param {jQuery} $handle - the currently selected handle.
  7175. * @param {Number} val - floating point of the new value.
  7176. */
  7177. _setValues($handle, val) {
  7178. var idx = this.options.doubleSided ? this.handles.index($handle) : 0;
  7179. this.inputs.eq(idx).val(val);
  7180. $handle.attr('aria-valuenow', val);
  7181. }
  7182. /**
  7183. * Handles events on the slider element.
  7184. * Calculates the new location of the current handle.
  7185. * If there are two handles and the bar was clicked, it determines which handle to move.
  7186. * @function
  7187. * @private
  7188. * @param {Object} e - the `event` object passed from the listener.
  7189. * @param {jQuery} $handle - the current handle to calculate for, if selected.
  7190. * @param {Number} val - floating point number for the new value of the slider.
  7191. * TODO clean this up, there's a lot of repeated code between this and the _setHandlePos fn.
  7192. */
  7193. _handleEvent(e, $handle, val) {
  7194. var value, hasVal;
  7195. if (!val) {//click or drag events
  7196. e.preventDefault();
  7197. var _this = this,
  7198. vertical = this.options.vertical,
  7199. param = vertical ? 'height' : 'width',
  7200. direction = vertical ? 'top' : 'left',
  7201. eventOffset = vertical ? e.pageY : e.pageX,
  7202. halfOfHandle = this.$handle[0].getBoundingClientRect()[param] / 2,
  7203. barDim = this.$element[0].getBoundingClientRect()[param],
  7204. windowScroll = vertical ? $(window).scrollTop() : $(window).scrollLeft();
  7205. var elemOffset = this.$element.offset()[direction];
  7206. // touch events emulated by the touch util give position relative to screen, add window.scroll to event coordinates...
  7207. // best way to guess this is simulated is if clientY == pageY
  7208. if (e.clientY === e.pageY) { eventOffset = eventOffset + windowScroll; }
  7209. var eventFromBar = eventOffset - elemOffset;
  7210. var barXY;
  7211. if (eventFromBar < 0) {
  7212. barXY = 0;
  7213. } else if (eventFromBar > barDim) {
  7214. barXY = barDim;
  7215. } else {
  7216. barXY = eventFromBar;
  7217. }
  7218. var offsetPct = percent(barXY, barDim);
  7219. value = this._value(offsetPct);
  7220. // turn everything around for RTL, yay math!
  7221. if (rtl() && !this.options.vertical) {value = this.options.end - value;}
  7222. value = _this._adjustValue(null, value);
  7223. //boolean flag for the setHandlePos fn, specifically for vertical sliders
  7224. hasVal = false;
  7225. if (!$handle) {//figure out which handle it is, pass it to the next function.
  7226. var firstHndlPos = absPosition(this.$handle, direction, barXY, param),
  7227. secndHndlPos = absPosition(this.$handle2, direction, barXY, param);
  7228. $handle = firstHndlPos <= secndHndlPos ? this.$handle : this.$handle2;
  7229. }
  7230. } else {//change event on input
  7231. value = this._adjustValue(null, val);
  7232. hasVal = true;
  7233. }
  7234. this._setHandlePos($handle, value, hasVal);
  7235. }
  7236. /**
  7237. * Adjustes value for handle in regard to step value. returns adjusted value
  7238. * @function
  7239. * @private
  7240. * @param {jQuery} $handle - the selected handle.
  7241. * @param {Number} value - value to adjust. used if $handle is falsy
  7242. */
  7243. _adjustValue($handle, value) {
  7244. var val,
  7245. step = this.options.step,
  7246. div = parseFloat(step/2),
  7247. left, prev_val, next_val;
  7248. if (!!$handle) {
  7249. val = parseFloat($handle.attr('aria-valuenow'));
  7250. }
  7251. else {
  7252. val = value;
  7253. }
  7254. if (val >= 0) {
  7255. left = val % step;
  7256. } else {
  7257. left = step + (val % step);
  7258. }
  7259. prev_val = val - left;
  7260. next_val = prev_val + step;
  7261. if (left === 0) {
  7262. return val;
  7263. }
  7264. val = val >= prev_val + div ? next_val : prev_val;
  7265. return val;
  7266. }
  7267. /**
  7268. * Adds event listeners to the slider elements.
  7269. * @function
  7270. * @private
  7271. */
  7272. _events() {
  7273. this._eventsForHandle(this.$handle);
  7274. if(this.handles[1]) {
  7275. this._eventsForHandle(this.$handle2);
  7276. }
  7277. }
  7278. /**
  7279. * Adds event listeners a particular handle
  7280. * @function
  7281. * @private
  7282. * @param {jQuery} $handle - the current handle to apply listeners to.
  7283. */
  7284. _eventsForHandle($handle) {
  7285. var _this = this,
  7286. curHandle;
  7287. const handleChangeEvent = function(e) {
  7288. const idx = _this.inputs.index($(this));
  7289. _this._handleEvent(e, _this.handles.eq(idx), $(this).val());
  7290. };
  7291. // IE only triggers the change event when the input loses focus which strictly follows the HTML specification
  7292. // listen for the enter key and trigger a change
  7293. // @see https://html.spec.whatwg.org/multipage/input.html#common-input-element-events
  7294. this.inputs.off('keyup.zf.slider').on('keyup.zf.slider', function (e) {
  7295. if(e.keyCode == 13) handleChangeEvent.call(this, e);
  7296. });
  7297. this.inputs.off('change.zf.slider').on('change.zf.slider', handleChangeEvent);
  7298. if (this.options.clickSelect) {
  7299. this.$element.off('click.zf.slider').on('click.zf.slider', function(e) {
  7300. if (_this.$element.data('dragging')) { return false; }
  7301. if (!$(e.target).is('[data-slider-handle]')) {
  7302. if (_this.options.doubleSided) {
  7303. _this._handleEvent(e);
  7304. } else {
  7305. _this._handleEvent(e, _this.$handle);
  7306. }
  7307. }
  7308. });
  7309. }
  7310. if (this.options.draggable) {
  7311. this.handles.addTouch();
  7312. var $body = $('body');
  7313. $handle
  7314. .off('mousedown.zf.slider')
  7315. .on('mousedown.zf.slider', function(e) {
  7316. $handle.addClass('is-dragging');
  7317. _this.$fill.addClass('is-dragging');//
  7318. _this.$element.data('dragging', true);
  7319. curHandle = $(e.currentTarget);
  7320. $body.on('mousemove.zf.slider', function(e) {
  7321. e.preventDefault();
  7322. _this._handleEvent(e, curHandle);
  7323. }).on('mouseup.zf.slider', function(e) {
  7324. _this._handleEvent(e, curHandle);
  7325. $handle.removeClass('is-dragging');
  7326. _this.$fill.removeClass('is-dragging');
  7327. _this.$element.data('dragging', false);
  7328. $body.off('mousemove.zf.slider mouseup.zf.slider');
  7329. });
  7330. })
  7331. // prevent events triggered by touch
  7332. .on('selectstart.zf.slider touchmove.zf.slider', function(e) {
  7333. e.preventDefault();
  7334. });
  7335. }
  7336. $handle.off('keydown.zf.slider').on('keydown.zf.slider', function(e) {
  7337. var _$handle = $(this),
  7338. idx = _this.options.doubleSided ? _this.handles.index(_$handle) : 0,
  7339. oldValue = parseFloat(_this.inputs.eq(idx).val()),
  7340. newValue;
  7341. // handle keyboard event with keyboard util
  7342. Keyboard.handleKey(e, 'Slider', {
  7343. decrease: function() {
  7344. newValue = oldValue - _this.options.step;
  7345. },
  7346. increase: function() {
  7347. newValue = oldValue + _this.options.step;
  7348. },
  7349. decrease_fast: function() {
  7350. newValue = oldValue - _this.options.step * 10;
  7351. },
  7352. increase_fast: function() {
  7353. newValue = oldValue + _this.options.step * 10;
  7354. },
  7355. min: function() {
  7356. newValue = _this.options.start;
  7357. },
  7358. max: function() {
  7359. newValue = _this.options.end;
  7360. },
  7361. handled: function() { // only set handle pos when event was handled specially
  7362. e.preventDefault();
  7363. _this._setHandlePos(_$handle, newValue, true);
  7364. }
  7365. });
  7366. /*if (newValue) { // if pressed key has special function, update value
  7367. e.preventDefault();
  7368. _this._setHandlePos(_$handle, newValue);
  7369. }*/
  7370. });
  7371. }
  7372. /**
  7373. * Destroys the slider plugin.
  7374. */
  7375. _destroy() {
  7376. this.handles.off('.zf.slider');
  7377. this.inputs.off('.zf.slider');
  7378. this.$element.off('.zf.slider');
  7379. clearTimeout(this.timeout);
  7380. }
  7381. }
  7382. Slider.defaults = {
  7383. /**
  7384. * Minimum value for the slider scale.
  7385. * @option
  7386. * @type {number}
  7387. * @default 0
  7388. */
  7389. start: 0,
  7390. /**
  7391. * Maximum value for the slider scale.
  7392. * @option
  7393. * @type {number}
  7394. * @default 100
  7395. */
  7396. end: 100,
  7397. /**
  7398. * Minimum value change per change event.
  7399. * @option
  7400. * @type {number}
  7401. * @default 1
  7402. */
  7403. step: 1,
  7404. /**
  7405. * Value at which the handle/input *(left handle/first input)* should be set to on initialization.
  7406. * @option
  7407. * @type {number}
  7408. * @default 0
  7409. */
  7410. initialStart: 0,
  7411. /**
  7412. * Value at which the right handle/second input should be set to on initialization.
  7413. * @option
  7414. * @type {number}
  7415. * @default 100
  7416. */
  7417. initialEnd: 100,
  7418. /**
  7419. * Allows the input to be located outside the container and visible. Set to by the JS
  7420. * @option
  7421. * @type {boolean}
  7422. * @default false
  7423. */
  7424. binding: false,
  7425. /**
  7426. * Allows the user to click/tap on the slider bar to select a value.
  7427. * @option
  7428. * @type {boolean}
  7429. * @default true
  7430. */
  7431. clickSelect: true,
  7432. /**
  7433. * Set to true and use the `vertical` class to change alignment to vertical.
  7434. * @option
  7435. * @type {boolean}
  7436. * @default false
  7437. */
  7438. vertical: false,
  7439. /**
  7440. * Allows the user to drag the slider handle(s) to select a value.
  7441. * @option
  7442. * @type {boolean}
  7443. * @default true
  7444. */
  7445. draggable: true,
  7446. /**
  7447. * Disables the slider and prevents event listeners from being applied. Double checked by JS with `disabledClass`.
  7448. * @option
  7449. * @type {boolean}
  7450. * @default false
  7451. */
  7452. disabled: false,
  7453. /**
  7454. * Allows the use of two handles. Double checked by the JS. Changes some logic handling.
  7455. * @option
  7456. * @type {boolean}
  7457. * @default false
  7458. */
  7459. doubleSided: false,
  7460. /**
  7461. * Potential future feature.
  7462. */
  7463. // steps: 100,
  7464. /**
  7465. * Number of decimal places the plugin should go to for floating point precision.
  7466. * @option
  7467. * @type {number}
  7468. * @default 2
  7469. */
  7470. decimal: 2,
  7471. /**
  7472. * Time delay for dragged elements.
  7473. */
  7474. // dragDelay: 0,
  7475. /**
  7476. * Time, in ms, to animate the movement of a slider handle if user clicks/taps on the bar. Needs to be manually set if updating the transition time in the Sass settings.
  7477. * @option
  7478. * @type {number}
  7479. * @default 200
  7480. */
  7481. moveTime: 200,//update this if changing the transition time in the sass
  7482. /**
  7483. * Class applied to disabled sliders.
  7484. * @option
  7485. * @type {string}
  7486. * @default 'disabled'
  7487. */
  7488. disabledClass: 'disabled',
  7489. /**
  7490. * Will invert the default layout for a vertical<span data-tooltip title="who would do this???"> </span>slider.
  7491. * @option
  7492. * @type {boolean}
  7493. * @default false
  7494. */
  7495. invertVertical: false,
  7496. /**
  7497. * Milliseconds before the `changed.zf-slider` event is triggered after value change.
  7498. * @option
  7499. * @type {number}
  7500. * @default 500
  7501. */
  7502. changedDelay: 500,
  7503. /**
  7504. * Basevalue for non-linear sliders
  7505. * @option
  7506. * @type {number}
  7507. * @default 5
  7508. */
  7509. nonLinearBase: 5,
  7510. /**
  7511. * Basevalue for non-linear sliders, possible values are: `'linear'`, `'pow'` & `'log'`. Pow and Log use the nonLinearBase setting.
  7512. * @option
  7513. * @type {string}
  7514. * @default 'linear'
  7515. */
  7516. positionValueFunction: 'linear',
  7517. };
  7518. function percent(frac, num) {
  7519. return (frac / num);
  7520. }
  7521. function absPosition($handle, dir, clickPos, param) {
  7522. return Math.abs(($handle.position()[dir] + ($handle[param]() / 2)) - clickPos);
  7523. }
  7524. function baseLog(base, value) {
  7525. return Math.log(value)/Math.log(base)
  7526. }
  7527. /**
  7528. * Sticky module.
  7529. * @module foundation.sticky
  7530. * @requires foundation.util.triggers
  7531. * @requires foundation.util.mediaQuery
  7532. */
  7533. class Sticky extends Plugin {
  7534. /**
  7535. * Creates a new instance of a sticky thing.
  7536. * @class
  7537. * @name Sticky
  7538. * @param {jQuery} element - jQuery object to make sticky.
  7539. * @param {Object} options - options object passed when creating the element programmatically.
  7540. */
  7541. _setup(element, options) {
  7542. this.$element = element;
  7543. this.options = $.extend({}, Sticky.defaults, this.$element.data(), options);
  7544. this.className = 'Sticky'; // ie9 back compat
  7545. // Triggers init is idempotent, just need to make sure it is initialized
  7546. Triggers.init($);
  7547. this._init();
  7548. }
  7549. /**
  7550. * Initializes the sticky element by adding classes, getting/setting dimensions, breakpoints and attributes
  7551. * @function
  7552. * @private
  7553. */
  7554. _init() {
  7555. MediaQuery._init();
  7556. var $parent = this.$element.parent('[data-sticky-container]'),
  7557. id = this.$element[0].id || GetYoDigits(6, 'sticky'),
  7558. _this = this;
  7559. if($parent.length){
  7560. this.$container = $parent;
  7561. } else {
  7562. this.wasWrapped = true;
  7563. this.$element.wrap(this.options.container);
  7564. this.$container = this.$element.parent();
  7565. }
  7566. this.$container.addClass(this.options.containerClass);
  7567. this.$element.addClass(this.options.stickyClass).attr({ 'data-resize': id, 'data-mutate': id });
  7568. if (this.options.anchor !== '') {
  7569. $('#' + _this.options.anchor).attr({ 'data-mutate': id });
  7570. }
  7571. this.scrollCount = this.options.checkEvery;
  7572. this.isStuck = false;
  7573. this.onLoadListener = onLoad($(window), function () {
  7574. //We calculate the container height to have correct values for anchor points offset calculation.
  7575. _this.containerHeight = _this.$element.css("display") == "none" ? 0 : _this.$element[0].getBoundingClientRect().height;
  7576. _this.$container.css('height', _this.containerHeight);
  7577. _this.elemHeight = _this.containerHeight;
  7578. if (_this.options.anchor !== '') {
  7579. _this.$anchor = $('#' + _this.options.anchor);
  7580. } else {
  7581. _this._parsePoints();
  7582. }
  7583. _this._setSizes(function () {
  7584. var scroll = window.pageYOffset;
  7585. _this._calc(false, scroll);
  7586. //Unstick the element will ensure that proper classes are set.
  7587. if (!_this.isStuck) {
  7588. _this._removeSticky((scroll >= _this.topPoint) ? false : true);
  7589. }
  7590. });
  7591. _this._events(id.split('-').reverse().join('-'));
  7592. });
  7593. }
  7594. /**
  7595. * If using multiple elements as anchors, calculates the top and bottom pixel values the sticky thing should stick and unstick on.
  7596. * @function
  7597. * @private
  7598. */
  7599. _parsePoints() {
  7600. var top = this.options.topAnchor == "" ? 1 : this.options.topAnchor,
  7601. btm = this.options.btmAnchor== "" ? document.documentElement.scrollHeight : this.options.btmAnchor,
  7602. pts = [top, btm],
  7603. breaks = {};
  7604. for (var i = 0, len = pts.length; i < len && pts[i]; i++) {
  7605. var pt;
  7606. if (typeof pts[i] === 'number') {
  7607. pt = pts[i];
  7608. } else {
  7609. var place = pts[i].split(':'),
  7610. anchor = $(`#${place[0]}`);
  7611. pt = anchor.offset().top;
  7612. if (place[1] && place[1].toLowerCase() === 'bottom') {
  7613. pt += anchor[0].getBoundingClientRect().height;
  7614. }
  7615. }
  7616. breaks[i] = pt;
  7617. }
  7618. this.points = breaks;
  7619. return;
  7620. }
  7621. /**
  7622. * Adds event handlers for the scrolling element.
  7623. * @private
  7624. * @param {String} id - pseudo-random id for unique scroll event listener.
  7625. */
  7626. _events(id) {
  7627. var _this = this,
  7628. scrollListener = this.scrollListener = `scroll.zf.${id}`;
  7629. if (this.isOn) { return; }
  7630. if (this.canStick) {
  7631. this.isOn = true;
  7632. $(window).off(scrollListener)
  7633. .on(scrollListener, function(e) {
  7634. if (_this.scrollCount === 0) {
  7635. _this.scrollCount = _this.options.checkEvery;
  7636. _this._setSizes(function() {
  7637. _this._calc(false, window.pageYOffset);
  7638. });
  7639. } else {
  7640. _this.scrollCount--;
  7641. _this._calc(false, window.pageYOffset);
  7642. }
  7643. });
  7644. }
  7645. this.$element.off('resizeme.zf.trigger')
  7646. .on('resizeme.zf.trigger', function(e, el) {
  7647. _this._eventsHandler(id);
  7648. });
  7649. this.$element.on('mutateme.zf.trigger', function (e, el) {
  7650. _this._eventsHandler(id);
  7651. });
  7652. if(this.$anchor) {
  7653. this.$anchor.on('mutateme.zf.trigger', function (e, el) {
  7654. _this._eventsHandler(id);
  7655. });
  7656. }
  7657. }
  7658. /**
  7659. * Handler for events.
  7660. * @private
  7661. * @param {String} id - pseudo-random id for unique scroll event listener.
  7662. */
  7663. _eventsHandler(id) {
  7664. var _this = this,
  7665. scrollListener = this.scrollListener = `scroll.zf.${id}`;
  7666. _this._setSizes(function() {
  7667. _this._calc(false);
  7668. if (_this.canStick) {
  7669. if (!_this.isOn) {
  7670. _this._events(id);
  7671. }
  7672. } else if (_this.isOn) {
  7673. _this._pauseListeners(scrollListener);
  7674. }
  7675. });
  7676. }
  7677. /**
  7678. * Removes event handlers for scroll and change events on anchor.
  7679. * @fires Sticky#pause
  7680. * @param {String} scrollListener - unique, namespaced scroll listener attached to `window`
  7681. */
  7682. _pauseListeners(scrollListener) {
  7683. this.isOn = false;
  7684. $(window).off(scrollListener);
  7685. /**
  7686. * Fires when the plugin is paused due to resize event shrinking the view.
  7687. * @event Sticky#pause
  7688. * @private
  7689. */
  7690. this.$element.trigger('pause.zf.sticky');
  7691. }
  7692. /**
  7693. * Called on every `scroll` event and on `_init`
  7694. * fires functions based on booleans and cached values
  7695. * @param {Boolean} checkSizes - true if plugin should recalculate sizes and breakpoints.
  7696. * @param {Number} scroll - current scroll position passed from scroll event cb function. If not passed, defaults to `window.pageYOffset`.
  7697. */
  7698. _calc(checkSizes, scroll) {
  7699. if (checkSizes) { this._setSizes(); }
  7700. if (!this.canStick) {
  7701. if (this.isStuck) {
  7702. this._removeSticky(true);
  7703. }
  7704. return false;
  7705. }
  7706. if (!scroll) { scroll = window.pageYOffset; }
  7707. if (scroll >= this.topPoint) {
  7708. if (scroll <= this.bottomPoint) {
  7709. if (!this.isStuck) {
  7710. this._setSticky();
  7711. }
  7712. } else {
  7713. if (this.isStuck) {
  7714. this._removeSticky(false);
  7715. }
  7716. }
  7717. } else {
  7718. if (this.isStuck) {
  7719. this._removeSticky(true);
  7720. }
  7721. }
  7722. }
  7723. /**
  7724. * Causes the $element to become stuck.
  7725. * Adds `position: fixed;`, and helper classes.
  7726. * @fires Sticky#stuckto
  7727. * @function
  7728. * @private
  7729. */
  7730. _setSticky() {
  7731. var _this = this,
  7732. stickTo = this.options.stickTo,
  7733. mrgn = stickTo === 'top' ? 'marginTop' : 'marginBottom',
  7734. notStuckTo = stickTo === 'top' ? 'bottom' : 'top',
  7735. css = {};
  7736. css[mrgn] = `${this.options[mrgn]}em`;
  7737. css[stickTo] = 0;
  7738. css[notStuckTo] = 'auto';
  7739. this.isStuck = true;
  7740. this.$element.removeClass(`is-anchored is-at-${notStuckTo}`)
  7741. .addClass(`is-stuck is-at-${stickTo}`)
  7742. .css(css)
  7743. /**
  7744. * Fires when the $element has become `position: fixed;`
  7745. * Namespaced to `top` or `bottom`, e.g. `sticky.zf.stuckto:top`
  7746. * @event Sticky#stuckto
  7747. */
  7748. .trigger(`sticky.zf.stuckto:${stickTo}`);
  7749. this.$element.on("transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd", function() {
  7750. _this._setSizes();
  7751. });
  7752. }
  7753. /**
  7754. * Causes the $element to become unstuck.
  7755. * Removes `position: fixed;`, and helper classes.
  7756. * Adds other helper classes.
  7757. * @param {Boolean} isTop - tells the function if the $element should anchor to the top or bottom of its $anchor element.
  7758. * @fires Sticky#unstuckfrom
  7759. * @private
  7760. */
  7761. _removeSticky(isTop) {
  7762. var stickTo = this.options.stickTo,
  7763. stickToTop = stickTo === 'top',
  7764. css = {},
  7765. anchorPt = (this.points ? this.points[1] - this.points[0] : this.anchorHeight) - this.elemHeight,
  7766. mrgn = stickToTop ? 'marginTop' : 'marginBottom',
  7767. topOrBottom = isTop ? 'top' : 'bottom';
  7768. css[mrgn] = 0;
  7769. css['bottom'] = 'auto';
  7770. if(isTop) {
  7771. css['top'] = 0;
  7772. } else {
  7773. css['top'] = anchorPt;
  7774. }
  7775. this.isStuck = false;
  7776. this.$element.removeClass(`is-stuck is-at-${stickTo}`)
  7777. .addClass(`is-anchored is-at-${topOrBottom}`)
  7778. .css(css)
  7779. /**
  7780. * Fires when the $element has become anchored.
  7781. * Namespaced to `top` or `bottom`, e.g. `sticky.zf.unstuckfrom:bottom`
  7782. * @event Sticky#unstuckfrom
  7783. */
  7784. .trigger(`sticky.zf.unstuckfrom:${topOrBottom}`);
  7785. }
  7786. /**
  7787. * Sets the $element and $container sizes for plugin.
  7788. * Calls `_setBreakPoints`.
  7789. * @param {Function} cb - optional callback function to fire on completion of `_setBreakPoints`.
  7790. * @private
  7791. */
  7792. _setSizes(cb) {
  7793. this.canStick = MediaQuery.is(this.options.stickyOn);
  7794. if (!this.canStick) {
  7795. if (cb && typeof cb === 'function') { cb(); }
  7796. }
  7797. var newElemWidth = this.$container[0].getBoundingClientRect().width,
  7798. comp = window.getComputedStyle(this.$container[0]),
  7799. pdngl = parseInt(comp['padding-left'], 10),
  7800. pdngr = parseInt(comp['padding-right'], 10);
  7801. if (this.$anchor && this.$anchor.length) {
  7802. this.anchorHeight = this.$anchor[0].getBoundingClientRect().height;
  7803. } else {
  7804. this._parsePoints();
  7805. }
  7806. this.$element.css({
  7807. 'max-width': `${newElemWidth - pdngl - pdngr}px`
  7808. });
  7809. var newContainerHeight = this.$element[0].getBoundingClientRect().height || this.containerHeight;
  7810. if (this.$element.css("display") == "none") {
  7811. newContainerHeight = 0;
  7812. }
  7813. this.containerHeight = newContainerHeight;
  7814. this.$container.css({
  7815. height: newContainerHeight
  7816. });
  7817. this.elemHeight = newContainerHeight;
  7818. if (!this.isStuck) {
  7819. if (this.$element.hasClass('is-at-bottom')) {
  7820. var anchorPt = (this.points ? this.points[1] - this.$container.offset().top : this.anchorHeight) - this.elemHeight;
  7821. this.$element.css('top', anchorPt);
  7822. }
  7823. }
  7824. this._setBreakPoints(newContainerHeight, function() {
  7825. if (cb && typeof cb === 'function') { cb(); }
  7826. });
  7827. }
  7828. /**
  7829. * Sets the upper and lower breakpoints for the element to become sticky/unsticky.
  7830. * @param {Number} elemHeight - px value for sticky.$element height, calculated by `_setSizes`.
  7831. * @param {Function} cb - optional callback function to be called on completion.
  7832. * @private
  7833. */
  7834. _setBreakPoints(elemHeight, cb) {
  7835. if (!this.canStick) {
  7836. if (cb && typeof cb === 'function') { cb(); }
  7837. else { return false; }
  7838. }
  7839. var mTop = emCalc(this.options.marginTop),
  7840. mBtm = emCalc(this.options.marginBottom),
  7841. topPoint = this.points ? this.points[0] : this.$anchor.offset().top,
  7842. bottomPoint = this.points ? this.points[1] : topPoint + this.anchorHeight,
  7843. // topPoint = this.$anchor.offset().top || this.points[0],
  7844. // bottomPoint = topPoint + this.anchorHeight || this.points[1],
  7845. winHeight = window.innerHeight;
  7846. if (this.options.stickTo === 'top') {
  7847. topPoint -= mTop;
  7848. bottomPoint -= (elemHeight + mTop);
  7849. } else if (this.options.stickTo === 'bottom') {
  7850. topPoint -= (winHeight - (elemHeight + mBtm));
  7851. bottomPoint -= (winHeight - mBtm);
  7852. }
  7853. this.topPoint = topPoint;
  7854. this.bottomPoint = bottomPoint;
  7855. if (cb && typeof cb === 'function') { cb(); }
  7856. }
  7857. /**
  7858. * Destroys the current sticky element.
  7859. * Resets the element to the top position first.
  7860. * Removes event listeners, JS-added css properties and classes, and unwraps the $element if the JS added the $container.
  7861. * @function
  7862. */
  7863. _destroy() {
  7864. this._removeSticky(true);
  7865. this.$element.removeClass(`${this.options.stickyClass} is-anchored is-at-top`)
  7866. .css({
  7867. height: '',
  7868. top: '',
  7869. bottom: '',
  7870. 'max-width': ''
  7871. })
  7872. .off('resizeme.zf.trigger')
  7873. .off('mutateme.zf.trigger');
  7874. if (this.$anchor && this.$anchor.length) {
  7875. this.$anchor.off('change.zf.sticky');
  7876. }
  7877. if (this.scrollListener) $(window).off(this.scrollListener);
  7878. if (this.onLoadListener) $(window).off(this.onLoadListener);
  7879. if (this.wasWrapped) {
  7880. this.$element.unwrap();
  7881. } else {
  7882. this.$container.removeClass(this.options.containerClass)
  7883. .css({
  7884. height: ''
  7885. });
  7886. }
  7887. }
  7888. }
  7889. Sticky.defaults = {
  7890. /**
  7891. * Customizable container template. Add your own classes for styling and sizing.
  7892. * @option
  7893. * @type {string}
  7894. * @default '&lt;div data-sticky-container&gt;&lt;/div&gt;'
  7895. */
  7896. container: '<div data-sticky-container></div>',
  7897. /**
  7898. * Location in the view the element sticks to. Can be `'top'` or `'bottom'`.
  7899. * @option
  7900. * @type {string}
  7901. * @default 'top'
  7902. */
  7903. stickTo: 'top',
  7904. /**
  7905. * If anchored to a single element, the id of that element.
  7906. * @option
  7907. * @type {string}
  7908. * @default ''
  7909. */
  7910. anchor: '',
  7911. /**
  7912. * If using more than one element as anchor points, the id of the top anchor.
  7913. * @option
  7914. * @type {string}
  7915. * @default ''
  7916. */
  7917. topAnchor: '',
  7918. /**
  7919. * If using more than one element as anchor points, the id of the bottom anchor.
  7920. * @option
  7921. * @type {string}
  7922. * @default ''
  7923. */
  7924. btmAnchor: '',
  7925. /**
  7926. * Margin, in `em`'s to apply to the top of the element when it becomes sticky.
  7927. * @option
  7928. * @type {number}
  7929. * @default 1
  7930. */
  7931. marginTop: 1,
  7932. /**
  7933. * Margin, in `em`'s to apply to the bottom of the element when it becomes sticky.
  7934. * @option
  7935. * @type {number}
  7936. * @default 1
  7937. */
  7938. marginBottom: 1,
  7939. /**
  7940. * Breakpoint string that is the minimum screen size an element should become sticky.
  7941. * @option
  7942. * @type {string}
  7943. * @default 'medium'
  7944. */
  7945. stickyOn: 'medium',
  7946. /**
  7947. * Class applied to sticky element, and removed on destruction. Foundation defaults to `sticky`.
  7948. * @option
  7949. * @type {string}
  7950. * @default 'sticky'
  7951. */
  7952. stickyClass: 'sticky',
  7953. /**
  7954. * Class applied to sticky container. Foundation defaults to `sticky-container`.
  7955. * @option
  7956. * @type {string}
  7957. * @default 'sticky-container'
  7958. */
  7959. containerClass: 'sticky-container',
  7960. /**
  7961. * Number of scroll events between the plugin's recalculating sticky points. Setting it to `0` will cause it to recalc every scroll event, setting it to `-1` will prevent recalc on scroll.
  7962. * @option
  7963. * @type {number}
  7964. * @default -1
  7965. */
  7966. checkEvery: -1
  7967. };
  7968. /**
  7969. * Helper function to calculate em values
  7970. * @param Number {em} - number of em's to calculate into pixels
  7971. */
  7972. function emCalc(em) {
  7973. return parseInt(window.getComputedStyle(document.body, null).fontSize, 10) * em;
  7974. }
  7975. /**
  7976. * Tabs module.
  7977. * @module foundation.tabs
  7978. * @requires foundation.util.keyboard
  7979. * @requires foundation.util.imageLoader if tabs contain images
  7980. */
  7981. class Tabs extends Plugin {
  7982. /**
  7983. * Creates a new instance of tabs.
  7984. * @class
  7985. * @name Tabs
  7986. * @fires Tabs#init
  7987. * @param {jQuery} element - jQuery object to make into tabs.
  7988. * @param {Object} options - Overrides to the default plugin settings.
  7989. */
  7990. _setup(element, options) {
  7991. this.$element = element;
  7992. this.options = $.extend({}, Tabs.defaults, this.$element.data(), options);
  7993. this.className = 'Tabs'; // ie9 back compat
  7994. this._init();
  7995. Keyboard.register('Tabs', {
  7996. 'ENTER': 'open',
  7997. 'SPACE': 'open',
  7998. 'ARROW_RIGHT': 'next',
  7999. 'ARROW_UP': 'previous',
  8000. 'ARROW_DOWN': 'next',
  8001. 'ARROW_LEFT': 'previous'
  8002. // 'TAB': 'next',
  8003. // 'SHIFT_TAB': 'previous'
  8004. });
  8005. }
  8006. /**
  8007. * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
  8008. * @private
  8009. */
  8010. _init() {
  8011. var _this = this;
  8012. this._isInitializing = true;
  8013. this.$element.attr({'role': 'tablist'});
  8014. this.$tabTitles = this.$element.find(`.${this.options.linkClass}`);
  8015. this.$tabContent = $(`[data-tabs-content="${this.$element[0].id}"]`);
  8016. this.$tabTitles.each(function(){
  8017. var $elem = $(this),
  8018. $link = $elem.find('a'),
  8019. isActive = $elem.hasClass(`${_this.options.linkActiveClass}`),
  8020. hash = $link.attr('data-tabs-target') || $link[0].hash.slice(1),
  8021. linkId = $link[0].id ? $link[0].id : `${hash}-label`,
  8022. $tabContent = $(`#${hash}`);
  8023. $elem.attr({'role': 'presentation'});
  8024. $link.attr({
  8025. 'role': 'tab',
  8026. 'aria-controls': hash,
  8027. 'aria-selected': isActive,
  8028. 'id': linkId,
  8029. 'tabindex': isActive ? '0' : '-1'
  8030. });
  8031. $tabContent.attr({
  8032. 'role': 'tabpanel',
  8033. 'aria-labelledby': linkId
  8034. });
  8035. // Save up the initial hash to return to it later when going back in history
  8036. if (isActive) {
  8037. _this._initialAnchor = `#${hash}`;
  8038. }
  8039. if(!isActive) {
  8040. $tabContent.attr('aria-hidden', 'true');
  8041. }
  8042. if(isActive && _this.options.autoFocus){
  8043. _this.onLoadListener = onLoad($(window), function() {
  8044. $('html, body').animate({ scrollTop: $elem.offset().top }, _this.options.deepLinkSmudgeDelay, () => {
  8045. $link.focus();
  8046. });
  8047. });
  8048. }
  8049. });
  8050. if(this.options.matchHeight) {
  8051. var $images = this.$tabContent.find('img');
  8052. if ($images.length) {
  8053. onImagesLoaded($images, this._setHeight.bind(this));
  8054. } else {
  8055. this._setHeight();
  8056. }
  8057. }
  8058. // Current context-bound function to open tabs on page load or history hashchange
  8059. this._checkDeepLink = () => {
  8060. var anchor = window.location.hash;
  8061. if (!anchor.length) {
  8062. // If we are still initializing and there is no anchor, then there is nothing to do
  8063. if (this._isInitializing) return;
  8064. // Otherwise, move to the initial anchor
  8065. if (this._initialAnchor) anchor = this._initialAnchor;
  8066. }
  8067. var $anchor = anchor && $(anchor);
  8068. var $link = anchor && this.$element.find('[href$="'+anchor+'"]');
  8069. // Whether the anchor element that has been found is part of this element
  8070. var isOwnAnchor = !!($anchor.length && $link.length);
  8071. // If there is an anchor for the hash, select it
  8072. if ($anchor && $anchor.length && $link && $link.length) {
  8073. this.selectTab($anchor, true);
  8074. }
  8075. // Otherwise, collapse everything
  8076. else {
  8077. this._collapse();
  8078. }
  8079. if (isOwnAnchor) {
  8080. // Roll up a little to show the titles
  8081. if (this.options.deepLinkSmudge) {
  8082. var offset = this.$element.offset();
  8083. $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay);
  8084. }
  8085. /**
  8086. * Fires when the plugin has deeplinked at pageload
  8087. * @event Tabs#deeplink
  8088. */
  8089. this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]);
  8090. }
  8091. };
  8092. //use browser to open a tab, if it exists in this tabset
  8093. if (this.options.deepLink) {
  8094. this._checkDeepLink();
  8095. }
  8096. this._events();
  8097. this._isInitializing = false;
  8098. }
  8099. /**
  8100. * Adds event handlers for items within the tabs.
  8101. * @private
  8102. */
  8103. _events() {
  8104. this._addKeyHandler();
  8105. this._addClickHandler();
  8106. this._setHeightMqHandler = null;
  8107. if (this.options.matchHeight) {
  8108. this._setHeightMqHandler = this._setHeight.bind(this);
  8109. $(window).on('changed.zf.mediaquery', this._setHeightMqHandler);
  8110. }
  8111. if(this.options.deepLink) {
  8112. $(window).on('hashchange', this._checkDeepLink);
  8113. }
  8114. }
  8115. /**
  8116. * Adds click handlers for items within the tabs.
  8117. * @private
  8118. */
  8119. _addClickHandler() {
  8120. var _this = this;
  8121. this.$element
  8122. .off('click.zf.tabs')
  8123. .on('click.zf.tabs', `.${this.options.linkClass}`, function(e){
  8124. e.preventDefault();
  8125. e.stopPropagation();
  8126. _this._handleTabChange($(this));
  8127. });
  8128. }
  8129. /**
  8130. * Adds keyboard event handlers for items within the tabs.
  8131. * @private
  8132. */
  8133. _addKeyHandler() {
  8134. var _this = this;
  8135. this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
  8136. if (e.which === 9) return;
  8137. var $element = $(this),
  8138. $elements = $element.parent('ul').children('li'),
  8139. $prevElement,
  8140. $nextElement;
  8141. $elements.each(function(i) {
  8142. if ($(this).is($element)) {
  8143. if (_this.options.wrapOnKeys) {
  8144. $prevElement = i === 0 ? $elements.last() : $elements.eq(i-1);
  8145. $nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1);
  8146. } else {
  8147. $prevElement = $elements.eq(Math.max(0, i-1));
  8148. $nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
  8149. }
  8150. return;
  8151. }
  8152. });
  8153. // handle keyboard event with keyboard util
  8154. Keyboard.handleKey(e, 'Tabs', {
  8155. open: function() {
  8156. $element.find('[role="tab"]').focus();
  8157. _this._handleTabChange($element);
  8158. },
  8159. previous: function() {
  8160. $prevElement.find('[role="tab"]').focus();
  8161. _this._handleTabChange($prevElement);
  8162. },
  8163. next: function() {
  8164. $nextElement.find('[role="tab"]').focus();
  8165. _this._handleTabChange($nextElement);
  8166. },
  8167. handled: function() {
  8168. e.stopPropagation();
  8169. e.preventDefault();
  8170. }
  8171. });
  8172. });
  8173. }
  8174. /**
  8175. * Opens the tab `$targetContent` defined by `$target`. Collapses active tab.
  8176. * @param {jQuery} $target - Tab to open.
  8177. * @param {boolean} historyHandled - browser has already handled a history update
  8178. * @fires Tabs#change
  8179. * @function
  8180. */
  8181. _handleTabChange($target, historyHandled) {
  8182. // With `activeCollapse`, if the target is the active Tab, collapse it.
  8183. if ($target.hasClass(`${this.options.linkActiveClass}`)) {
  8184. if(this.options.activeCollapse) {
  8185. this._collapse();
  8186. }
  8187. return;
  8188. }
  8189. var $oldTab = this.$element.
  8190. find(`.${this.options.linkClass}.${this.options.linkActiveClass}`),
  8191. $tabLink = $target.find('[role="tab"]'),
  8192. target = $tabLink.attr('data-tabs-target'),
  8193. anchor = target && target.length ? `#${target}` : $tabLink[0].hash,
  8194. $targetContent = this.$tabContent.find(anchor);
  8195. //close old tab
  8196. this._collapseTab($oldTab);
  8197. //open new tab
  8198. this._openTab($target);
  8199. //either replace or update browser history
  8200. if (this.options.deepLink && !historyHandled) {
  8201. if (this.options.updateHistory) {
  8202. history.pushState({}, '', anchor);
  8203. } else {
  8204. history.replaceState({}, '', anchor);
  8205. }
  8206. }
  8207. /**
  8208. * Fires when the plugin has successfully changed tabs.
  8209. * @event Tabs#change
  8210. */
  8211. this.$element.trigger('change.zf.tabs', [$target, $targetContent]);
  8212. //fire to children a mutation event
  8213. $targetContent.find("[data-mutate]").trigger("mutateme.zf.trigger");
  8214. }
  8215. /**
  8216. * Opens the tab `$targetContent` defined by `$target`.
  8217. * @param {jQuery} $target - Tab to open.
  8218. * @function
  8219. */
  8220. _openTab($target) {
  8221. var $tabLink = $target.find('[role="tab"]'),
  8222. hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1),
  8223. $targetContent = this.$tabContent.find(`#${hash}`);
  8224. $target.addClass(`${this.options.linkActiveClass}`);
  8225. $tabLink.attr({
  8226. 'aria-selected': 'true',
  8227. 'tabindex': '0'
  8228. });
  8229. $targetContent
  8230. .addClass(`${this.options.panelActiveClass}`).removeAttr('aria-hidden');
  8231. }
  8232. /**
  8233. * Collapses `$targetContent` defined by `$target`.
  8234. * @param {jQuery} $target - Tab to collapse.
  8235. * @function
  8236. */
  8237. _collapseTab($target) {
  8238. var $target_anchor = $target
  8239. .removeClass(`${this.options.linkActiveClass}`)
  8240. .find('[role="tab"]')
  8241. .attr({
  8242. 'aria-selected': 'false',
  8243. 'tabindex': -1
  8244. });
  8245. $(`#${$target_anchor.attr('aria-controls')}`)
  8246. .removeClass(`${this.options.panelActiveClass}`)
  8247. .attr({ 'aria-hidden': 'true' });
  8248. }
  8249. /**
  8250. * Collapses the active Tab.
  8251. * @fires Tabs#collapse
  8252. * @function
  8253. */
  8254. _collapse() {
  8255. var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`);
  8256. if ($activeTab.length) {
  8257. this._collapseTab($activeTab);
  8258. /**
  8259. * Fires when the plugin has successfully collapsed tabs.
  8260. * @event Tabs#collapse
  8261. */
  8262. this.$element.trigger('collapse.zf.tabs', [$activeTab]);
  8263. }
  8264. }
  8265. /**
  8266. * Public method for selecting a content pane to display.
  8267. * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display.
  8268. * @param {boolean} historyHandled - browser has already handled a history update
  8269. * @function
  8270. */
  8271. selectTab(elem, historyHandled) {
  8272. var idStr;
  8273. if (typeof elem === 'object') {
  8274. idStr = elem[0].id;
  8275. } else {
  8276. idStr = elem;
  8277. }
  8278. if (idStr.indexOf('#') < 0) {
  8279. idStr = `#${idStr}`;
  8280. }
  8281. var $target = this.$tabTitles.has(`[href$="${idStr}"]`);
  8282. this._handleTabChange($target, historyHandled);
  8283. };
  8284. /**
  8285. * Sets the height of each panel to the height of the tallest panel.
  8286. * If enabled in options, gets called on media query change.
  8287. * If loading content via external source, can be called directly or with _reflow.
  8288. * If enabled with `data-match-height="true"`, tabs sets to equal height
  8289. * @function
  8290. * @private
  8291. */
  8292. _setHeight() {
  8293. var max = 0,
  8294. _this = this; // Lock down the `this` value for the root tabs object
  8295. this.$tabContent
  8296. .find(`.${this.options.panelClass}`)
  8297. .css('height', '')
  8298. .each(function() {
  8299. var panel = $(this),
  8300. isActive = panel.hasClass(`${_this.options.panelActiveClass}`); // get the options from the parent instead of trying to get them from the child
  8301. if (!isActive) {
  8302. panel.css({'visibility': 'hidden', 'display': 'block'});
  8303. }
  8304. var temp = this.getBoundingClientRect().height;
  8305. if (!isActive) {
  8306. panel.css({
  8307. 'visibility': '',
  8308. 'display': ''
  8309. });
  8310. }
  8311. max = temp > max ? temp : max;
  8312. })
  8313. .css('height', `${max}px`);
  8314. }
  8315. /**
  8316. * Destroys an instance of tabs.
  8317. * @fires Tabs#destroyed
  8318. */
  8319. _destroy() {
  8320. this.$element
  8321. .find(`.${this.options.linkClass}`)
  8322. .off('.zf.tabs').hide().end()
  8323. .find(`.${this.options.panelClass}`)
  8324. .hide();
  8325. if (this.options.matchHeight) {
  8326. if (this._setHeightMqHandler != null) {
  8327. $(window).off('changed.zf.mediaquery', this._setHeightMqHandler);
  8328. }
  8329. }
  8330. if (this.options.deepLink) {
  8331. $(window).off('hashchange', this._checkDeepLink);
  8332. }
  8333. if (this.onLoadListener) {
  8334. $(window).off(this.onLoadListener);
  8335. }
  8336. }
  8337. }
  8338. Tabs.defaults = {
  8339. /**
  8340. * Link the location hash to the active pane.
  8341. * Set the location hash when the active pane changes, and open the corresponding pane when the location changes.
  8342. * @option
  8343. * @type {boolean}
  8344. * @default false
  8345. */
  8346. deepLink: false,
  8347. /**
  8348. * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the tab panel is visible
  8349. * @option
  8350. * @type {boolean}
  8351. * @default false
  8352. */
  8353. deepLinkSmudge: false,
  8354. /**
  8355. * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment
  8356. * @option
  8357. * @type {number}
  8358. * @default 300
  8359. */
  8360. deepLinkSmudgeDelay: 300,
  8361. /**
  8362. * If `deepLink` is enabled, update the browser history with the open tab
  8363. * @option
  8364. * @type {boolean}
  8365. * @default false
  8366. */
  8367. updateHistory: false,
  8368. /**
  8369. * Allows the window to scroll to content of active pane on load.
  8370. * Not recommended if more than one tab panel per page.
  8371. * @option
  8372. * @type {boolean}
  8373. * @default false
  8374. */
  8375. autoFocus: false,
  8376. /**
  8377. * Allows keyboard input to 'wrap' around the tab links.
  8378. * @option
  8379. * @type {boolean}
  8380. * @default true
  8381. */
  8382. wrapOnKeys: true,
  8383. /**
  8384. * Allows the tab content panes to match heights if set to true.
  8385. * @option
  8386. * @type {boolean}
  8387. * @default false
  8388. */
  8389. matchHeight: false,
  8390. /**
  8391. * Allows active tabs to collapse when clicked.
  8392. * @option
  8393. * @type {boolean}
  8394. * @default false
  8395. */
  8396. activeCollapse: false,
  8397. /**
  8398. * Class applied to `li`'s in tab link list.
  8399. * @option
  8400. * @type {string}
  8401. * @default 'tabs-title'
  8402. */
  8403. linkClass: 'tabs-title',
  8404. /**
  8405. * Class applied to the active `li` in tab link list.
  8406. * @option
  8407. * @type {string}
  8408. * @default 'is-active'
  8409. */
  8410. linkActiveClass: 'is-active',
  8411. /**
  8412. * Class applied to the content containers.
  8413. * @option
  8414. * @type {string}
  8415. * @default 'tabs-panel'
  8416. */
  8417. panelClass: 'tabs-panel',
  8418. /**
  8419. * Class applied to the active content container.
  8420. * @option
  8421. * @type {string}
  8422. * @default 'is-active'
  8423. */
  8424. panelActiveClass: 'is-active'
  8425. };
  8426. /**
  8427. * Toggler module.
  8428. * @module foundation.toggler
  8429. * @requires foundation.util.motion
  8430. * @requires foundation.util.triggers
  8431. */
  8432. class Toggler extends Plugin {
  8433. /**
  8434. * Creates a new instance of Toggler.
  8435. * @class
  8436. * @name Toggler
  8437. * @fires Toggler#init
  8438. * @param {Object} element - jQuery object to add the trigger to.
  8439. * @param {Object} options - Overrides to the default plugin settings.
  8440. */
  8441. _setup(element, options) {
  8442. this.$element = element;
  8443. this.options = $.extend({}, Toggler.defaults, element.data(), options);
  8444. this.className = '';
  8445. this.className = 'Toggler'; // ie9 back compat
  8446. // Triggers init is idempotent, just need to make sure it is initialized
  8447. Triggers.init($);
  8448. this._init();
  8449. this._events();
  8450. }
  8451. /**
  8452. * Initializes the Toggler plugin by parsing the toggle class from data-toggler, or animation classes from data-animate.
  8453. * @function
  8454. * @private
  8455. */
  8456. _init() {
  8457. var input;
  8458. // Parse animation classes if they were set
  8459. if (this.options.animate) {
  8460. input = this.options.animate.split(' ');
  8461. this.animationIn = input[0];
  8462. this.animationOut = input[1] || null;
  8463. }
  8464. // Otherwise, parse toggle class
  8465. else {
  8466. input = this.$element.data('toggler');
  8467. // Allow for a . at the beginning of the string
  8468. this.className = input[0] === '.' ? input.slice(1) : input;
  8469. }
  8470. // Add ARIA attributes to triggers:
  8471. var id = this.$element[0].id,
  8472. $triggers = $(`[data-open~="${id}"], [data-close~="${id}"], [data-toggle~="${id}"]`);
  8473. // - aria-expanded: according to the element visibility.
  8474. $triggers.attr('aria-expanded', !this.$element.is(':hidden'));
  8475. // - aria-controls: adding the element id to it if not already in it.
  8476. $triggers.each((index, trigger) => {
  8477. const $trigger = $(trigger);
  8478. const controls = $trigger.attr('aria-controls') || '';
  8479. const containsId = new RegExp(`\\b${RegExpEscape(id)}\\b`).test(controls);
  8480. if (!containsId) $trigger.attr('aria-controls', controls ? `${controls} ${id}` : id);
  8481. });
  8482. }
  8483. /**
  8484. * Initializes events for the toggle trigger.
  8485. * @function
  8486. * @private
  8487. */
  8488. _events() {
  8489. this.$element.off('toggle.zf.trigger').on('toggle.zf.trigger', this.toggle.bind(this));
  8490. }
  8491. /**
  8492. * Toggles the target class on the target element. An event is fired from the original trigger depending on if the resultant state was "on" or "off".
  8493. * @function
  8494. * @fires Toggler#on
  8495. * @fires Toggler#off
  8496. */
  8497. toggle() {
  8498. this[ this.options.animate ? '_toggleAnimate' : '_toggleClass']();
  8499. }
  8500. _toggleClass() {
  8501. this.$element.toggleClass(this.className);
  8502. var isOn = this.$element.hasClass(this.className);
  8503. if (isOn) {
  8504. /**
  8505. * Fires if the target element has the class after a toggle.
  8506. * @event Toggler#on
  8507. */
  8508. this.$element.trigger('on.zf.toggler');
  8509. }
  8510. else {
  8511. /**
  8512. * Fires if the target element does not have the class after a toggle.
  8513. * @event Toggler#off
  8514. */
  8515. this.$element.trigger('off.zf.toggler');
  8516. }
  8517. this._updateARIA(isOn);
  8518. this.$element.find('[data-mutate]').trigger('mutateme.zf.trigger');
  8519. }
  8520. _toggleAnimate() {
  8521. var _this = this;
  8522. if (this.$element.is(':hidden')) {
  8523. Motion.animateIn(this.$element, this.animationIn, function() {
  8524. _this._updateARIA(true);
  8525. this.trigger('on.zf.toggler');
  8526. this.find('[data-mutate]').trigger('mutateme.zf.trigger');
  8527. });
  8528. }
  8529. else {
  8530. Motion.animateOut(this.$element, this.animationOut, function() {
  8531. _this._updateARIA(false);
  8532. this.trigger('off.zf.toggler');
  8533. this.find('[data-mutate]').trigger('mutateme.zf.trigger');
  8534. });
  8535. }
  8536. }
  8537. _updateARIA(isOn) {
  8538. var id = this.$element[0].id;
  8539. $(`[data-open="${id}"], [data-close="${id}"], [data-toggle="${id}"]`)
  8540. .attr({
  8541. 'aria-expanded': isOn ? true : false
  8542. });
  8543. }
  8544. /**
  8545. * Destroys the instance of Toggler on the element.
  8546. * @function
  8547. */
  8548. _destroy() {
  8549. this.$element.off('.zf.toggler');
  8550. }
  8551. }
  8552. Toggler.defaults = {
  8553. /**
  8554. * Tells the plugin if the element should animated when toggled.
  8555. * @option
  8556. * @type {boolean}
  8557. * @default false
  8558. */
  8559. animate: false
  8560. };
  8561. /**
  8562. * Tooltip module.
  8563. * @module foundation.tooltip
  8564. * @requires foundation.util.box
  8565. * @requires foundation.util.mediaQuery
  8566. * @requires foundation.util.triggers
  8567. */
  8568. class Tooltip extends Positionable {
  8569. /**
  8570. * Creates a new instance of a Tooltip.
  8571. * @class
  8572. * @name Tooltip
  8573. * @fires Tooltip#init
  8574. * @param {jQuery} element - jQuery object to attach a tooltip to.
  8575. * @param {Object} options - object to extend the default configuration.
  8576. */
  8577. _setup(element, options) {
  8578. this.$element = element;
  8579. this.options = $.extend({}, Tooltip.defaults, this.$element.data(), options);
  8580. this.className = 'Tooltip'; // ie9 back compat
  8581. this.isActive = false;
  8582. this.isClick = false;
  8583. // Triggers init is idempotent, just need to make sure it is initialized
  8584. Triggers.init($);
  8585. this._init();
  8586. }
  8587. /**
  8588. * Initializes the tooltip by setting the creating the tip element, adding it's text, setting private variables and setting attributes on the anchor.
  8589. * @private
  8590. */
  8591. _init() {
  8592. MediaQuery._init();
  8593. var elemId = this.$element.attr('aria-describedby') || GetYoDigits(6, 'tooltip');
  8594. this.options.tipText = this.options.tipText || this.$element.attr('title');
  8595. this.template = this.options.template ? $(this.options.template) : this._buildTemplate(elemId);
  8596. if (this.options.allowHtml) {
  8597. this.template.appendTo(document.body)
  8598. .html(this.options.tipText)
  8599. .hide();
  8600. } else {
  8601. this.template.appendTo(document.body)
  8602. .text(this.options.tipText)
  8603. .hide();
  8604. }
  8605. this.$element.attr({
  8606. 'title': '',
  8607. 'aria-describedby': elemId,
  8608. 'data-yeti-box': elemId,
  8609. 'data-toggle': elemId,
  8610. 'data-resize': elemId
  8611. }).addClass(this.options.triggerClass);
  8612. super._init();
  8613. this._events();
  8614. }
  8615. _getDefaultPosition() {
  8616. // handle legacy classnames
  8617. var position = this.$element[0].className.match(/\b(top|left|right|bottom)\b/g);
  8618. return position ? position[0] : 'top';
  8619. }
  8620. _getDefaultAlignment() {
  8621. return 'center';
  8622. }
  8623. _getHOffset() {
  8624. if(this.position === 'left' || this.position === 'right') {
  8625. return this.options.hOffset + this.options.tooltipWidth;
  8626. } else {
  8627. return this.options.hOffset
  8628. }
  8629. }
  8630. _getVOffset() {
  8631. if(this.position === 'top' || this.position === 'bottom') {
  8632. return this.options.vOffset + this.options.tooltipHeight;
  8633. } else {
  8634. return this.options.vOffset
  8635. }
  8636. }
  8637. /**
  8638. * builds the tooltip element, adds attributes, and returns the template.
  8639. * @private
  8640. */
  8641. _buildTemplate(id) {
  8642. var templateClasses = (`${this.options.tooltipClass} ${this.options.templateClasses}`).trim();
  8643. var $template = $('<div></div>').addClass(templateClasses).attr({
  8644. 'role': 'tooltip',
  8645. 'aria-hidden': true,
  8646. 'data-is-active': false,
  8647. 'data-is-focus': false,
  8648. 'id': id
  8649. });
  8650. return $template;
  8651. }
  8652. /**
  8653. * sets the position class of an element and recursively calls itself until there are no more possible positions to attempt, or the tooltip element is no longer colliding.
  8654. * if the tooltip is larger than the screen width, default to full width - any user selected margin
  8655. * @private
  8656. */
  8657. _setPosition() {
  8658. super._setPosition(this.$element, this.template);
  8659. }
  8660. /**
  8661. * reveals the tooltip, and fires an event to close any other open tooltips on the page
  8662. * @fires Tooltip#closeme
  8663. * @fires Tooltip#show
  8664. * @function
  8665. */
  8666. show() {
  8667. if (this.options.showOn !== 'all' && !MediaQuery.is(this.options.showOn)) {
  8668. // console.error('The screen is too small to display this tooltip');
  8669. return false;
  8670. }
  8671. var _this = this;
  8672. this.template.css('visibility', 'hidden').show();
  8673. this._setPosition();
  8674. this.template.removeClass('top bottom left right').addClass(this.position);
  8675. this.template.removeClass('align-top align-bottom align-left align-right align-center').addClass('align-' + this.alignment);
  8676. /**
  8677. * Fires to close all other open tooltips on the page
  8678. * @event Closeme#tooltip
  8679. */
  8680. this.$element.trigger('closeme.zf.tooltip', this.template.attr('id'));
  8681. this.template.attr({
  8682. 'data-is-active': true,
  8683. 'aria-hidden': false
  8684. });
  8685. _this.isActive = true;
  8686. // console.log(this.template);
  8687. this.template.stop().hide().css('visibility', '').fadeIn(this.options.fadeInDuration, function() {
  8688. //maybe do stuff?
  8689. });
  8690. /**
  8691. * Fires when the tooltip is shown
  8692. * @event Tooltip#show
  8693. */
  8694. this.$element.trigger('show.zf.tooltip');
  8695. }
  8696. /**
  8697. * Hides the current tooltip, and resets the positioning class if it was changed due to collision
  8698. * @fires Tooltip#hide
  8699. * @function
  8700. */
  8701. hide() {
  8702. // console.log('hiding', this.$element.data('yeti-box'));
  8703. var _this = this;
  8704. this.template.stop().attr({
  8705. 'aria-hidden': true,
  8706. 'data-is-active': false
  8707. }).fadeOut(this.options.fadeOutDuration, function() {
  8708. _this.isActive = false;
  8709. _this.isClick = false;
  8710. });
  8711. /**
  8712. * fires when the tooltip is hidden
  8713. * @event Tooltip#hide
  8714. */
  8715. this.$element.trigger('hide.zf.tooltip');
  8716. }
  8717. /**
  8718. * adds event listeners for the tooltip and its anchor
  8719. * TODO combine some of the listeners like focus and mouseenter, etc.
  8720. * @private
  8721. */
  8722. _events() {
  8723. var _this = this;
  8724. var $template = this.template;
  8725. var isFocus = false;
  8726. if (!this.options.disableHover) {
  8727. this.$element
  8728. .on('mouseenter.zf.tooltip', function(e) {
  8729. if (!_this.isActive) {
  8730. _this.timeout = setTimeout(function() {
  8731. _this.show();
  8732. }, _this.options.hoverDelay);
  8733. }
  8734. })
  8735. .on('mouseleave.zf.tooltip', ignoreMousedisappear(function(e) {
  8736. clearTimeout(_this.timeout);
  8737. if (!isFocus || (_this.isClick && !_this.options.clickOpen)) {
  8738. _this.hide();
  8739. }
  8740. }));
  8741. }
  8742. if (this.options.clickOpen) {
  8743. this.$element.on('mousedown.zf.tooltip', function(e) {
  8744. e.stopImmediatePropagation();
  8745. if (_this.isClick) ; else {
  8746. _this.isClick = true;
  8747. if ((_this.options.disableHover || !_this.$element.attr('tabindex')) && !_this.isActive) {
  8748. _this.show();
  8749. }
  8750. }
  8751. });
  8752. } else {
  8753. this.$element.on('mousedown.zf.tooltip', function(e) {
  8754. e.stopImmediatePropagation();
  8755. _this.isClick = true;
  8756. });
  8757. }
  8758. if (!this.options.disableForTouch) {
  8759. this.$element
  8760. .on('tap.zf.tooltip touchend.zf.tooltip', function(e) {
  8761. _this.isActive ? _this.hide() : _this.show();
  8762. });
  8763. }
  8764. this.$element.on({
  8765. // 'toggle.zf.trigger': this.toggle.bind(this),
  8766. // 'close.zf.trigger': this.hide.bind(this)
  8767. 'close.zf.trigger': this.hide.bind(this)
  8768. });
  8769. this.$element
  8770. .on('focus.zf.tooltip', function(e) {
  8771. isFocus = true;
  8772. if (_this.isClick) {
  8773. // If we're not showing open on clicks, we need to pretend a click-launched focus isn't
  8774. // a real focus, otherwise on hover and come back we get bad behavior
  8775. if(!_this.options.clickOpen) { isFocus = false; }
  8776. return false;
  8777. } else {
  8778. _this.show();
  8779. }
  8780. })
  8781. .on('focusout.zf.tooltip', function(e) {
  8782. isFocus = false;
  8783. _this.isClick = false;
  8784. _this.hide();
  8785. })
  8786. .on('resizeme.zf.trigger', function() {
  8787. if (_this.isActive) {
  8788. _this._setPosition();
  8789. }
  8790. });
  8791. }
  8792. /**
  8793. * adds a toggle method, in addition to the static show() & hide() functions
  8794. * @function
  8795. */
  8796. toggle() {
  8797. if (this.isActive) {
  8798. this.hide();
  8799. } else {
  8800. this.show();
  8801. }
  8802. }
  8803. /**
  8804. * Destroys an instance of tooltip, removes template element from the view.
  8805. * @function
  8806. */
  8807. _destroy() {
  8808. this.$element.attr('title', this.template.text())
  8809. .off('.zf.trigger .zf.tooltip')
  8810. .removeClass(this.options.triggerClass)
  8811. .removeClass('top right left bottom')
  8812. .removeAttr('aria-describedby data-disable-hover data-resize data-toggle data-tooltip data-yeti-box');
  8813. this.template.remove();
  8814. }
  8815. }
  8816. Tooltip.defaults = {
  8817. disableForTouch: false,
  8818. /**
  8819. * Time, in ms, before a tooltip should open on hover.
  8820. * @option
  8821. * @type {number}
  8822. * @default 200
  8823. */
  8824. hoverDelay: 200,
  8825. /**
  8826. * Time, in ms, a tooltip should take to fade into view.
  8827. * @option
  8828. * @type {number}
  8829. * @default 150
  8830. */
  8831. fadeInDuration: 150,
  8832. /**
  8833. * Time, in ms, a tooltip should take to fade out of view.
  8834. * @option
  8835. * @type {number}
  8836. * @default 150
  8837. */
  8838. fadeOutDuration: 150,
  8839. /**
  8840. * Disables hover events from opening the tooltip if set to true
  8841. * @option
  8842. * @type {boolean}
  8843. * @default false
  8844. */
  8845. disableHover: false,
  8846. /**
  8847. * Optional addtional classes to apply to the tooltip template on init.
  8848. * @option
  8849. * @type {string}
  8850. * @default ''
  8851. */
  8852. templateClasses: '',
  8853. /**
  8854. * Non-optional class added to tooltip templates. Foundation default is 'tooltip'.
  8855. * @option
  8856. * @type {string}
  8857. * @default 'tooltip'
  8858. */
  8859. tooltipClass: 'tooltip',
  8860. /**
  8861. * Class applied to the tooltip anchor element.
  8862. * @option
  8863. * @type {string}
  8864. * @default 'has-tip'
  8865. */
  8866. triggerClass: 'has-tip',
  8867. /**
  8868. * Minimum breakpoint size at which to open the tooltip.
  8869. * @option
  8870. * @type {string}
  8871. * @default 'small'
  8872. */
  8873. showOn: 'small',
  8874. /**
  8875. * Custom template to be used to generate markup for tooltip.
  8876. * @option
  8877. * @type {string}
  8878. * @default ''
  8879. */
  8880. template: '',
  8881. /**
  8882. * Text displayed in the tooltip template on open.
  8883. * @option
  8884. * @type {string}
  8885. * @default ''
  8886. */
  8887. tipText: '',
  8888. touchCloseText: 'Tap to close.',
  8889. /**
  8890. * Allows the tooltip to remain open if triggered with a click or touch event.
  8891. * @option
  8892. * @type {boolean}
  8893. * @default true
  8894. */
  8895. clickOpen: true,
  8896. /**
  8897. * Position of tooltip. Can be left, right, bottom, top, or auto.
  8898. * @option
  8899. * @type {string}
  8900. * @default 'auto'
  8901. */
  8902. position: 'auto',
  8903. /**
  8904. * Alignment of tooltip relative to anchor. Can be left, right, bottom, top, center, or auto.
  8905. * @option
  8906. * @type {string}
  8907. * @default 'auto'
  8908. */
  8909. alignment: 'auto',
  8910. /**
  8911. * Allow overlap of container/window. If false, tooltip will first try to
  8912. * position as defined by data-position and data-alignment, but reposition if
  8913. * it would cause an overflow. @option
  8914. * @type {boolean}
  8915. * @default false
  8916. */
  8917. allowOverlap: false,
  8918. /**
  8919. * Allow overlap of only the bottom of the container. This is the most common
  8920. * behavior for dropdowns, allowing the dropdown to extend the bottom of the
  8921. * screen but not otherwise influence or break out of the container.
  8922. * Less common for tooltips.
  8923. * @option
  8924. * @type {boolean}
  8925. * @default false
  8926. */
  8927. allowBottomOverlap: false,
  8928. /**
  8929. * Distance, in pixels, the template should push away from the anchor on the Y axis.
  8930. * @option
  8931. * @type {number}
  8932. * @default 0
  8933. */
  8934. vOffset: 0,
  8935. /**
  8936. * Distance, in pixels, the template should push away from the anchor on the X axis
  8937. * @option
  8938. * @type {number}
  8939. * @default 0
  8940. */
  8941. hOffset: 0,
  8942. /**
  8943. * Distance, in pixels, the template spacing auto-adjust for a vertical tooltip
  8944. * @option
  8945. * @type {number}
  8946. * @default 14
  8947. */
  8948. tooltipHeight: 14,
  8949. /**
  8950. * Distance, in pixels, the template spacing auto-adjust for a horizontal tooltip
  8951. * @option
  8952. * @type {number}
  8953. * @default 12
  8954. */
  8955. tooltipWidth: 12,
  8956. /**
  8957. * Allow HTML in tooltip. Warning: If you are loading user-generated content into tooltips,
  8958. * allowing HTML may open yourself up to XSS attacks.
  8959. * @option
  8960. * @type {boolean}
  8961. * @default false
  8962. */
  8963. allowHtml: false
  8964. };
  8965. // The plugin matches the plugin classes with these plugin instances.
  8966. var MenuPlugins$1 = {
  8967. tabs: {
  8968. cssClass: 'tabs',
  8969. plugin: Tabs
  8970. },
  8971. accordion: {
  8972. cssClass: 'accordion',
  8973. plugin: Accordion
  8974. }
  8975. };
  8976. /**
  8977. * ResponsiveAccordionTabs module.
  8978. * @module foundation.responsiveAccordionTabs
  8979. * @requires foundation.util.motion
  8980. * @requires foundation.accordion
  8981. * @requires foundation.tabs
  8982. */
  8983. class ResponsiveAccordionTabs extends Plugin{
  8984. /**
  8985. * Creates a new instance of a responsive accordion tabs.
  8986. * @class
  8987. * @name ResponsiveAccordionTabs
  8988. * @fires ResponsiveAccordionTabs#init
  8989. * @param {jQuery} element - jQuery object to make into Responsive Accordion Tabs.
  8990. * @param {Object} options - Overrides to the default plugin settings.
  8991. */
  8992. _setup(element, options) {
  8993. this.$element = $(element);
  8994. this.options = $.extend({}, this.$element.data(), options);
  8995. this.rules = this.$element.data('responsive-accordion-tabs');
  8996. this.currentMq = null;
  8997. this.currentPlugin = null;
  8998. this.className = 'ResponsiveAccordionTabs'; // ie9 back compat
  8999. if (!this.$element.attr('id')) {
  9000. this.$element.attr('id',GetYoDigits(6, 'responsiveaccordiontabs'));
  9001. }
  9002. this._init();
  9003. this._events();
  9004. }
  9005. /**
  9006. * Initializes the Menu by parsing the classes from the 'data-responsive-accordion-tabs' attribute on the element.
  9007. * @function
  9008. * @private
  9009. */
  9010. _init() {
  9011. MediaQuery._init();
  9012. // The first time an Interchange plugin is initialized, this.rules is converted from a string of "classes" to an object of rules
  9013. if (typeof this.rules === 'string') {
  9014. let rulesTree = {};
  9015. // Parse rules from "classes" pulled from data attribute
  9016. let rules = this.rules.split(' ');
  9017. // Iterate through every rule found
  9018. for (let i = 0; i < rules.length; i++) {
  9019. let rule = rules[i].split('-');
  9020. let ruleSize = rule.length > 1 ? rule[0] : 'small';
  9021. let rulePlugin = rule.length > 1 ? rule[1] : rule[0];
  9022. if (MenuPlugins$1[rulePlugin] !== null) {
  9023. rulesTree[ruleSize] = MenuPlugins$1[rulePlugin];
  9024. }
  9025. }
  9026. this.rules = rulesTree;
  9027. }
  9028. this._getAllOptions();
  9029. if (!$.isEmptyObject(this.rules)) {
  9030. this._checkMediaQueries();
  9031. }
  9032. }
  9033. _getAllOptions() {
  9034. //get all defaults and options
  9035. var _this = this;
  9036. _this.allOptions = {};
  9037. for (var key in MenuPlugins$1) {
  9038. if (MenuPlugins$1.hasOwnProperty(key)) {
  9039. var obj = MenuPlugins$1[key];
  9040. try {
  9041. var dummyPlugin = $('<ul></ul>');
  9042. var tmpPlugin = new obj.plugin(dummyPlugin,_this.options);
  9043. for (var keyKey in tmpPlugin.options) {
  9044. if (tmpPlugin.options.hasOwnProperty(keyKey) && keyKey !== 'zfPlugin') {
  9045. var objObj = tmpPlugin.options[keyKey];
  9046. _this.allOptions[keyKey] = objObj;
  9047. }
  9048. }
  9049. tmpPlugin.destroy();
  9050. }
  9051. catch(e) {
  9052. }
  9053. }
  9054. }
  9055. }
  9056. /**
  9057. * Initializes events for the Menu.
  9058. * @function
  9059. * @private
  9060. */
  9061. _events() {
  9062. this._changedZfMediaQueryHandler = this._checkMediaQueries.bind(this);
  9063. $(window).on('changed.zf.mediaquery', this._changedZfMediaQueryHandler);
  9064. }
  9065. /**
  9066. * Checks the current screen width against available media queries. If the media query has changed, and the plugin needed has changed, the plugins will swap out.
  9067. * @function
  9068. * @private
  9069. */
  9070. _checkMediaQueries() {
  9071. var matchedMq, _this = this;
  9072. // Iterate through each rule and find the last matching rule
  9073. $.each(this.rules, function(key) {
  9074. if (MediaQuery.atLeast(key)) {
  9075. matchedMq = key;
  9076. }
  9077. });
  9078. // No match? No dice
  9079. if (!matchedMq) return;
  9080. // Plugin already initialized? We good
  9081. if (this.currentPlugin instanceof this.rules[matchedMq].plugin) return;
  9082. // Remove existing plugin-specific CSS classes
  9083. $.each(MenuPlugins$1, function(key, value) {
  9084. _this.$element.removeClass(value.cssClass);
  9085. });
  9086. // Add the CSS class for the new plugin
  9087. this.$element.addClass(this.rules[matchedMq].cssClass);
  9088. // Create an instance of the new plugin
  9089. if (this.currentPlugin) {
  9090. //don't know why but on nested elements data zfPlugin get's lost
  9091. if (!this.currentPlugin.$element.data('zfPlugin') && this.storezfData) this.currentPlugin.$element.data('zfPlugin',this.storezfData);
  9092. this.currentPlugin.destroy();
  9093. }
  9094. this._handleMarkup(this.rules[matchedMq].cssClass);
  9095. this.currentPlugin = new this.rules[matchedMq].plugin(this.$element, {});
  9096. this.storezfData = this.currentPlugin.$element.data('zfPlugin');
  9097. }
  9098. _handleMarkup(toSet){
  9099. var _this = this, fromString = 'accordion';
  9100. var $panels = $('[data-tabs-content='+this.$element.attr('id')+']');
  9101. if ($panels.length) fromString = 'tabs';
  9102. if (fromString === toSet) {
  9103. return;
  9104. }
  9105. var tabsTitle = _this.allOptions.linkClass?_this.allOptions.linkClass:'tabs-title';
  9106. var tabsPanel = _this.allOptions.panelClass?_this.allOptions.panelClass:'tabs-panel';
  9107. this.$element.removeAttr('role');
  9108. var $liHeads = this.$element.children('.'+tabsTitle+',[data-accordion-item]').removeClass(tabsTitle).removeClass('accordion-item').removeAttr('data-accordion-item');
  9109. var $liHeadsA = $liHeads.children('a').removeClass('accordion-title');
  9110. if (fromString === 'tabs') {
  9111. $panels = $panels.children('.'+tabsPanel).removeClass(tabsPanel).removeAttr('role').removeAttr('aria-hidden').removeAttr('aria-labelledby');
  9112. $panels.children('a').removeAttr('role').removeAttr('aria-controls').removeAttr('aria-selected');
  9113. }else{
  9114. $panels = $liHeads.children('[data-tab-content]').removeClass('accordion-content');
  9115. }
  9116. $panels.css({display:'',visibility:''});
  9117. $liHeads.css({display:'',visibility:''});
  9118. if (toSet === 'accordion') {
  9119. $panels.each(function(key,value){
  9120. $(value).appendTo($liHeads.get(key)).addClass('accordion-content').attr('data-tab-content','').removeClass('is-active').css({height:''});
  9121. $('[data-tabs-content='+_this.$element.attr('id')+']').after('<div id="tabs-placeholder-'+_this.$element.attr('id')+'"></div>').detach();
  9122. $liHeads.addClass('accordion-item').attr('data-accordion-item','');
  9123. $liHeadsA.addClass('accordion-title');
  9124. });
  9125. }else if (toSet === 'tabs'){
  9126. var $tabsContent = $('[data-tabs-content='+_this.$element.attr('id')+']');
  9127. var $placeholder = $('#tabs-placeholder-'+_this.$element.attr('id'));
  9128. if ($placeholder.length) {
  9129. $tabsContent = $('<div class="tabs-content"></div>').insertAfter($placeholder).attr('data-tabs-content',_this.$element.attr('id'));
  9130. $placeholder.remove();
  9131. }else{
  9132. $tabsContent = $('<div class="tabs-content"></div>').insertAfter(_this.$element).attr('data-tabs-content',_this.$element.attr('id'));
  9133. } $panels.each(function(key,value){
  9134. var tempValue = $(value).appendTo($tabsContent).addClass(tabsPanel);
  9135. var hash = $liHeadsA.get(key).hash.slice(1);
  9136. var id = $(value).attr('id') || GetYoDigits(6, 'accordion');
  9137. if (hash !== id) {
  9138. if (hash !== '') {
  9139. $(value).attr('id',hash);
  9140. }else{
  9141. hash = id;
  9142. $(value).attr('id',hash);
  9143. $($liHeadsA.get(key)).attr('href',$($liHeadsA.get(key)).attr('href').replace('#','')+'#'+hash);
  9144. } } var isActive = $($liHeads.get(key)).hasClass('is-active');
  9145. if (isActive) {
  9146. tempValue.addClass('is-active');
  9147. } });
  9148. $liHeads.addClass(tabsTitle);
  9149. } }
  9150. /**
  9151. * Destroys the instance of the current plugin on this element, as well as the window resize handler that switches the plugins out.
  9152. * @function
  9153. */
  9154. _destroy() {
  9155. if (this.currentPlugin) this.currentPlugin.destroy();
  9156. $(window).off('changed.zf.mediaquery', this._changedZfMediaQueryHandler);
  9157. }
  9158. }
  9159. ResponsiveAccordionTabs.defaults = {};
  9160. Foundation.addToJquery($);
  9161. // Add Foundation Utils to Foundation global namespace for backwards
  9162. // compatibility.
  9163. Foundation.rtl = rtl;
  9164. Foundation.GetYoDigits = GetYoDigits;
  9165. Foundation.transitionend = transitionend;
  9166. Foundation.RegExpEscape = RegExpEscape;
  9167. Foundation.onLoad = onLoad;
  9168. Foundation.Box = Box;
  9169. Foundation.onImagesLoaded = onImagesLoaded;
  9170. Foundation.Keyboard = Keyboard;
  9171. Foundation.MediaQuery = MediaQuery;
  9172. Foundation.Motion = Motion;
  9173. Foundation.Move = Move;
  9174. Foundation.Nest = Nest;
  9175. Foundation.Timer = Timer;
  9176. // Touch and Triggers previously were almost purely sede effect driven,
  9177. // so no need to add it to Foundation, just init them.
  9178. Touch.init($);
  9179. Triggers.init($, Foundation);
  9180. MediaQuery._init();
  9181. Foundation.plugin(Abide, 'Abide');
  9182. Foundation.plugin(Accordion, 'Accordion');
  9183. Foundation.plugin(AccordionMenu, 'AccordionMenu');
  9184. Foundation.plugin(Drilldown, 'Drilldown');
  9185. Foundation.plugin(Dropdown, 'Dropdown');
  9186. Foundation.plugin(DropdownMenu, 'DropdownMenu');
  9187. Foundation.plugin(Equalizer, 'Equalizer');
  9188. Foundation.plugin(Interchange, 'Interchange');
  9189. Foundation.plugin(Magellan, 'Magellan');
  9190. Foundation.plugin(OffCanvas, 'OffCanvas');
  9191. Foundation.plugin(Orbit, 'Orbit');
  9192. Foundation.plugin(ResponsiveMenu, 'ResponsiveMenu');
  9193. Foundation.plugin(ResponsiveToggle, 'ResponsiveToggle');
  9194. Foundation.plugin(Reveal, 'Reveal');
  9195. Foundation.plugin(Slider, 'Slider');
  9196. Foundation.plugin(SmoothScroll, 'SmoothScroll');
  9197. Foundation.plugin(Sticky, 'Sticky');
  9198. Foundation.plugin(Tabs, 'Tabs');
  9199. Foundation.plugin(Toggler, 'Toggler');
  9200. Foundation.plugin(Tooltip, 'Tooltip');
  9201. Foundation.plugin(ResponsiveAccordionTabs, 'ResponsiveAccordionTabs');
  9202. export default Foundation;
  9203. export { foundation_core_utils as CoreUtils, Foundation as Core, Foundation, Box, onImagesLoaded, Keyboard, MediaQuery, Motion, Move, Nest, Timer, Touch, Triggers, Abide, Accordion, AccordionMenu, Drilldown, Dropdown, DropdownMenu, Equalizer, Interchange, Magellan, OffCanvas, Orbit, ResponsiveMenu, ResponsiveToggle, Reveal, Slider, SmoothScroll, Sticky, Tabs, Toggler, Tooltip, ResponsiveAccordionTabs };
  9204. //# sourceMappingURL=foundation.es6.js.map