customize-controls.js 284 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270
  1. /**
  2. * @output wp-admin/js/customize-controls.js
  3. */
  4. /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */
  5. (function( exports, $ ){
  6. var Container, focus, normalizedTransitionendEventName, api = wp.customize;
  7. api.OverlayNotification = api.Notification.extend(/** @lends wp.customize.OverlayNotification.prototype */{
  8. /**
  9. * Whether the notification should show a loading spinner.
  10. *
  11. * @since 4.9.0
  12. * @var {boolean}
  13. */
  14. loading: false,
  15. /**
  16. * A notification that is displayed in a full-screen overlay.
  17. *
  18. * @constructs wp.customize.OverlayNotification
  19. * @augments wp.customize.Notification
  20. *
  21. * @since 4.9.0
  22. *
  23. * @param {string} code - Code.
  24. * @param {object} params - Params.
  25. */
  26. initialize: function( code, params ) {
  27. var notification = this;
  28. api.Notification.prototype.initialize.call( notification, code, params );
  29. notification.containerClasses += ' notification-overlay';
  30. if ( notification.loading ) {
  31. notification.containerClasses += ' notification-loading';
  32. }
  33. },
  34. /**
  35. * Render notification.
  36. *
  37. * @since 4.9.0
  38. *
  39. * @return {jQuery} Notification container.
  40. */
  41. render: function() {
  42. var li = api.Notification.prototype.render.call( this );
  43. li.on( 'keydown', _.bind( this.handleEscape, this ) );
  44. return li;
  45. },
  46. /**
  47. * Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
  48. *
  49. * @since 4.9.0
  50. *
  51. * @param {jQuery.Event} event - Event.
  52. * @returns {void}
  53. */
  54. handleEscape: function( event ) {
  55. var notification = this;
  56. if ( 27 === event.which ) {
  57. event.stopPropagation();
  58. if ( notification.dismissible && notification.parent ) {
  59. notification.parent.remove( notification.code );
  60. }
  61. }
  62. }
  63. });
  64. api.Notifications = api.Values.extend(/** @lends wp.customize.Notifications.prototype */{
  65. /**
  66. * Whether the alternative style should be used.
  67. *
  68. * @since 4.9.0
  69. * @type {boolean}
  70. */
  71. alt: false,
  72. /**
  73. * The default constructor for items of the collection.
  74. *
  75. * @since 4.9.0
  76. * @type {object}
  77. */
  78. defaultConstructor: api.Notification,
  79. /**
  80. * A collection of observable notifications.
  81. *
  82. * @since 4.9.0
  83. *
  84. * @constructs wp.customize.Notifications
  85. * @augments wp.customize.Values
  86. *
  87. * @param {object} options - Options.
  88. * @param {jQuery} [options.container] - Container element for notifications. This can be injected later.
  89. * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
  90. *
  91. * @returns {void}
  92. */
  93. initialize: function( options ) {
  94. var collection = this;
  95. api.Values.prototype.initialize.call( collection, options );
  96. _.bindAll( collection, 'constrainFocus' );
  97. // Keep track of the order in which the notifications were added for sorting purposes.
  98. collection._addedIncrement = 0;
  99. collection._addedOrder = {};
  100. // Trigger change event when notification is added or removed.
  101. collection.bind( 'add', function( notification ) {
  102. collection.trigger( 'change', notification );
  103. });
  104. collection.bind( 'removed', function( notification ) {
  105. collection.trigger( 'change', notification );
  106. });
  107. },
  108. /**
  109. * Get the number of notifications added.
  110. *
  111. * @since 4.9.0
  112. * @return {number} Count of notifications.
  113. */
  114. count: function() {
  115. return _.size( this._value );
  116. },
  117. /**
  118. * Add notification to the collection.
  119. *
  120. * @since 4.9.0
  121. *
  122. * @param {string|wp.customize.Notification} notification - Notification object to add. Alternatively code may be supplied, and in that case the second notificationObject argument must be supplied.
  123. * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string.
  124. * @returns {wp.customize.Notification} Added notification (or existing instance if it was already added).
  125. */
  126. add: function( notification, notificationObject ) {
  127. var collection = this, code, instance;
  128. if ( 'string' === typeof notification ) {
  129. code = notification;
  130. instance = notificationObject;
  131. } else {
  132. code = notification.code;
  133. instance = notification;
  134. }
  135. if ( ! collection.has( code ) ) {
  136. collection._addedIncrement += 1;
  137. collection._addedOrder[ code ] = collection._addedIncrement;
  138. }
  139. return api.Values.prototype.add.call( collection, code, instance );
  140. },
  141. /**
  142. * Add notification to the collection.
  143. *
  144. * @since 4.9.0
  145. * @param {string} code - Notification code to remove.
  146. * @return {api.Notification} Added instance (or existing instance if it was already added).
  147. */
  148. remove: function( code ) {
  149. var collection = this;
  150. delete collection._addedOrder[ code ];
  151. return api.Values.prototype.remove.call( this, code );
  152. },
  153. /**
  154. * Get list of notifications.
  155. *
  156. * Notifications may be sorted by type followed by added time.
  157. *
  158. * @since 4.9.0
  159. * @param {object} args - Args.
  160. * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
  161. * @return {Array.<wp.customize.Notification>} Notifications.
  162. */
  163. get: function( args ) {
  164. var collection = this, notifications, errorTypePriorities, params;
  165. notifications = _.values( collection._value );
  166. params = _.extend(
  167. { sort: false },
  168. args
  169. );
  170. if ( params.sort ) {
  171. errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
  172. notifications.sort( function( a, b ) {
  173. var aPriority = 0, bPriority = 0;
  174. if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
  175. aPriority = errorTypePriorities[ a.type ];
  176. }
  177. if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
  178. bPriority = errorTypePriorities[ b.type ];
  179. }
  180. if ( aPriority !== bPriority ) {
  181. return bPriority - aPriority; // Show errors first.
  182. }
  183. return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
  184. });
  185. }
  186. return notifications;
  187. },
  188. /**
  189. * Render notifications area.
  190. *
  191. * @since 4.9.0
  192. * @returns {void}
  193. */
  194. render: function() {
  195. var collection = this,
  196. notifications, hadOverlayNotification = false, hasOverlayNotification, overlayNotifications = [],
  197. previousNotificationsByCode = {},
  198. listElement, focusableElements;
  199. // Short-circuit if there are no container to render into.
  200. if ( ! collection.container || ! collection.container.length ) {
  201. return;
  202. }
  203. notifications = collection.get( { sort: true } );
  204. collection.container.toggle( 0 !== notifications.length );
  205. // Short-circuit if there are no changes to the notifications.
  206. if ( collection.container.is( collection.previousContainer ) && _.isEqual( notifications, collection.previousNotifications ) ) {
  207. return;
  208. }
  209. // Make sure list is part of the container.
  210. listElement = collection.container.children( 'ul' ).first();
  211. if ( ! listElement.length ) {
  212. listElement = $( '<ul></ul>' );
  213. collection.container.append( listElement );
  214. }
  215. // Remove all notifications prior to re-rendering.
  216. listElement.find( '> [data-code]' ).remove();
  217. _.each( collection.previousNotifications, function( notification ) {
  218. previousNotificationsByCode[ notification.code ] = notification;
  219. });
  220. // Add all notifications in the sorted order.
  221. _.each( notifications, function( notification ) {
  222. var notificationContainer;
  223. if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) {
  224. wp.a11y.speak( notification.message, 'assertive' );
  225. }
  226. notificationContainer = $( notification.render() );
  227. notification.container = notificationContainer;
  228. listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
  229. if ( notification.extended( api.OverlayNotification ) ) {
  230. overlayNotifications.push( notification );
  231. }
  232. });
  233. hasOverlayNotification = Boolean( overlayNotifications.length );
  234. if ( collection.previousNotifications ) {
  235. hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) {
  236. return notification.extended( api.OverlayNotification );
  237. } ) );
  238. }
  239. if ( hasOverlayNotification !== hadOverlayNotification ) {
  240. $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification );
  241. collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification );
  242. if ( hasOverlayNotification ) {
  243. collection.previousActiveElement = document.activeElement;
  244. $( document ).on( 'keydown', collection.constrainFocus );
  245. } else {
  246. $( document ).off( 'keydown', collection.constrainFocus );
  247. }
  248. }
  249. if ( hasOverlayNotification ) {
  250. collection.focusContainer = overlayNotifications[ overlayNotifications.length - 1 ].container;
  251. collection.focusContainer.prop( 'tabIndex', -1 );
  252. focusableElements = collection.focusContainer.find( ':focusable' );
  253. if ( focusableElements.length ) {
  254. focusableElements.first().focus();
  255. } else {
  256. collection.focusContainer.focus();
  257. }
  258. } else if ( collection.previousActiveElement ) {
  259. $( collection.previousActiveElement ).focus();
  260. collection.previousActiveElement = null;
  261. }
  262. collection.previousNotifications = notifications;
  263. collection.previousContainer = collection.container;
  264. collection.trigger( 'rendered' );
  265. },
  266. /**
  267. * Constrain focus on focus container.
  268. *
  269. * @since 4.9.0
  270. *
  271. * @param {jQuery.Event} event - Event.
  272. * @returns {void}
  273. */
  274. constrainFocus: function constrainFocus( event ) {
  275. var collection = this, focusableElements;
  276. // Prevent keys from escaping.
  277. event.stopPropagation();
  278. if ( 9 !== event.which ) { // Tab key.
  279. return;
  280. }
  281. focusableElements = collection.focusContainer.find( ':focusable' );
  282. if ( 0 === focusableElements.length ) {
  283. focusableElements = collection.focusContainer;
  284. }
  285. if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
  286. event.preventDefault();
  287. focusableElements.first().focus();
  288. } else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
  289. event.preventDefault();
  290. focusableElements.first().focus();
  291. } else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
  292. event.preventDefault();
  293. focusableElements.last().focus();
  294. }
  295. }
  296. });
  297. api.Setting = api.Value.extend(/** @lends wp.customize.Setting.prototype */{
  298. /**
  299. * Default params.
  300. *
  301. * @since 4.9.0
  302. * @var {object}
  303. */
  304. defaults: {
  305. transport: 'refresh',
  306. dirty: false
  307. },
  308. /**
  309. * A Customizer Setting.
  310. *
  311. * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
  312. * draft changes to in the Customizer.
  313. *
  314. * @see PHP class WP_Customize_Setting.
  315. *
  316. * @constructs wp.customize.Setting
  317. * @augments wp.customize.Value
  318. *
  319. * @since 3.4.0
  320. *
  321. * @param {string} id - The setting ID.
  322. * @param {*} value - The initial value of the setting.
  323. * @param {object} [options={}] - Options.
  324. * @param {string} [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
  325. * @param {boolean} [options.dirty=false] - Whether the setting should be considered initially dirty.
  326. * @param {object} [options.previewer] - The Previewer instance to sync with. Defaults to wp.customize.previewer.
  327. */
  328. initialize: function( id, value, options ) {
  329. var setting = this, params;
  330. params = _.extend(
  331. { previewer: api.previewer },
  332. setting.defaults,
  333. options || {}
  334. );
  335. api.Value.prototype.initialize.call( setting, value, params );
  336. setting.id = id;
  337. setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from.
  338. setting.notifications = new api.Notifications();
  339. // Whenever the setting's value changes, refresh the preview.
  340. setting.bind( setting.preview );
  341. },
  342. /**
  343. * Refresh the preview, respective of the setting's refresh policy.
  344. *
  345. * If the preview hasn't sent a keep-alive message and is likely
  346. * disconnected by having navigated to a non-allowed URL, then the
  347. * refresh transport will be forced when postMessage is the transport.
  348. * Note that postMessage does not throw an error when the recipient window
  349. * fails to match the origin window, so using try/catch around the
  350. * previewer.send() call to then fallback to refresh will not work.
  351. *
  352. * @since 3.4.0
  353. * @access public
  354. *
  355. * @returns {void}
  356. */
  357. preview: function() {
  358. var setting = this, transport;
  359. transport = setting.transport;
  360. if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
  361. transport = 'refresh';
  362. }
  363. if ( 'postMessage' === transport ) {
  364. setting.previewer.send( 'setting', [ setting.id, setting() ] );
  365. } else if ( 'refresh' === transport ) {
  366. setting.previewer.refresh();
  367. }
  368. },
  369. /**
  370. * Find controls associated with this setting.
  371. *
  372. * @since 4.6.0
  373. * @returns {wp.customize.Control[]} Controls associated with setting.
  374. */
  375. findControls: function() {
  376. var setting = this, controls = [];
  377. api.control.each( function( control ) {
  378. _.each( control.settings, function( controlSetting ) {
  379. if ( controlSetting.id === setting.id ) {
  380. controls.push( control );
  381. }
  382. } );
  383. } );
  384. return controls;
  385. }
  386. });
  387. /**
  388. * Current change count.
  389. *
  390. * @alias wp.customize._latestRevision
  391. *
  392. * @since 4.7.0
  393. * @type {number}
  394. * @protected
  395. */
  396. api._latestRevision = 0;
  397. /**
  398. * Last revision that was saved.
  399. *
  400. * @alias wp.customize._lastSavedRevision
  401. *
  402. * @since 4.7.0
  403. * @type {number}
  404. * @protected
  405. */
  406. api._lastSavedRevision = 0;
  407. /**
  408. * Latest revisions associated with the updated setting.
  409. *
  410. * @alias wp.customize._latestSettingRevisions
  411. *
  412. * @since 4.7.0
  413. * @type {object}
  414. * @protected
  415. */
  416. api._latestSettingRevisions = {};
  417. /*
  418. * Keep track of the revision associated with each updated setting so that
  419. * requestChangesetUpdate knows which dirty settings to include. Also, once
  420. * ready is triggered and all initial settings have been added, increment
  421. * revision for each newly-created initially-dirty setting so that it will
  422. * also be included in changeset update requests.
  423. */
  424. api.bind( 'change', function incrementChangedSettingRevision( setting ) {
  425. api._latestRevision += 1;
  426. api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  427. } );
  428. api.bind( 'ready', function() {
  429. api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
  430. if ( setting._dirty ) {
  431. api._latestRevision += 1;
  432. api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  433. }
  434. } );
  435. } );
  436. /**
  437. * Get the dirty setting values.
  438. *
  439. * @alias wp.customize.dirtyValues
  440. *
  441. * @since 4.7.0
  442. * @access public
  443. *
  444. * @param {object} [options] Options.
  445. * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
  446. * @returns {object} Dirty setting values.
  447. */
  448. api.dirtyValues = function dirtyValues( options ) {
  449. var values = {};
  450. api.each( function( setting ) {
  451. var settingRevision;
  452. if ( ! setting._dirty ) {
  453. return;
  454. }
  455. settingRevision = api._latestSettingRevisions[ setting.id ];
  456. // Skip including settings that have already been included in the changeset, if only requesting unsaved.
  457. if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
  458. return;
  459. }
  460. values[ setting.id ] = setting.get();
  461. } );
  462. return values;
  463. };
  464. /**
  465. * Request updates to the changeset.
  466. *
  467. * @alias wp.customize.requestChangesetUpdate
  468. *
  469. * @since 4.7.0
  470. * @access public
  471. *
  472. * @param {object} [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
  473. * If not provided, then the changes will still be obtained from unsaved dirty settings.
  474. * @param {object} [args] - Additional options for the save request.
  475. * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
  476. * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
  477. * @param {string} [args.title] - Title to update in the changeset. Optional.
  478. * @param {string} [args.date] - Date to update in the changeset. Optional.
  479. * @returns {jQuery.Promise} Promise resolving with the response data.
  480. */
  481. api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) {
  482. var deferred, request, submittedChanges = {}, data, submittedArgs;
  483. deferred = new $.Deferred();
  484. // Prevent attempting changeset update while request is being made.
  485. if ( 0 !== api.state( 'processing' ).get() ) {
  486. deferred.reject( 'already_processing' );
  487. return deferred.promise();
  488. }
  489. submittedArgs = _.extend( {
  490. title: null,
  491. date: null,
  492. autosave: false,
  493. force: false
  494. }, args );
  495. if ( changes ) {
  496. _.extend( submittedChanges, changes );
  497. }
  498. // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
  499. _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
  500. if ( ! changes || null !== changes[ settingId ] ) {
  501. submittedChanges[ settingId ] = _.extend(
  502. {},
  503. submittedChanges[ settingId ] || {},
  504. { value: dirtyValue }
  505. );
  506. }
  507. } );
  508. // Allow plugins to attach additional params to the settings.
  509. api.trigger( 'changeset-save', submittedChanges, submittedArgs );
  510. // Short-circuit when there are no pending changes.
  511. if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
  512. deferred.resolve( {} );
  513. return deferred.promise();
  514. }
  515. // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. Status is also disallowed for revisions regardless.
  516. if ( submittedArgs.status ) {
  517. return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
  518. }
  519. // Dates not beung allowed for revisions are is a technical limitation of post revisions.
  520. if ( submittedArgs.date && submittedArgs.autosave ) {
  521. return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise();
  522. }
  523. // Make sure that publishing a changeset waits for all changeset update requests to complete.
  524. api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  525. deferred.always( function() {
  526. api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  527. } );
  528. // Ensure that if any plugins add data to save requests by extending query() that they get included here.
  529. data = api.previewer.query( { excludeCustomizedSaved: true } );
  530. delete data.customized; // Being sent in customize_changeset_data instead.
  531. _.extend( data, {
  532. nonce: api.settings.nonce.save,
  533. customize_theme: api.settings.theme.stylesheet,
  534. customize_changeset_data: JSON.stringify( submittedChanges )
  535. } );
  536. if ( null !== submittedArgs.title ) {
  537. data.customize_changeset_title = submittedArgs.title;
  538. }
  539. if ( null !== submittedArgs.date ) {
  540. data.customize_changeset_date = submittedArgs.date;
  541. }
  542. if ( false !== submittedArgs.autosave ) {
  543. data.customize_changeset_autosave = 'true';
  544. }
  545. // Allow plugins to modify the params included with the save request.
  546. api.trigger( 'save-request-params', data );
  547. request = wp.ajax.post( 'customize_save', data );
  548. request.done( function requestChangesetUpdateDone( data ) {
  549. var savedChangesetValues = {};
  550. // Ensure that all settings updated subsequently will be included in the next changeset update request.
  551. api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
  552. api.state( 'changesetStatus' ).set( data.changeset_status );
  553. if ( data.changeset_date ) {
  554. api.state( 'changesetDate' ).set( data.changeset_date );
  555. }
  556. deferred.resolve( data );
  557. api.trigger( 'changeset-saved', data );
  558. if ( data.setting_validities ) {
  559. _.each( data.setting_validities, function( validity, settingId ) {
  560. if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
  561. savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
  562. }
  563. } );
  564. }
  565. api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
  566. } );
  567. request.fail( function requestChangesetUpdateFail( data ) {
  568. deferred.reject( data );
  569. api.trigger( 'changeset-error', data );
  570. } );
  571. request.always( function( data ) {
  572. if ( data.setting_validities ) {
  573. api._handleSettingValidities( {
  574. settingValidities: data.setting_validities
  575. } );
  576. }
  577. } );
  578. return deferred.promise();
  579. };
  580. /**
  581. * Watch all changes to Value properties, and bubble changes to parent Values instance
  582. *
  583. * @alias wp.customize.utils.bubbleChildValueChanges
  584. *
  585. * @since 4.1.0
  586. *
  587. * @param {wp.customize.Class} instance
  588. * @param {Array} properties The names of the Value instances to watch.
  589. */
  590. api.utils.bubbleChildValueChanges = function ( instance, properties ) {
  591. $.each( properties, function ( i, key ) {
  592. instance[ key ].bind( function ( to, from ) {
  593. if ( instance.parent && to !== from ) {
  594. instance.parent.trigger( 'change', instance );
  595. }
  596. } );
  597. } );
  598. };
  599. /**
  600. * Expand a panel, section, or control and focus on the first focusable element.
  601. *
  602. * @alias wp.customize~focus
  603. *
  604. * @since 4.1.0
  605. *
  606. * @param {Object} [params]
  607. * @param {Function} [params.completeCallback]
  608. */
  609. focus = function ( params ) {
  610. var construct, completeCallback, focus, focusElement;
  611. construct = this;
  612. params = params || {};
  613. focus = function () {
  614. var focusContainer;
  615. if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
  616. focusContainer = construct.contentContainer;
  617. } else {
  618. focusContainer = construct.container;
  619. }
  620. focusElement = focusContainer.find( '.control-focus:first' );
  621. if ( 0 === focusElement.length ) {
  622. // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
  623. focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
  624. }
  625. focusElement.focus();
  626. };
  627. if ( params.completeCallback ) {
  628. completeCallback = params.completeCallback;
  629. params.completeCallback = function () {
  630. focus();
  631. completeCallback();
  632. };
  633. } else {
  634. params.completeCallback = focus;
  635. }
  636. api.state( 'paneVisible' ).set( true );
  637. if ( construct.expand ) {
  638. construct.expand( params );
  639. } else {
  640. params.completeCallback();
  641. }
  642. };
  643. /**
  644. * Stable sort for Panels, Sections, and Controls.
  645. *
  646. * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
  647. *
  648. * @alias wp.customize.utils.prioritySort
  649. *
  650. * @since 4.1.0
  651. *
  652. * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
  653. * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
  654. * @returns {Number}
  655. */
  656. api.utils.prioritySort = function ( a, b ) {
  657. if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
  658. return a.params.instanceNumber - b.params.instanceNumber;
  659. } else {
  660. return a.priority() - b.priority();
  661. }
  662. };
  663. /**
  664. * Return whether the supplied Event object is for a keydown event but not the Enter key.
  665. *
  666. * @alias wp.customize.utils.isKeydownButNotEnterEvent
  667. *
  668. * @since 4.1.0
  669. *
  670. * @param {jQuery.Event} event
  671. * @returns {boolean}
  672. */
  673. api.utils.isKeydownButNotEnterEvent = function ( event ) {
  674. return ( 'keydown' === event.type && 13 !== event.which );
  675. };
  676. /**
  677. * Return whether the two lists of elements are the same and are in the same order.
  678. *
  679. * @alias wp.customize.utils.areElementListsEqual
  680. *
  681. * @since 4.1.0
  682. *
  683. * @param {Array|jQuery} listA
  684. * @param {Array|jQuery} listB
  685. * @returns {boolean}
  686. */
  687. api.utils.areElementListsEqual = function ( listA, listB ) {
  688. var equal = (
  689. listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
  690. -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
  691. _.zip( listA, listB ), // pair up each element between the two lists
  692. function ( pair ) {
  693. return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
  694. }
  695. ), false ) // check for presence of false in map's return value
  696. );
  697. return equal;
  698. };
  699. /**
  700. * Highlight the existence of a button.
  701. *
  702. * This function reminds the user of a button represented by the specified
  703. * UI element, after an optional delay. If the user focuses the element
  704. * before the delay passes, the reminder is canceled.
  705. *
  706. * @alias wp.customize.utils.highlightButton
  707. *
  708. * @since 4.9.0
  709. *
  710. * @param {jQuery} button - The element to highlight.
  711. * @param {object} [options] - Options.
  712. * @param {number} [options.delay=0] - Delay in milliseconds.
  713. * @param {jQuery} [options.focusTarget] - A target for user focus that defaults to the highlighted element.
  714. * If the user focuses the target before the delay passes, the reminder
  715. * is canceled. This option exists to accommodate compound buttons
  716. * containing auxiliary UI, such as the Publish button augmented with a
  717. * Settings button.
  718. * @returns {Function} An idempotent function that cancels the reminder.
  719. */
  720. api.utils.highlightButton = function highlightButton( button, options ) {
  721. var animationClass = 'button-see-me',
  722. canceled = false,
  723. params;
  724. params = _.extend(
  725. {
  726. delay: 0,
  727. focusTarget: button
  728. },
  729. options
  730. );
  731. function cancelReminder() {
  732. canceled = true;
  733. }
  734. params.focusTarget.on( 'focusin', cancelReminder );
  735. setTimeout( function() {
  736. params.focusTarget.off( 'focusin', cancelReminder );
  737. if ( ! canceled ) {
  738. button.addClass( animationClass );
  739. button.one( 'animationend', function() {
  740. /*
  741. * Remove animation class to avoid situations in Customizer where
  742. * DOM nodes are moved (re-inserted) and the animation repeats.
  743. */
  744. button.removeClass( animationClass );
  745. } );
  746. }
  747. }, params.delay );
  748. return cancelReminder;
  749. };
  750. /**
  751. * Get current timestamp adjusted for server clock time.
  752. *
  753. * Same functionality as the `current_time( 'mysql', false )` function in PHP.
  754. *
  755. * @alias wp.customize.utils.getCurrentTimestamp
  756. *
  757. * @since 4.9.0
  758. *
  759. * @returns {int} Current timestamp.
  760. */
  761. api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
  762. var currentDate, currentClientTimestamp, timestampDifferential;
  763. currentClientTimestamp = _.now();
  764. currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
  765. timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
  766. timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
  767. currentDate.setTime( currentDate.getTime() + timestampDifferential );
  768. return currentDate.getTime();
  769. };
  770. /**
  771. * Get remaining time of when the date is set.
  772. *
  773. * @alias wp.customize.utils.getRemainingTime
  774. *
  775. * @since 4.9.0
  776. *
  777. * @param {string|int|Date} datetime - Date time or timestamp of the future date.
  778. * @return {int} remainingTime - Remaining time in milliseconds.
  779. */
  780. api.utils.getRemainingTime = function getRemainingTime( datetime ) {
  781. var millisecondsDivider = 1000, remainingTime, timestamp;
  782. if ( datetime instanceof Date ) {
  783. timestamp = datetime.getTime();
  784. } else if ( 'string' === typeof datetime ) {
  785. timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
  786. } else {
  787. timestamp = datetime;
  788. }
  789. remainingTime = timestamp - api.utils.getCurrentTimestamp();
  790. remainingTime = Math.ceil( remainingTime / millisecondsDivider );
  791. return remainingTime;
  792. };
  793. /**
  794. * Return browser supported `transitionend` event name.
  795. *
  796. * @since 4.7.0
  797. *
  798. * @ignore
  799. *
  800. * @returns {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
  801. */
  802. normalizedTransitionendEventName = (function () {
  803. var el, transitions, prop;
  804. el = document.createElement( 'div' );
  805. transitions = {
  806. 'transition' : 'transitionend',
  807. 'OTransition' : 'oTransitionEnd',
  808. 'MozTransition' : 'transitionend',
  809. 'WebkitTransition': 'webkitTransitionEnd'
  810. };
  811. prop = _.find( _.keys( transitions ), function( prop ) {
  812. return ! _.isUndefined( el.style[ prop ] );
  813. } );
  814. if ( prop ) {
  815. return transitions[ prop ];
  816. } else {
  817. return null;
  818. }
  819. })();
  820. Container = api.Class.extend(/** @lends wp.customize~Container.prototype */{
  821. defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  822. defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
  823. containerType: 'container',
  824. defaults: {
  825. title: '',
  826. description: '',
  827. priority: 100,
  828. type: 'default',
  829. content: null,
  830. active: true,
  831. instanceNumber: null
  832. },
  833. /**
  834. * Base class for Panel and Section.
  835. *
  836. * @constructs wp.customize~Container
  837. * @augments wp.customize.Class
  838. *
  839. * @since 4.1.0
  840. *
  841. * @borrows wp.customize~focus as focus
  842. *
  843. * @param {string} id - The ID for the container.
  844. * @param {object} options - Object containing one property: params.
  845. * @param {string} options.title - Title shown when panel is collapsed and expanded.
  846. * @param {string} [options.description] - Description shown at the top of the panel.
  847. * @param {number} [options.priority=100] - The sort priority for the panel.
  848. * @param {string} [options.templateId] - Template selector for container.
  849. * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
  850. * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
  851. * @param {boolean} [options.active=true] - Whether the panel is active or not.
  852. * @param {object} [options.params] - Deprecated wrapper for the above properties.
  853. */
  854. initialize: function ( id, options ) {
  855. var container = this;
  856. container.id = id;
  857. if ( ! Container.instanceCounter ) {
  858. Container.instanceCounter = 0;
  859. }
  860. Container.instanceCounter++;
  861. $.extend( container, {
  862. params: _.defaults(
  863. options.params || options, // Passing the params is deprecated.
  864. container.defaults
  865. )
  866. } );
  867. if ( ! container.params.instanceNumber ) {
  868. container.params.instanceNumber = Container.instanceCounter;
  869. }
  870. container.notifications = new api.Notifications();
  871. container.templateSelector = container.params.templateId || 'customize-' + container.containerType + '-' + container.params.type;
  872. container.container = $( container.params.content );
  873. if ( 0 === container.container.length ) {
  874. container.container = $( container.getContainer() );
  875. }
  876. container.headContainer = container.container;
  877. container.contentContainer = container.getContent();
  878. container.container = container.container.add( container.contentContainer );
  879. container.deferred = {
  880. embedded: new $.Deferred()
  881. };
  882. container.priority = new api.Value();
  883. container.active = new api.Value();
  884. container.activeArgumentsQueue = [];
  885. container.expanded = new api.Value();
  886. container.expandedArgumentsQueue = [];
  887. container.active.bind( function ( active ) {
  888. var args = container.activeArgumentsQueue.shift();
  889. args = $.extend( {}, container.defaultActiveArguments, args );
  890. active = ( active && container.isContextuallyActive() );
  891. container.onChangeActive( active, args );
  892. });
  893. container.expanded.bind( function ( expanded ) {
  894. var args = container.expandedArgumentsQueue.shift();
  895. args = $.extend( {}, container.defaultExpandedArguments, args );
  896. container.onChangeExpanded( expanded, args );
  897. });
  898. container.deferred.embedded.done( function () {
  899. container.setupNotifications();
  900. container.attachEvents();
  901. });
  902. api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
  903. container.priority.set( container.params.priority );
  904. container.active.set( container.params.active );
  905. container.expanded.set( false );
  906. },
  907. /**
  908. * Get the element that will contain the notifications.
  909. *
  910. * @since 4.9.0
  911. * @returns {jQuery} Notification container element.
  912. */
  913. getNotificationsContainerElement: function() {
  914. var container = this;
  915. return container.contentContainer.find( '.customize-control-notifications-container:first' );
  916. },
  917. /**
  918. * Set up notifications.
  919. *
  920. * @since 4.9.0
  921. * @returns {void}
  922. */
  923. setupNotifications: function() {
  924. var container = this, renderNotifications;
  925. container.notifications.container = container.getNotificationsContainerElement();
  926. // Render notifications when they change and when the construct is expanded.
  927. renderNotifications = function() {
  928. if ( container.expanded.get() ) {
  929. container.notifications.render();
  930. }
  931. };
  932. container.expanded.bind( renderNotifications );
  933. renderNotifications();
  934. container.notifications.bind( 'change', _.debounce( renderNotifications ) );
  935. },
  936. /**
  937. * @since 4.1.0
  938. *
  939. * @abstract
  940. */
  941. ready: function() {},
  942. /**
  943. * Get the child models associated with this parent, sorting them by their priority Value.
  944. *
  945. * @since 4.1.0
  946. *
  947. * @param {String} parentType
  948. * @param {String} childType
  949. * @returns {Array}
  950. */
  951. _children: function ( parentType, childType ) {
  952. var parent = this,
  953. children = [];
  954. api[ childType ].each( function ( child ) {
  955. if ( child[ parentType ].get() === parent.id ) {
  956. children.push( child );
  957. }
  958. } );
  959. children.sort( api.utils.prioritySort );
  960. return children;
  961. },
  962. /**
  963. * To override by subclass, to return whether the container has active children.
  964. *
  965. * @since 4.1.0
  966. *
  967. * @abstract
  968. */
  969. isContextuallyActive: function () {
  970. throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
  971. },
  972. /**
  973. * Active state change handler.
  974. *
  975. * Shows the container if it is active, hides it if not.
  976. *
  977. * To override by subclass, update the container's UI to reflect the provided active state.
  978. *
  979. * @since 4.1.0
  980. *
  981. * @param {boolean} active - The active state to transiution to.
  982. * @param {Object} [args] - Args.
  983. * @param {Object} [args.duration] - The duration for the slideUp/slideDown animation.
  984. * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
  985. * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
  986. */
  987. onChangeActive: function( active, args ) {
  988. var construct = this,
  989. headContainer = construct.headContainer,
  990. duration, expandedOtherPanel;
  991. if ( args.unchanged ) {
  992. if ( args.completeCallback ) {
  993. args.completeCallback();
  994. }
  995. return;
  996. }
  997. duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
  998. if ( construct.extended( api.Panel ) ) {
  999. // If this is a panel is not currently expanded but another panel is expanded, do not animate.
  1000. api.panel.each(function ( panel ) {
  1001. if ( panel !== construct && panel.expanded() ) {
  1002. expandedOtherPanel = panel;
  1003. duration = 0;
  1004. }
  1005. });
  1006. // Collapse any expanded sections inside of this panel first before deactivating.
  1007. if ( ! active ) {
  1008. _.each( construct.sections(), function( section ) {
  1009. section.collapse( { duration: 0 } );
  1010. } );
  1011. }
  1012. }
  1013. if ( ! $.contains( document, headContainer.get( 0 ) ) ) {
  1014. // If the element is not in the DOM, then jQuery.fn.slideUp() does nothing. In this case, a hard toggle is required instead.
  1015. headContainer.toggle( active );
  1016. if ( args.completeCallback ) {
  1017. args.completeCallback();
  1018. }
  1019. } else if ( active ) {
  1020. headContainer.slideDown( duration, args.completeCallback );
  1021. } else {
  1022. if ( construct.expanded() ) {
  1023. construct.collapse({
  1024. duration: duration,
  1025. completeCallback: function() {
  1026. headContainer.slideUp( duration, args.completeCallback );
  1027. }
  1028. });
  1029. } else {
  1030. headContainer.slideUp( duration, args.completeCallback );
  1031. }
  1032. }
  1033. },
  1034. /**
  1035. * @since 4.1.0
  1036. *
  1037. * @params {Boolean} active
  1038. * @param {Object} [params]
  1039. * @returns {Boolean} false if state already applied
  1040. */
  1041. _toggleActive: function ( active, params ) {
  1042. var self = this;
  1043. params = params || {};
  1044. if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
  1045. params.unchanged = true;
  1046. self.onChangeActive( self.active.get(), params );
  1047. return false;
  1048. } else {
  1049. params.unchanged = false;
  1050. this.activeArgumentsQueue.push( params );
  1051. this.active.set( active );
  1052. return true;
  1053. }
  1054. },
  1055. /**
  1056. * @param {Object} [params]
  1057. * @returns {Boolean} false if already active
  1058. */
  1059. activate: function ( params ) {
  1060. return this._toggleActive( true, params );
  1061. },
  1062. /**
  1063. * @param {Object} [params]
  1064. * @returns {Boolean} false if already inactive
  1065. */
  1066. deactivate: function ( params ) {
  1067. return this._toggleActive( false, params );
  1068. },
  1069. /**
  1070. * To override by subclass, update the container's UI to reflect the provided active state.
  1071. * @abstract
  1072. */
  1073. onChangeExpanded: function () {
  1074. throw new Error( 'Must override with subclass.' );
  1075. },
  1076. /**
  1077. * Handle the toggle logic for expand/collapse.
  1078. *
  1079. * @param {Boolean} expanded - The new state to apply.
  1080. * @param {Object} [params] - Object containing options for expand/collapse.
  1081. * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
  1082. * @returns {Boolean} false if state already applied or active state is false
  1083. */
  1084. _toggleExpanded: function( expanded, params ) {
  1085. var instance = this, previousCompleteCallback;
  1086. params = params || {};
  1087. previousCompleteCallback = params.completeCallback;
  1088. // Short-circuit expand() if the instance is not active.
  1089. if ( expanded && ! instance.active() ) {
  1090. return false;
  1091. }
  1092. api.state( 'paneVisible' ).set( true );
  1093. params.completeCallback = function() {
  1094. if ( previousCompleteCallback ) {
  1095. previousCompleteCallback.apply( instance, arguments );
  1096. }
  1097. if ( expanded ) {
  1098. instance.container.trigger( 'expanded' );
  1099. } else {
  1100. instance.container.trigger( 'collapsed' );
  1101. }
  1102. };
  1103. if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
  1104. params.unchanged = true;
  1105. instance.onChangeExpanded( instance.expanded.get(), params );
  1106. return false;
  1107. } else {
  1108. params.unchanged = false;
  1109. instance.expandedArgumentsQueue.push( params );
  1110. instance.expanded.set( expanded );
  1111. return true;
  1112. }
  1113. },
  1114. /**
  1115. * @param {Object} [params]
  1116. * @returns {Boolean} false if already expanded or if inactive.
  1117. */
  1118. expand: function ( params ) {
  1119. return this._toggleExpanded( true, params );
  1120. },
  1121. /**
  1122. * @param {Object} [params]
  1123. * @returns {Boolean} false if already collapsed.
  1124. */
  1125. collapse: function ( params ) {
  1126. return this._toggleExpanded( false, params );
  1127. },
  1128. /**
  1129. * Animate container state change if transitions are supported by the browser.
  1130. *
  1131. * @since 4.7.0
  1132. * @private
  1133. *
  1134. * @param {function} completeCallback Function to be called after transition is completed.
  1135. * @returns {void}
  1136. */
  1137. _animateChangeExpanded: function( completeCallback ) {
  1138. // Return if CSS transitions are not supported.
  1139. if ( ! normalizedTransitionendEventName ) {
  1140. if ( completeCallback ) {
  1141. completeCallback();
  1142. }
  1143. return;
  1144. }
  1145. var construct = this,
  1146. content = construct.contentContainer,
  1147. overlay = content.closest( '.wp-full-overlay' ),
  1148. elements, transitionEndCallback, transitionParentPane;
  1149. // Determine set of elements that are affected by the animation.
  1150. elements = overlay.add( content );
  1151. if ( ! construct.panel || '' === construct.panel() ) {
  1152. transitionParentPane = true;
  1153. } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) {
  1154. transitionParentPane = true;
  1155. } else {
  1156. transitionParentPane = false;
  1157. }
  1158. if ( transitionParentPane ) {
  1159. elements = elements.add( '#customize-info, .customize-pane-parent' );
  1160. }
  1161. // Handle `transitionEnd` event.
  1162. transitionEndCallback = function( e ) {
  1163. if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
  1164. return;
  1165. }
  1166. content.off( normalizedTransitionendEventName, transitionEndCallback );
  1167. elements.removeClass( 'busy' );
  1168. if ( completeCallback ) {
  1169. completeCallback();
  1170. }
  1171. };
  1172. content.on( normalizedTransitionendEventName, transitionEndCallback );
  1173. elements.addClass( 'busy' );
  1174. // Prevent screen flicker when pane has been scrolled before expanding.
  1175. _.defer( function() {
  1176. var container = content.closest( '.wp-full-overlay-sidebar-content' ),
  1177. currentScrollTop = container.scrollTop(),
  1178. previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
  1179. expanded = construct.expanded();
  1180. if ( expanded && 0 < currentScrollTop ) {
  1181. content.css( 'top', currentScrollTop + 'px' );
  1182. content.data( 'previous-scrollTop', currentScrollTop );
  1183. } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
  1184. content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
  1185. container.scrollTop( previousScrollTop );
  1186. }
  1187. } );
  1188. },
  1189. /*
  1190. * is documented using @borrows in the constructor.
  1191. */
  1192. focus: focus,
  1193. /**
  1194. * Return the container html, generated from its JS template, if it exists.
  1195. *
  1196. * @since 4.3.0
  1197. */
  1198. getContainer: function () {
  1199. var template,
  1200. container = this;
  1201. if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
  1202. template = wp.template( container.templateSelector );
  1203. } else {
  1204. template = wp.template( 'customize-' + container.containerType + '-default' );
  1205. }
  1206. if ( template && container.container ) {
  1207. return $.trim( template( _.extend(
  1208. { id: container.id },
  1209. container.params
  1210. ) ) );
  1211. }
  1212. return '<li></li>';
  1213. },
  1214. /**
  1215. * Find content element which is displayed when the section is expanded.
  1216. *
  1217. * After a construct is initialized, the return value will be available via the `contentContainer` property.
  1218. * By default the element will be related it to the parent container with `aria-owns` and detached.
  1219. * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
  1220. * just return the content element without needing to add the `aria-owns` element or detach it from
  1221. * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
  1222. * method to handle animating the panel/section into and out of view.
  1223. *
  1224. * @since 4.7.0
  1225. * @access public
  1226. *
  1227. * @returns {jQuery} Detached content element.
  1228. */
  1229. getContent: function() {
  1230. var construct = this,
  1231. container = construct.container,
  1232. content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
  1233. contentId = 'sub-' + container.attr( 'id' ),
  1234. ownedElements = contentId,
  1235. alreadyOwnedElements = container.attr( 'aria-owns' );
  1236. if ( alreadyOwnedElements ) {
  1237. ownedElements = ownedElements + ' ' + alreadyOwnedElements;
  1238. }
  1239. container.attr( 'aria-owns', ownedElements );
  1240. return content.detach().attr( {
  1241. 'id': contentId,
  1242. 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
  1243. } );
  1244. }
  1245. });
  1246. api.Section = Container.extend(/** @lends wp.customize.Section.prototype */{
  1247. containerType: 'section',
  1248. containerParent: '#customize-theme-controls',
  1249. containerPaneParent: '.customize-pane-parent',
  1250. defaults: {
  1251. title: '',
  1252. description: '',
  1253. priority: 100,
  1254. type: 'default',
  1255. content: null,
  1256. active: true,
  1257. instanceNumber: null,
  1258. panel: null,
  1259. customizeAction: ''
  1260. },
  1261. /**
  1262. * @constructs wp.customize.Section
  1263. * @augments wp.customize~Container
  1264. *
  1265. * @since 4.1.0
  1266. *
  1267. * @param {string} id - The ID for the section.
  1268. * @param {object} options - Options.
  1269. * @param {string} options.title - Title shown when section is collapsed and expanded.
  1270. * @param {string} [options.description] - Description shown at the top of the section.
  1271. * @param {number} [options.priority=100] - The sort priority for the section.
  1272. * @param {string} [options.type=default] - The type of the section. See wp.customize.sectionConstructor.
  1273. * @param {string} [options.content] - The markup to be used for the section container. If empty, a JS template is used.
  1274. * @param {boolean} [options.active=true] - Whether the section is active or not.
  1275. * @param {string} options.panel - The ID for the panel this section is associated with.
  1276. * @param {string} [options.customizeAction] - Additional context information shown before the section title when expanded.
  1277. * @param {object} [options.params] - Deprecated wrapper for the above properties.
  1278. */
  1279. initialize: function ( id, options ) {
  1280. var section = this, params;
  1281. params = options.params || options;
  1282. // Look up the type if one was not supplied.
  1283. if ( ! params.type ) {
  1284. _.find( api.sectionConstructor, function( Constructor, type ) {
  1285. if ( Constructor === section.constructor ) {
  1286. params.type = type;
  1287. return true;
  1288. }
  1289. return false;
  1290. } );
  1291. }
  1292. Container.prototype.initialize.call( section, id, params );
  1293. section.id = id;
  1294. section.panel = new api.Value();
  1295. section.panel.bind( function ( id ) {
  1296. $( section.headContainer ).toggleClass( 'control-subsection', !! id );
  1297. });
  1298. section.panel.set( section.params.panel || '' );
  1299. api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
  1300. section.embed();
  1301. section.deferred.embedded.done( function () {
  1302. section.ready();
  1303. });
  1304. },
  1305. /**
  1306. * Embed the container in the DOM when any parent panel is ready.
  1307. *
  1308. * @since 4.1.0
  1309. */
  1310. embed: function () {
  1311. var inject,
  1312. section = this;
  1313. section.containerParent = api.ensure( section.containerParent );
  1314. // Watch for changes to the panel state.
  1315. inject = function ( panelId ) {
  1316. var parentContainer;
  1317. if ( panelId ) {
  1318. // The panel has been supplied, so wait until the panel object is registered.
  1319. api.panel( panelId, function ( panel ) {
  1320. // The panel has been registered, wait for it to become ready/initialized.
  1321. panel.deferred.embedded.done( function () {
  1322. parentContainer = panel.contentContainer;
  1323. if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1324. parentContainer.append( section.headContainer );
  1325. }
  1326. if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1327. section.containerParent.append( section.contentContainer );
  1328. }
  1329. section.deferred.embedded.resolve();
  1330. });
  1331. } );
  1332. } else {
  1333. // There is no panel, so embed the section in the root of the customizer
  1334. parentContainer = api.ensure( section.containerPaneParent );
  1335. if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1336. parentContainer.append( section.headContainer );
  1337. }
  1338. if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1339. section.containerParent.append( section.contentContainer );
  1340. }
  1341. section.deferred.embedded.resolve();
  1342. }
  1343. };
  1344. section.panel.bind( inject );
  1345. inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
  1346. },
  1347. /**
  1348. * Add behaviors for the accordion section.
  1349. *
  1350. * @since 4.1.0
  1351. */
  1352. attachEvents: function () {
  1353. var meta, content, section = this;
  1354. if ( section.container.hasClass( 'cannot-expand' ) ) {
  1355. return;
  1356. }
  1357. // Expand/Collapse accordion sections on click.
  1358. section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
  1359. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1360. return;
  1361. }
  1362. event.preventDefault(); // Keep this AFTER the key filter above
  1363. if ( section.expanded() ) {
  1364. section.collapse();
  1365. } else {
  1366. section.expand();
  1367. }
  1368. });
  1369. // This is very similar to what is found for api.Panel.attachEvents().
  1370. section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
  1371. meta = section.container.find( '.section-meta' );
  1372. if ( meta.hasClass( 'cannot-expand' ) ) {
  1373. return;
  1374. }
  1375. content = meta.find( '.customize-section-description:first' );
  1376. content.toggleClass( 'open' );
  1377. content.slideToggle( section.defaultExpandedArguments.duration, function() {
  1378. content.trigger( 'toggled' );
  1379. } );
  1380. $( this ).attr( 'aria-expanded', function( i, attr ) {
  1381. return 'true' === attr ? 'false' : 'true';
  1382. });
  1383. });
  1384. },
  1385. /**
  1386. * Return whether this section has any active controls.
  1387. *
  1388. * @since 4.1.0
  1389. *
  1390. * @returns {Boolean}
  1391. */
  1392. isContextuallyActive: function () {
  1393. var section = this,
  1394. controls = section.controls(),
  1395. activeCount = 0;
  1396. _( controls ).each( function ( control ) {
  1397. if ( control.active() ) {
  1398. activeCount += 1;
  1399. }
  1400. } );
  1401. return ( activeCount !== 0 );
  1402. },
  1403. /**
  1404. * Get the controls that are associated with this section, sorted by their priority Value.
  1405. *
  1406. * @since 4.1.0
  1407. *
  1408. * @returns {Array}
  1409. */
  1410. controls: function () {
  1411. return this._children( 'section', 'control' );
  1412. },
  1413. /**
  1414. * Update UI to reflect expanded state.
  1415. *
  1416. * @since 4.1.0
  1417. *
  1418. * @param {Boolean} expanded
  1419. * @param {Object} args
  1420. */
  1421. onChangeExpanded: function ( expanded, args ) {
  1422. var section = this,
  1423. container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
  1424. content = section.contentContainer,
  1425. overlay = section.headContainer.closest( '.wp-full-overlay' ),
  1426. backBtn = content.find( '.customize-section-back' ),
  1427. sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
  1428. expand, panel;
  1429. if ( expanded && ! content.hasClass( 'open' ) ) {
  1430. if ( args.unchanged ) {
  1431. expand = args.completeCallback;
  1432. } else {
  1433. expand = $.proxy( function() {
  1434. section._animateChangeExpanded( function() {
  1435. sectionTitle.attr( 'tabindex', '-1' );
  1436. backBtn.attr( 'tabindex', '0' );
  1437. backBtn.focus();
  1438. content.css( 'top', '' );
  1439. container.scrollTop( 0 );
  1440. if ( args.completeCallback ) {
  1441. args.completeCallback();
  1442. }
  1443. } );
  1444. content.addClass( 'open' );
  1445. overlay.addClass( 'section-open' );
  1446. api.state( 'expandedSection' ).set( section );
  1447. }, this );
  1448. }
  1449. if ( ! args.allowMultiple ) {
  1450. api.section.each( function ( otherSection ) {
  1451. if ( otherSection !== section ) {
  1452. otherSection.collapse( { duration: args.duration } );
  1453. }
  1454. });
  1455. }
  1456. if ( section.panel() ) {
  1457. api.panel( section.panel() ).expand({
  1458. duration: args.duration,
  1459. completeCallback: expand
  1460. });
  1461. } else {
  1462. if ( ! args.allowMultiple ) {
  1463. api.panel.each( function( panel ) {
  1464. panel.collapse();
  1465. });
  1466. }
  1467. expand();
  1468. }
  1469. } else if ( ! expanded && content.hasClass( 'open' ) ) {
  1470. if ( section.panel() ) {
  1471. panel = api.panel( section.panel() );
  1472. if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
  1473. panel.collapse();
  1474. }
  1475. }
  1476. section._animateChangeExpanded( function() {
  1477. backBtn.attr( 'tabindex', '-1' );
  1478. sectionTitle.attr( 'tabindex', '0' );
  1479. sectionTitle.focus();
  1480. content.css( 'top', '' );
  1481. if ( args.completeCallback ) {
  1482. args.completeCallback();
  1483. }
  1484. } );
  1485. content.removeClass( 'open' );
  1486. overlay.removeClass( 'section-open' );
  1487. if ( section === api.state( 'expandedSection' ).get() ) {
  1488. api.state( 'expandedSection' ).set( false );
  1489. }
  1490. } else {
  1491. if ( args.completeCallback ) {
  1492. args.completeCallback();
  1493. }
  1494. }
  1495. }
  1496. });
  1497. api.ThemesSection = api.Section.extend(/** @lends wp.customize.ThemesSection.prototype */{
  1498. currentTheme: '',
  1499. overlay: '',
  1500. template: '',
  1501. screenshotQueue: null,
  1502. $window: null,
  1503. $body: null,
  1504. loaded: 0,
  1505. loading: false,
  1506. fullyLoaded: false,
  1507. term: '',
  1508. tags: '',
  1509. nextTerm: '',
  1510. nextTags: '',
  1511. filtersHeight: 0,
  1512. headerContainer: null,
  1513. updateCountDebounced: null,
  1514. /**
  1515. * wp.customize.ThemesSection
  1516. *
  1517. * Custom section for themes that loads themes by category, and also
  1518. * handles the theme-details view rendering and navigation.
  1519. *
  1520. * @constructs wp.customize.ThemesSection
  1521. * @augments wp.customize.Section
  1522. *
  1523. * @since 4.9.0
  1524. *
  1525. * @param {string} id - ID.
  1526. * @param {object} options - Options.
  1527. * @returns {void}
  1528. */
  1529. initialize: function( id, options ) {
  1530. var section = this;
  1531. section.headerContainer = $();
  1532. section.$window = $( window );
  1533. section.$body = $( document.body );
  1534. api.Section.prototype.initialize.call( section, id, options );
  1535. section.updateCountDebounced = _.debounce( section.updateCount, 500 );
  1536. },
  1537. /**
  1538. * Embed the section in the DOM when the themes panel is ready.
  1539. *
  1540. * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
  1541. *
  1542. * @since 4.9.0
  1543. */
  1544. embed: function() {
  1545. var inject,
  1546. section = this;
  1547. // Watch for changes to the panel state
  1548. inject = function( panelId ) {
  1549. var parentContainer;
  1550. api.panel( panelId, function( panel ) {
  1551. // The panel has been registered, wait for it to become ready/initialized
  1552. panel.deferred.embedded.done( function() {
  1553. parentContainer = panel.contentContainer;
  1554. if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1555. parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
  1556. }
  1557. if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1558. section.containerParent.append( section.contentContainer );
  1559. }
  1560. section.deferred.embedded.resolve();
  1561. });
  1562. } );
  1563. };
  1564. section.panel.bind( inject );
  1565. inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
  1566. },
  1567. /**
  1568. * Set up.
  1569. *
  1570. * @since 4.2.0
  1571. *
  1572. * @returns {void}
  1573. */
  1574. ready: function() {
  1575. var section = this;
  1576. section.overlay = section.container.find( '.theme-overlay' );
  1577. section.template = wp.template( 'customize-themes-details-view' );
  1578. // Bind global keyboard events.
  1579. section.container.on( 'keydown', function( event ) {
  1580. if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
  1581. return;
  1582. }
  1583. // Pressing the right arrow key fires a theme:next event
  1584. if ( 39 === event.keyCode ) {
  1585. section.nextTheme();
  1586. }
  1587. // Pressing the left arrow key fires a theme:previous event
  1588. if ( 37 === event.keyCode ) {
  1589. section.previousTheme();
  1590. }
  1591. // Pressing the escape key fires a theme:collapse event
  1592. if ( 27 === event.keyCode ) {
  1593. if ( section.$body.hasClass( 'modal-open' ) ) {
  1594. // Escape from the details modal.
  1595. section.closeDetails();
  1596. } else {
  1597. // Escape from the inifinite scroll list.
  1598. section.headerContainer.find( '.customize-themes-section-title' ).focus();
  1599. }
  1600. event.stopPropagation(); // Prevent section from being collapsed.
  1601. }
  1602. });
  1603. section.renderScreenshots = _.throttle( section.renderScreenshots, 100 );
  1604. _.bindAll( section, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
  1605. },
  1606. /**
  1607. * Override Section.isContextuallyActive method.
  1608. *
  1609. * Ignore the active states' of the contained theme controls, and just
  1610. * use the section's own active state instead. This prevents empty search
  1611. * results for theme sections from causing the section to become inactive.
  1612. *
  1613. * @since 4.2.0
  1614. *
  1615. * @returns {Boolean}
  1616. */
  1617. isContextuallyActive: function () {
  1618. return this.active();
  1619. },
  1620. /**
  1621. * Attach events.
  1622. *
  1623. * @since 4.2.0
  1624. *
  1625. * @returns {void}
  1626. */
  1627. attachEvents: function () {
  1628. var section = this, debounced;
  1629. // Expand/Collapse accordion sections on click.
  1630. section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) {
  1631. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1632. return;
  1633. }
  1634. event.preventDefault(); // Keep this AFTER the key filter above
  1635. section.collapse();
  1636. });
  1637. section.headerContainer = $( '#accordion-section-' + section.id );
  1638. // Expand section/panel. Only collapse when opening another section.
  1639. section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
  1640. // Toggle accordion filters under section headers.
  1641. if ( section.headerContainer.find( '.filter-details' ).length ) {
  1642. section.headerContainer.find( '.customize-themes-section-title' )
  1643. .toggleClass( 'details-open' )
  1644. .attr( 'aria-expanded', function( i, attr ) {
  1645. return 'true' === attr ? 'false' : 'true';
  1646. });
  1647. section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
  1648. }
  1649. // Open the section.
  1650. if ( ! section.expanded() ) {
  1651. section.expand();
  1652. }
  1653. });
  1654. // Preview installed themes.
  1655. section.container.on( 'click', '.theme-actions .preview-theme', function() {
  1656. api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) );
  1657. });
  1658. // Theme navigation in details view.
  1659. section.container.on( 'click', '.left', function() {
  1660. section.previousTheme();
  1661. });
  1662. section.container.on( 'click', '.right', function() {
  1663. section.nextTheme();
  1664. });
  1665. section.container.on( 'click', '.theme-backdrop, .close', function() {
  1666. section.closeDetails();
  1667. });
  1668. if ( 'local' === section.params.filter_type ) {
  1669. // Filter-search all theme objects loaded in the section.
  1670. section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
  1671. section.filterSearch( event.currentTarget.value );
  1672. });
  1673. } else if ( 'remote' === section.params.filter_type ) {
  1674. // Event listeners for remote queries with user-entered terms.
  1675. // Search terms.
  1676. debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
  1677. section.contentContainer.on( 'input', '.wp-filter-search', function() {
  1678. if ( ! api.panel( 'themes' ).expanded() ) {
  1679. return;
  1680. }
  1681. debounced( section );
  1682. if ( ! section.expanded() ) {
  1683. section.expand();
  1684. }
  1685. });
  1686. // Feature filters.
  1687. section.contentContainer.on( 'click', '.filter-group input', function() {
  1688. section.filtersChecked();
  1689. section.checkTerm( section );
  1690. });
  1691. }
  1692. // Toggle feature filters.
  1693. section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
  1694. var $themeContainer = $( '.customize-themes-full-container' ),
  1695. $filterToggle = $( e.currentTarget );
  1696. section.filtersHeight = $filterToggle.parent().next( '.filter-drawer' ).height();
  1697. if ( 0 < $themeContainer.scrollTop() ) {
  1698. $themeContainer.animate( { scrollTop: 0 }, 400 );
  1699. if ( $filterToggle.hasClass( 'open' ) ) {
  1700. return;
  1701. }
  1702. }
  1703. $filterToggle
  1704. .toggleClass( 'open' )
  1705. .attr( 'aria-expanded', function( i, attr ) {
  1706. return 'true' === attr ? 'false' : 'true';
  1707. })
  1708. .parent().next( '.filter-drawer' ).slideToggle( 180, 'linear' );
  1709. if ( $filterToggle.hasClass( 'open' ) ) {
  1710. var marginOffset = 1018 < window.innerWidth ? 50 : 76;
  1711. section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + marginOffset );
  1712. } else {
  1713. section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
  1714. }
  1715. });
  1716. // Setup section cross-linking.
  1717. section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
  1718. api.section( 'wporg_themes' ).focus();
  1719. });
  1720. function updateSelectedState() {
  1721. var el = section.headerContainer.find( '.customize-themes-section-title' );
  1722. el.toggleClass( 'selected', section.expanded() );
  1723. el.attr( 'aria-expanded', section.expanded() ? 'true' : 'false' );
  1724. if ( ! section.expanded() ) {
  1725. el.removeClass( 'details-open' );
  1726. }
  1727. }
  1728. section.expanded.bind( updateSelectedState );
  1729. updateSelectedState();
  1730. // Move section controls to the themes area.
  1731. api.bind( 'ready', function () {
  1732. section.contentContainer = section.container.find( '.customize-themes-section' );
  1733. section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
  1734. section.container.add( section.headerContainer );
  1735. });
  1736. },
  1737. /**
  1738. * Update UI to reflect expanded state
  1739. *
  1740. * @since 4.2.0
  1741. *
  1742. * @param {Boolean} expanded
  1743. * @param {Object} args
  1744. * @param {Boolean} args.unchanged
  1745. * @param {Function} args.completeCallback
  1746. * @returns {void}
  1747. */
  1748. onChangeExpanded: function ( expanded, args ) {
  1749. // Note: there is a second argument 'args' passed
  1750. var section = this,
  1751. container = section.contentContainer.closest( '.customize-themes-full-container' );
  1752. // Immediately call the complete callback if there were no changes
  1753. if ( args.unchanged ) {
  1754. if ( args.completeCallback ) {
  1755. args.completeCallback();
  1756. }
  1757. return;
  1758. }
  1759. function expand() {
  1760. // Try to load controls if none are loaded yet.
  1761. if ( 0 === section.loaded ) {
  1762. section.loadThemes();
  1763. }
  1764. // Collapse any sibling sections/panels
  1765. api.section.each( function ( otherSection ) {
  1766. var searchTerm;
  1767. if ( otherSection !== section ) {
  1768. // Try to sync the current search term to the new section.
  1769. if ( 'themes' === otherSection.params.type ) {
  1770. searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
  1771. section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
  1772. // Directly initialize an empty remote search to avoid a race condition.
  1773. if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
  1774. section.term = '';
  1775. section.initializeNewQuery( section.term, section.tags );
  1776. } else {
  1777. if ( 'remote' === section.params.filter_type ) {
  1778. section.checkTerm( section );
  1779. } else if ( 'local' === section.params.filter_type ) {
  1780. section.filterSearch( searchTerm );
  1781. }
  1782. }
  1783. otherSection.collapse( { duration: args.duration } );
  1784. }
  1785. }
  1786. });
  1787. section.contentContainer.addClass( 'current-section' );
  1788. container.scrollTop();
  1789. container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
  1790. container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
  1791. if ( args.completeCallback ) {
  1792. args.completeCallback();
  1793. }
  1794. section.updateCount(); // Show this section's count.
  1795. }
  1796. if ( expanded ) {
  1797. if ( section.panel() && api.panel.has( section.panel() ) ) {
  1798. api.panel( section.panel() ).expand({
  1799. duration: args.duration,
  1800. completeCallback: expand
  1801. });
  1802. } else {
  1803. expand();
  1804. }
  1805. } else {
  1806. section.contentContainer.removeClass( 'current-section' );
  1807. // Always hide, even if they don't exist or are already hidden.
  1808. section.headerContainer.find( '.filter-details' ).slideUp( 180 );
  1809. container.off( 'scroll' );
  1810. if ( args.completeCallback ) {
  1811. args.completeCallback();
  1812. }
  1813. }
  1814. },
  1815. /**
  1816. * Return the section's content element without detaching from the parent.
  1817. *
  1818. * @since 4.9.0
  1819. *
  1820. * @returns {jQuery}
  1821. */
  1822. getContent: function() {
  1823. return this.container.find( '.control-section-content' );
  1824. },
  1825. /**
  1826. * Load theme data via Ajax and add themes to the section as controls.
  1827. *
  1828. * @since 4.9.0
  1829. *
  1830. * @returns {void}
  1831. */
  1832. loadThemes: function() {
  1833. var section = this, params, page, request;
  1834. if ( section.loading ) {
  1835. return; // We're already loading a batch of themes.
  1836. }
  1837. // Parameters for every API query. Additional params are set in PHP.
  1838. page = Math.ceil( section.loaded / 100 ) + 1;
  1839. params = {
  1840. 'nonce': api.settings.nonce.switch_themes,
  1841. 'wp_customize': 'on',
  1842. 'theme_action': section.params.action,
  1843. 'customized_theme': api.settings.theme.stylesheet,
  1844. 'page': page
  1845. };
  1846. // Add fields for remote filtering.
  1847. if ( 'remote' === section.params.filter_type ) {
  1848. params.search = section.term;
  1849. params.tags = section.tags;
  1850. }
  1851. // Load themes.
  1852. section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
  1853. section.loading = true;
  1854. section.container.find( '.no-themes' ).hide();
  1855. request = wp.ajax.post( 'customize_load_themes', params );
  1856. request.done(function( data ) {
  1857. var themes = data.themes;
  1858. // Stop and try again if the term changed while loading.
  1859. if ( '' !== section.nextTerm || '' !== section.nextTags ) {
  1860. if ( section.nextTerm ) {
  1861. section.term = section.nextTerm;
  1862. }
  1863. if ( section.nextTags ) {
  1864. section.tags = section.nextTags;
  1865. }
  1866. section.nextTerm = '';
  1867. section.nextTags = '';
  1868. section.loading = false;
  1869. section.loadThemes();
  1870. return;
  1871. }
  1872. if ( 0 !== themes.length ) {
  1873. section.loadControls( themes, page );
  1874. if ( 1 === page ) {
  1875. // Pre-load the first 3 theme screenshots.
  1876. _.each( section.controls().slice( 0, 3 ), function( control ) {
  1877. var img, src = control.params.theme.screenshot[0];
  1878. if ( src ) {
  1879. img = new Image();
  1880. img.src = src;
  1881. }
  1882. });
  1883. if ( 'local' !== section.params.filter_type ) {
  1884. wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
  1885. }
  1886. }
  1887. _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
  1888. if ( 'local' === section.params.filter_type || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
  1889. section.fullyLoaded = true;
  1890. }
  1891. } else {
  1892. if ( 0 === section.loaded ) {
  1893. section.container.find( '.no-themes' ).show();
  1894. wp.a11y.speak( section.container.find( '.no-themes' ).text() );
  1895. } else {
  1896. section.fullyLoaded = true;
  1897. }
  1898. }
  1899. if ( 'local' === section.params.filter_type ) {
  1900. section.updateCount(); // Count of visible theme controls.
  1901. } else {
  1902. section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
  1903. }
  1904. section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
  1905. // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
  1906. section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
  1907. section.loading = false;
  1908. });
  1909. request.fail(function( data ) {
  1910. if ( 'undefined' === typeof data ) {
  1911. section.container.find( '.unexpected-error' ).show();
  1912. wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
  1913. } else if ( 'undefined' !== typeof console && console.error ) {
  1914. console.error( data );
  1915. }
  1916. // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
  1917. section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
  1918. section.loading = false;
  1919. });
  1920. },
  1921. /**
  1922. * Loads controls into the section from data received from loadThemes().
  1923. *
  1924. * @since 4.9.0
  1925. * @param {Array} themes - Array of theme data to create controls with.
  1926. * @param {integer} page - Page of results being loaded.
  1927. * @returns {void}
  1928. */
  1929. loadControls: function( themes, page ) {
  1930. var newThemeControls = [],
  1931. section = this;
  1932. // Add controls for each theme.
  1933. _.each( themes, function( theme ) {
  1934. var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
  1935. type: 'theme',
  1936. section: section.params.id,
  1937. theme: theme,
  1938. priority: section.loaded + 1
  1939. } );
  1940. api.control.add( themeControl );
  1941. newThemeControls.push( themeControl );
  1942. section.loaded = section.loaded + 1;
  1943. });
  1944. if ( 1 !== page ) {
  1945. Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
  1946. }
  1947. },
  1948. /**
  1949. * Determines whether more themes should be loaded, and loads them.
  1950. *
  1951. * @since 4.9.0
  1952. * @returns {void}
  1953. */
  1954. loadMore: function() {
  1955. var section = this, container, bottom, threshold;
  1956. if ( ! section.fullyLoaded && ! section.loading ) {
  1957. container = section.container.closest( '.customize-themes-full-container' );
  1958. bottom = container.scrollTop() + container.height();
  1959. threshold = container.prop( 'scrollHeight' ) - 3000; // Use a fixed distance to the bottom of loaded results to avoid unnecessarily loading results sooner when using a percentage of scroll distance.
  1960. if ( bottom > threshold ) {
  1961. section.loadThemes();
  1962. }
  1963. }
  1964. },
  1965. /**
  1966. * Event handler for search input that filters visible controls.
  1967. *
  1968. * @since 4.9.0
  1969. *
  1970. * @param {string} term - The raw search input value.
  1971. * @returns {void}
  1972. */
  1973. filterSearch: function( term ) {
  1974. var count = 0,
  1975. visible = false,
  1976. section = this,
  1977. noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
  1978. controls = section.controls(),
  1979. terms;
  1980. if ( section.loading ) {
  1981. return;
  1982. }
  1983. // Standardize search term format and split into an array of individual words.
  1984. terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
  1985. _.each( controls, function( control ) {
  1986. visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
  1987. if ( visible ) {
  1988. count = count + 1;
  1989. }
  1990. });
  1991. if ( 0 === count ) {
  1992. section.container.find( noFilter ).show();
  1993. wp.a11y.speak( section.container.find( noFilter ).text() );
  1994. } else {
  1995. section.container.find( noFilter ).hide();
  1996. }
  1997. section.renderScreenshots();
  1998. api.reflowPaneContents();
  1999. // Update theme count.
  2000. section.updateCountDebounced( count );
  2001. },
  2002. /**
  2003. * Event handler for search input that determines if the terms have changed and loads new controls as needed.
  2004. *
  2005. * @since 4.9.0
  2006. *
  2007. * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
  2008. * @returns {void}
  2009. */
  2010. checkTerm: function( section ) {
  2011. var newTerm;
  2012. if ( 'remote' === section.params.filter_type ) {
  2013. newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
  2014. if ( section.term !== newTerm.trim() ) {
  2015. section.initializeNewQuery( newTerm, section.tags );
  2016. }
  2017. }
  2018. },
  2019. /**
  2020. * Check for filters checked in the feature filter list and initialize a new query.
  2021. *
  2022. * @since 4.9.0
  2023. *
  2024. * @returns {void}
  2025. */
  2026. filtersChecked: function() {
  2027. var section = this,
  2028. items = section.container.find( '.filter-group' ).find( ':checkbox' ),
  2029. tags = [];
  2030. _.each( items.filter( ':checked' ), function( item ) {
  2031. tags.push( $( item ).prop( 'value' ) );
  2032. });
  2033. // When no filters are checked, restore initial state. Update filter count.
  2034. if ( 0 === tags.length ) {
  2035. tags = '';
  2036. section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
  2037. section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
  2038. } else {
  2039. section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
  2040. section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
  2041. section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
  2042. }
  2043. // Check whether tags have changed, and either load or queue them.
  2044. if ( ! _.isEqual( section.tags, tags ) ) {
  2045. if ( section.loading ) {
  2046. section.nextTags = tags;
  2047. } else {
  2048. if ( 'remote' === section.params.filter_type ) {
  2049. section.initializeNewQuery( section.term, tags );
  2050. } else if ( 'local' === section.params.filter_type ) {
  2051. section.filterSearch( tags.join( ' ' ) );
  2052. }
  2053. }
  2054. }
  2055. },
  2056. /**
  2057. * Reset the current query and load new results.
  2058. *
  2059. * @since 4.9.0
  2060. *
  2061. * @param {string} newTerm - New term.
  2062. * @param {Array} newTags - New tags.
  2063. * @returns {void}
  2064. */
  2065. initializeNewQuery: function( newTerm, newTags ) {
  2066. var section = this;
  2067. // Clear the controls in the section.
  2068. _.each( section.controls(), function( control ) {
  2069. control.container.remove();
  2070. api.control.remove( control.id );
  2071. });
  2072. section.loaded = 0;
  2073. section.fullyLoaded = false;
  2074. section.screenshotQueue = null;
  2075. // Run a new query, with loadThemes handling paging, etc.
  2076. if ( ! section.loading ) {
  2077. section.term = newTerm;
  2078. section.tags = newTags;
  2079. section.loadThemes();
  2080. } else {
  2081. section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
  2082. section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
  2083. }
  2084. if ( ! section.expanded() ) {
  2085. section.expand(); // Expand the section if it isn't expanded.
  2086. }
  2087. },
  2088. /**
  2089. * Render control's screenshot if the control comes into view.
  2090. *
  2091. * @since 4.2.0
  2092. *
  2093. * @returns {void}
  2094. */
  2095. renderScreenshots: function() {
  2096. var section = this;
  2097. // Fill queue initially, or check for more if empty.
  2098. if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
  2099. // Add controls that haven't had their screenshots rendered.
  2100. section.screenshotQueue = _.filter( section.controls(), function( control ) {
  2101. return ! control.screenshotRendered;
  2102. });
  2103. }
  2104. // Are all screenshots rendered (for now)?
  2105. if ( ! section.screenshotQueue.length ) {
  2106. return;
  2107. }
  2108. section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
  2109. var $imageWrapper = control.container.find( '.theme-screenshot' ),
  2110. $image = $imageWrapper.find( 'img' );
  2111. if ( ! $image.length ) {
  2112. return false;
  2113. }
  2114. if ( $image.is( ':hidden' ) ) {
  2115. return true;
  2116. }
  2117. // Based on unveil.js.
  2118. var wt = section.$window.scrollTop(),
  2119. wb = wt + section.$window.height(),
  2120. et = $image.offset().top,
  2121. ih = $imageWrapper.height(),
  2122. eb = et + ih,
  2123. threshold = ih * 3,
  2124. inView = eb >= wt - threshold && et <= wb + threshold;
  2125. if ( inView ) {
  2126. control.container.trigger( 'render-screenshot' );
  2127. }
  2128. // If the image is in view return false so it's cleared from the queue.
  2129. return ! inView;
  2130. } );
  2131. },
  2132. /**
  2133. * Get visible count.
  2134. *
  2135. * @since 4.9.0
  2136. *
  2137. * @returns {int} Visible count.
  2138. */
  2139. getVisibleCount: function() {
  2140. return this.contentContainer.find( 'li.customize-control:visible' ).length;
  2141. },
  2142. /**
  2143. * Update the number of themes in the section.
  2144. *
  2145. * @since 4.9.0
  2146. *
  2147. * @returns {void}
  2148. */
  2149. updateCount: function( count ) {
  2150. var section = this, countEl, displayed;
  2151. if ( ! count && 0 !== count ) {
  2152. count = section.getVisibleCount();
  2153. }
  2154. displayed = section.contentContainer.find( '.themes-displayed' );
  2155. countEl = section.contentContainer.find( '.theme-count' );
  2156. if ( 0 === count ) {
  2157. countEl.text( '0' );
  2158. } else {
  2159. // Animate the count change for emphasis.
  2160. displayed.fadeOut( 180, function() {
  2161. countEl.text( count );
  2162. displayed.fadeIn( 180 );
  2163. } );
  2164. wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
  2165. }
  2166. },
  2167. /**
  2168. * Advance the modal to the next theme.
  2169. *
  2170. * @since 4.2.0
  2171. *
  2172. * @returns {void}
  2173. */
  2174. nextTheme: function () {
  2175. var section = this;
  2176. if ( section.getNextTheme() ) {
  2177. section.showDetails( section.getNextTheme(), function() {
  2178. section.overlay.find( '.right' ).focus();
  2179. } );
  2180. }
  2181. },
  2182. /**
  2183. * Get the next theme model.
  2184. *
  2185. * @since 4.2.0
  2186. *
  2187. * @returns {wp.customize.ThemeControl|boolean} Next theme.
  2188. */
  2189. getNextTheme: function () {
  2190. var section = this, control, nextControl, sectionControls, i;
  2191. control = api.control( section.params.action + '_theme_' + section.currentTheme );
  2192. sectionControls = section.controls();
  2193. i = _.indexOf( sectionControls, control );
  2194. if ( -1 === i ) {
  2195. return false;
  2196. }
  2197. nextControl = sectionControls[ i + 1 ];
  2198. if ( ! nextControl ) {
  2199. return false;
  2200. }
  2201. return nextControl.params.theme;
  2202. },
  2203. /**
  2204. * Advance the modal to the previous theme.
  2205. *
  2206. * @since 4.2.0
  2207. * @returns {void}
  2208. */
  2209. previousTheme: function () {
  2210. var section = this;
  2211. if ( section.getPreviousTheme() ) {
  2212. section.showDetails( section.getPreviousTheme(), function() {
  2213. section.overlay.find( '.left' ).focus();
  2214. } );
  2215. }
  2216. },
  2217. /**
  2218. * Get the previous theme model.
  2219. *
  2220. * @since 4.2.0
  2221. * @returns {wp.customize.ThemeControl|boolean} Previous theme.
  2222. */
  2223. getPreviousTheme: function () {
  2224. var section = this, control, nextControl, sectionControls, i;
  2225. control = api.control( section.params.action + '_theme_' + section.currentTheme );
  2226. sectionControls = section.controls();
  2227. i = _.indexOf( sectionControls, control );
  2228. if ( -1 === i ) {
  2229. return false;
  2230. }
  2231. nextControl = sectionControls[ i - 1 ];
  2232. if ( ! nextControl ) {
  2233. return false;
  2234. }
  2235. return nextControl.params.theme;
  2236. },
  2237. /**
  2238. * Disable buttons when we're viewing the first or last theme.
  2239. *
  2240. * @since 4.2.0
  2241. *
  2242. * @returns {void}
  2243. */
  2244. updateLimits: function () {
  2245. if ( ! this.getNextTheme() ) {
  2246. this.overlay.find( '.right' ).addClass( 'disabled' );
  2247. }
  2248. if ( ! this.getPreviousTheme() ) {
  2249. this.overlay.find( '.left' ).addClass( 'disabled' );
  2250. }
  2251. },
  2252. /**
  2253. * Load theme preview.
  2254. *
  2255. * @since 4.7.0
  2256. * @access public
  2257. *
  2258. * @deprecated
  2259. * @param {string} themeId Theme ID.
  2260. * @returns {jQuery.promise} Promise.
  2261. */
  2262. loadThemePreview: function( themeId ) {
  2263. return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
  2264. },
  2265. /**
  2266. * Render & show the theme details for a given theme model.
  2267. *
  2268. * @since 4.2.0
  2269. *
  2270. * @param {object} theme - Theme.
  2271. * @param {Function} [callback] - Callback once the details have been shown.
  2272. * @returns {void}
  2273. */
  2274. showDetails: function ( theme, callback ) {
  2275. var section = this, panel = api.panel( 'themes' );
  2276. section.currentTheme = theme.id;
  2277. section.overlay.html( section.template( theme ) )
  2278. .fadeIn( 'fast' )
  2279. .focus();
  2280. function disableSwitchButtons() {
  2281. return ! panel.canSwitchTheme( theme.id );
  2282. }
  2283. // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  2284. function disableInstallButtons() {
  2285. return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
  2286. }
  2287. section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
  2288. section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
  2289. section.$body.addClass( 'modal-open' );
  2290. section.containFocus( section.overlay );
  2291. section.updateLimits();
  2292. wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
  2293. if ( callback ) {
  2294. callback();
  2295. }
  2296. },
  2297. /**
  2298. * Close the theme details modal.
  2299. *
  2300. * @since 4.2.0
  2301. *
  2302. * @returns {void}
  2303. */
  2304. closeDetails: function () {
  2305. var section = this;
  2306. section.$body.removeClass( 'modal-open' );
  2307. section.overlay.fadeOut( 'fast' );
  2308. api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
  2309. },
  2310. /**
  2311. * Keep tab focus within the theme details modal.
  2312. *
  2313. * @since 4.2.0
  2314. *
  2315. * @param {jQuery} el - Element to contain focus.
  2316. * @returns {void}
  2317. */
  2318. containFocus: function( el ) {
  2319. var tabbables;
  2320. el.on( 'keydown', function( event ) {
  2321. // Return if it's not the tab key
  2322. // When navigating with prev/next focus is already handled
  2323. if ( 9 !== event.keyCode ) {
  2324. return;
  2325. }
  2326. // uses jQuery UI to get the tabbable elements
  2327. tabbables = $( ':tabbable', el );
  2328. // Keep focus within the overlay
  2329. if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
  2330. tabbables.first().focus();
  2331. return false;
  2332. } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
  2333. tabbables.last().focus();
  2334. return false;
  2335. }
  2336. });
  2337. }
  2338. });
  2339. api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{
  2340. /**
  2341. * Class wp.customize.OuterSection.
  2342. *
  2343. * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
  2344. * it would require custom handling.
  2345. *
  2346. * @constructs wp.customize.OuterSection
  2347. * @augments wp.customize.Section
  2348. *
  2349. * @since 4.9.0
  2350. *
  2351. * @returns {void}
  2352. */
  2353. initialize: function() {
  2354. var section = this;
  2355. section.containerParent = '#customize-outer-theme-controls';
  2356. section.containerPaneParent = '.customize-outer-pane-parent';
  2357. api.Section.prototype.initialize.apply( section, arguments );
  2358. },
  2359. /**
  2360. * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
  2361. * on other sections and panels.
  2362. *
  2363. * @since 4.9.0
  2364. *
  2365. * @param {Boolean} expanded - The expanded state to transition to.
  2366. * @param {Object} [args] - Args.
  2367. * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
  2368. * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
  2369. * @param {Object} [args.duration] - The duration for the animation.
  2370. */
  2371. onChangeExpanded: function( expanded, args ) {
  2372. var section = this,
  2373. container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
  2374. content = section.contentContainer,
  2375. backBtn = content.find( '.customize-section-back' ),
  2376. sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
  2377. body = $( document.body ),
  2378. expand, panel;
  2379. body.toggleClass( 'outer-section-open', expanded );
  2380. section.container.toggleClass( 'open', expanded );
  2381. section.container.removeClass( 'busy' );
  2382. api.section.each( function( _section ) {
  2383. if ( 'outer' === _section.params.type && _section.id !== section.id ) {
  2384. _section.container.removeClass( 'open' );
  2385. }
  2386. } );
  2387. if ( expanded && ! content.hasClass( 'open' ) ) {
  2388. if ( args.unchanged ) {
  2389. expand = args.completeCallback;
  2390. } else {
  2391. expand = $.proxy( function() {
  2392. section._animateChangeExpanded( function() {
  2393. sectionTitle.attr( 'tabindex', '-1' );
  2394. backBtn.attr( 'tabindex', '0' );
  2395. backBtn.focus();
  2396. content.css( 'top', '' );
  2397. container.scrollTop( 0 );
  2398. if ( args.completeCallback ) {
  2399. args.completeCallback();
  2400. }
  2401. } );
  2402. content.addClass( 'open' );
  2403. }, this );
  2404. }
  2405. if ( section.panel() ) {
  2406. api.panel( section.panel() ).expand({
  2407. duration: args.duration,
  2408. completeCallback: expand
  2409. });
  2410. } else {
  2411. expand();
  2412. }
  2413. } else if ( ! expanded && content.hasClass( 'open' ) ) {
  2414. if ( section.panel() ) {
  2415. panel = api.panel( section.panel() );
  2416. if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
  2417. panel.collapse();
  2418. }
  2419. }
  2420. section._animateChangeExpanded( function() {
  2421. backBtn.attr( 'tabindex', '-1' );
  2422. sectionTitle.attr( 'tabindex', '0' );
  2423. sectionTitle.focus();
  2424. content.css( 'top', '' );
  2425. if ( args.completeCallback ) {
  2426. args.completeCallback();
  2427. }
  2428. } );
  2429. content.removeClass( 'open' );
  2430. } else {
  2431. if ( args.completeCallback ) {
  2432. args.completeCallback();
  2433. }
  2434. }
  2435. }
  2436. });
  2437. api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{
  2438. containerType: 'panel',
  2439. /**
  2440. * @constructs wp.customize.Panel
  2441. * @augments wp.customize~Container
  2442. *
  2443. * @since 4.1.0
  2444. *
  2445. * @param {string} id - The ID for the panel.
  2446. * @param {object} options - Object containing one property: params.
  2447. * @param {string} options.title - Title shown when panel is collapsed and expanded.
  2448. * @param {string} [options.description] - Description shown at the top of the panel.
  2449. * @param {number} [options.priority=100] - The sort priority for the panel.
  2450. * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
  2451. * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
  2452. * @param {boolean} [options.active=true] - Whether the panel is active or not.
  2453. * @param {object} [options.params] - Deprecated wrapper for the above properties.
  2454. */
  2455. initialize: function ( id, options ) {
  2456. var panel = this, params;
  2457. params = options.params || options;
  2458. // Look up the type if one was not supplied.
  2459. if ( ! params.type ) {
  2460. _.find( api.panelConstructor, function( Constructor, type ) {
  2461. if ( Constructor === panel.constructor ) {
  2462. params.type = type;
  2463. return true;
  2464. }
  2465. return false;
  2466. } );
  2467. }
  2468. Container.prototype.initialize.call( panel, id, params );
  2469. panel.embed();
  2470. panel.deferred.embedded.done( function () {
  2471. panel.ready();
  2472. });
  2473. },
  2474. /**
  2475. * Embed the container in the DOM when any parent panel is ready.
  2476. *
  2477. * @since 4.1.0
  2478. */
  2479. embed: function () {
  2480. var panel = this,
  2481. container = $( '#customize-theme-controls' ),
  2482. parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  2483. if ( ! panel.headContainer.parent().is( parentContainer ) ) {
  2484. parentContainer.append( panel.headContainer );
  2485. }
  2486. if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
  2487. container.append( panel.contentContainer );
  2488. }
  2489. panel.renderContent();
  2490. panel.deferred.embedded.resolve();
  2491. },
  2492. /**
  2493. * @since 4.1.0
  2494. */
  2495. attachEvents: function () {
  2496. var meta, panel = this;
  2497. // Expand/Collapse accordion sections on click.
  2498. panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
  2499. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2500. return;
  2501. }
  2502. event.preventDefault(); // Keep this AFTER the key filter above
  2503. if ( ! panel.expanded() ) {
  2504. panel.expand();
  2505. }
  2506. });
  2507. // Close panel.
  2508. panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
  2509. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2510. return;
  2511. }
  2512. event.preventDefault(); // Keep this AFTER the key filter above
  2513. if ( panel.expanded() ) {
  2514. panel.collapse();
  2515. }
  2516. });
  2517. meta = panel.container.find( '.panel-meta:first' );
  2518. meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
  2519. if ( meta.hasClass( 'cannot-expand' ) ) {
  2520. return;
  2521. }
  2522. var content = meta.find( '.customize-panel-description:first' );
  2523. if ( meta.hasClass( 'open' ) ) {
  2524. meta.toggleClass( 'open' );
  2525. content.slideUp( panel.defaultExpandedArguments.duration, function() {
  2526. content.trigger( 'toggled' );
  2527. } );
  2528. $( this ).attr( 'aria-expanded', false );
  2529. } else {
  2530. content.slideDown( panel.defaultExpandedArguments.duration, function() {
  2531. content.trigger( 'toggled' );
  2532. } );
  2533. meta.toggleClass( 'open' );
  2534. $( this ).attr( 'aria-expanded', true );
  2535. }
  2536. });
  2537. },
  2538. /**
  2539. * Get the sections that are associated with this panel, sorted by their priority Value.
  2540. *
  2541. * @since 4.1.0
  2542. *
  2543. * @returns {Array}
  2544. */
  2545. sections: function () {
  2546. return this._children( 'panel', 'section' );
  2547. },
  2548. /**
  2549. * Return whether this panel has any active sections.
  2550. *
  2551. * @since 4.1.0
  2552. *
  2553. * @returns {boolean} Whether contextually active.
  2554. */
  2555. isContextuallyActive: function () {
  2556. var panel = this,
  2557. sections = panel.sections(),
  2558. activeCount = 0;
  2559. _( sections ).each( function ( section ) {
  2560. if ( section.active() && section.isContextuallyActive() ) {
  2561. activeCount += 1;
  2562. }
  2563. } );
  2564. return ( activeCount !== 0 );
  2565. },
  2566. /**
  2567. * Update UI to reflect expanded state.
  2568. *
  2569. * @since 4.1.0
  2570. *
  2571. * @param {Boolean} expanded
  2572. * @param {Object} args
  2573. * @param {Boolean} args.unchanged
  2574. * @param {Function} args.completeCallback
  2575. * @returns {void}
  2576. */
  2577. onChangeExpanded: function ( expanded, args ) {
  2578. // Immediately call the complete callback if there were no changes
  2579. if ( args.unchanged ) {
  2580. if ( args.completeCallback ) {
  2581. args.completeCallback();
  2582. }
  2583. return;
  2584. }
  2585. // Note: there is a second argument 'args' passed
  2586. var panel = this,
  2587. accordionSection = panel.contentContainer,
  2588. overlay = accordionSection.closest( '.wp-full-overlay' ),
  2589. container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
  2590. topPanel = panel.headContainer.find( '.accordion-section-title' ),
  2591. backBtn = accordionSection.find( '.customize-panel-back' ),
  2592. childSections = panel.sections(),
  2593. skipTransition;
  2594. if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
  2595. // Collapse any sibling sections/panels
  2596. api.section.each( function ( section ) {
  2597. if ( panel.id !== section.panel() ) {
  2598. section.collapse( { duration: 0 } );
  2599. }
  2600. });
  2601. api.panel.each( function ( otherPanel ) {
  2602. if ( panel !== otherPanel ) {
  2603. otherPanel.collapse( { duration: 0 } );
  2604. }
  2605. });
  2606. if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
  2607. accordionSection.addClass( 'current-panel skip-transition' );
  2608. overlay.addClass( 'in-sub-panel' );
  2609. childSections[0].expand( {
  2610. completeCallback: args.completeCallback
  2611. } );
  2612. } else {
  2613. panel._animateChangeExpanded( function() {
  2614. topPanel.attr( 'tabindex', '-1' );
  2615. backBtn.attr( 'tabindex', '0' );
  2616. backBtn.focus();
  2617. accordionSection.css( 'top', '' );
  2618. container.scrollTop( 0 );
  2619. if ( args.completeCallback ) {
  2620. args.completeCallback();
  2621. }
  2622. } );
  2623. accordionSection.addClass( 'current-panel' );
  2624. overlay.addClass( 'in-sub-panel' );
  2625. }
  2626. api.state( 'expandedPanel' ).set( panel );
  2627. } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
  2628. skipTransition = accordionSection.hasClass( 'skip-transition' );
  2629. if ( ! skipTransition ) {
  2630. panel._animateChangeExpanded( function() {
  2631. topPanel.attr( 'tabindex', '0' );
  2632. backBtn.attr( 'tabindex', '-1' );
  2633. topPanel.focus();
  2634. accordionSection.css( 'top', '' );
  2635. if ( args.completeCallback ) {
  2636. args.completeCallback();
  2637. }
  2638. } );
  2639. } else {
  2640. accordionSection.removeClass( 'skip-transition' );
  2641. }
  2642. overlay.removeClass( 'in-sub-panel' );
  2643. accordionSection.removeClass( 'current-panel' );
  2644. if ( panel === api.state( 'expandedPanel' ).get() ) {
  2645. api.state( 'expandedPanel' ).set( false );
  2646. }
  2647. }
  2648. },
  2649. /**
  2650. * Render the panel from its JS template, if it exists.
  2651. *
  2652. * The panel's container must already exist in the DOM.
  2653. *
  2654. * @since 4.3.0
  2655. */
  2656. renderContent: function () {
  2657. var template,
  2658. panel = this;
  2659. // Add the content to the container.
  2660. if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
  2661. template = wp.template( panel.templateSelector + '-content' );
  2662. } else {
  2663. template = wp.template( 'customize-panel-default-content' );
  2664. }
  2665. if ( template && panel.headContainer ) {
  2666. panel.contentContainer.html( template( _.extend(
  2667. { id: panel.id },
  2668. panel.params
  2669. ) ) );
  2670. }
  2671. }
  2672. });
  2673. api.ThemesPanel = api.Panel.extend(/** @lends wp.customize.ThemsPanel.prototype */{
  2674. /**
  2675. * Class wp.customize.ThemesPanel.
  2676. *
  2677. * Custom section for themes that displays without the customize preview.
  2678. *
  2679. * @constructs wp.customize.ThemesPanel
  2680. * @augments wp.customize.Panel
  2681. *
  2682. * @since 4.9.0
  2683. *
  2684. * @param {string} id - The ID for the panel.
  2685. * @param {object} options - Options.
  2686. * @returns {void}
  2687. */
  2688. initialize: function( id, options ) {
  2689. var panel = this;
  2690. panel.installingThemes = [];
  2691. api.Panel.prototype.initialize.call( panel, id, options );
  2692. },
  2693. /**
  2694. * Determine whether a given theme can be switched to, or in general.
  2695. *
  2696. * @since 4.9.0
  2697. *
  2698. * @param {string} [slug] - Theme slug.
  2699. * @returns {boolean} Whether the theme can be switched to.
  2700. */
  2701. canSwitchTheme: function canSwitchTheme( slug ) {
  2702. if ( slug && slug === api.settings.theme.stylesheet ) {
  2703. return true;
  2704. }
  2705. return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() );
  2706. },
  2707. /**
  2708. * Attach events.
  2709. *
  2710. * @since 4.9.0
  2711. * @returns {void}
  2712. */
  2713. attachEvents: function() {
  2714. var panel = this;
  2715. // Attach regular panel events.
  2716. api.Panel.prototype.attachEvents.apply( panel );
  2717. // Temporary since supplying SFTP credentials does not work yet. See #42184
  2718. if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) {
  2719. panel.notifications.add( new api.Notification( 'theme_install_unavailable', {
  2720. message: api.l10n.themeInstallUnavailable,
  2721. type: 'info',
  2722. dismissible: true
  2723. } ) );
  2724. }
  2725. function toggleDisabledNotifications() {
  2726. if ( panel.canSwitchTheme() ) {
  2727. panel.notifications.remove( 'theme_switch_unavailable' );
  2728. } else {
  2729. panel.notifications.add( new api.Notification( 'theme_switch_unavailable', {
  2730. message: api.l10n.themePreviewUnavailable,
  2731. type: 'warning'
  2732. } ) );
  2733. }
  2734. }
  2735. toggleDisabledNotifications();
  2736. api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications );
  2737. api.state( 'changesetStatus' ).bind( toggleDisabledNotifications );
  2738. // Collapse panel to customize the current theme.
  2739. panel.contentContainer.on( 'click', '.customize-theme', function() {
  2740. panel.collapse();
  2741. });
  2742. // Toggle between filtering and browsing themes on mobile.
  2743. panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
  2744. $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
  2745. });
  2746. // Install (and maybe preview) a theme.
  2747. panel.contentContainer.on( 'click', '.theme-install', function( event ) {
  2748. panel.installTheme( event );
  2749. });
  2750. // Update a theme. Theme cards have the class, the details modal has the id.
  2751. panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
  2752. // #update-theme is a link.
  2753. event.preventDefault();
  2754. event.stopPropagation();
  2755. panel.updateTheme( event );
  2756. });
  2757. // Delete a theme.
  2758. panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
  2759. panel.deleteTheme( event );
  2760. });
  2761. _.bindAll( panel, 'installTheme', 'updateTheme' );
  2762. },
  2763. /**
  2764. * Update UI to reflect expanded state
  2765. *
  2766. * @since 4.9.0
  2767. *
  2768. * @param {Boolean} expanded - Expanded state.
  2769. * @param {Object} args - Args.
  2770. * @param {Boolean} args.unchanged - Whether or not the state changed.
  2771. * @param {Function} args.completeCallback - Callback to execute when the animation completes.
  2772. * @returns {void}
  2773. */
  2774. onChangeExpanded: function( expanded, args ) {
  2775. var panel = this, overlay, sections, hasExpandedSection = false;
  2776. // Expand/collapse the panel normally.
  2777. api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
  2778. // Immediately call the complete callback if there were no changes
  2779. if ( args.unchanged ) {
  2780. if ( args.completeCallback ) {
  2781. args.completeCallback();
  2782. }
  2783. return;
  2784. }
  2785. overlay = panel.headContainer.closest( '.wp-full-overlay' );
  2786. if ( expanded ) {
  2787. overlay
  2788. .addClass( 'in-themes-panel' )
  2789. .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
  2790. _.delay( function() {
  2791. overlay.addClass( 'themes-panel-expanded' );
  2792. }, 200 );
  2793. // Automatically open the first section (except on small screens), if one isn't already expanded.
  2794. if ( 600 < window.innerWidth ) {
  2795. sections = panel.sections();
  2796. _.each( sections, function( section ) {
  2797. if ( section.expanded() ) {
  2798. hasExpandedSection = true;
  2799. }
  2800. } );
  2801. if ( ! hasExpandedSection && sections.length > 0 ) {
  2802. sections[0].expand();
  2803. }
  2804. }
  2805. } else {
  2806. overlay
  2807. .removeClass( 'in-themes-panel themes-panel-expanded' )
  2808. .find( '.customize-themes-full-container' ).removeClass( 'animate' );
  2809. }
  2810. },
  2811. /**
  2812. * Install a theme via wp.updates.
  2813. *
  2814. * @since 4.9.0
  2815. *
  2816. * @param {jQuery.Event} event - Event.
  2817. * @returns {jQuery.promise} Promise.
  2818. */
  2819. installTheme: function( event ) {
  2820. var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request;
  2821. preview = $( event.target ).hasClass( 'preview' );
  2822. // Temporary since supplying SFTP credentials does not work yet. See #42184.
  2823. if ( api.settings.theme._filesystemCredentialsNeeded ) {
  2824. deferred.reject({
  2825. errorCode: 'theme_install_unavailable'
  2826. });
  2827. return deferred.promise();
  2828. }
  2829. // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
  2830. if ( ! panel.canSwitchTheme( slug ) ) {
  2831. deferred.reject({
  2832. errorCode: 'theme_switch_unavailable'
  2833. });
  2834. return deferred.promise();
  2835. }
  2836. // Theme is already being installed.
  2837. if ( _.contains( panel.installingThemes, slug ) ) {
  2838. deferred.reject({
  2839. errorCode: 'theme_already_installing'
  2840. });
  2841. return deferred.promise();
  2842. }
  2843. wp.updates.maybeRequestFilesystemCredentials( event );
  2844. onInstallSuccess = function( response ) {
  2845. var theme = false, themeControl;
  2846. if ( preview ) {
  2847. api.notifications.remove( 'theme_installing' );
  2848. panel.loadThemePreview( slug );
  2849. } else {
  2850. api.control.each( function( control ) {
  2851. if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
  2852. theme = control.params.theme; // Used below to add theme control.
  2853. control.rerenderAsInstalled( true );
  2854. }
  2855. });
  2856. // Don't add the same theme more than once.
  2857. if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
  2858. deferred.resolve( response );
  2859. return;
  2860. }
  2861. // Add theme control to installed section.
  2862. theme.type = 'installed';
  2863. themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, {
  2864. type: 'theme',
  2865. section: 'installed_themes',
  2866. theme: theme,
  2867. priority: 0 // Add all newly-installed themes to the top.
  2868. } );
  2869. api.control.add( themeControl );
  2870. api.control( themeControl.id ).container.trigger( 'render-screenshot' );
  2871. // Close the details modal if it's open to the installed theme.
  2872. api.section.each( function( section ) {
  2873. if ( 'themes' === section.params.type ) {
  2874. if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
  2875. section.closeDetails();
  2876. }
  2877. }
  2878. });
  2879. }
  2880. deferred.resolve( response );
  2881. };
  2882. panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
  2883. request = wp.updates.installTheme( {
  2884. slug: slug
  2885. } );
  2886. // Also preview the theme as the event is triggered on Install & Preview.
  2887. if ( preview ) {
  2888. api.notifications.add( new api.OverlayNotification( 'theme_installing', {
  2889. message: api.l10n.themeDownloading,
  2890. type: 'info',
  2891. loading: true
  2892. } ) );
  2893. }
  2894. request.done( onInstallSuccess );
  2895. request.fail( function() {
  2896. api.notifications.remove( 'theme_installing' );
  2897. } );
  2898. return deferred.promise();
  2899. },
  2900. /**
  2901. * Load theme preview.
  2902. *
  2903. * @since 4.9.0
  2904. *
  2905. * @param {string} themeId Theme ID.
  2906. * @returns {jQuery.promise} Promise.
  2907. */
  2908. loadThemePreview: function( themeId ) {
  2909. var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
  2910. // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
  2911. if ( ! panel.canSwitchTheme( themeId ) ) {
  2912. deferred.reject({
  2913. errorCode: 'theme_switch_unavailable'
  2914. });
  2915. return deferred.promise();
  2916. }
  2917. urlParser = document.createElement( 'a' );
  2918. urlParser.href = location.href;
  2919. queryParams = _.extend(
  2920. api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  2921. {
  2922. theme: themeId,
  2923. changeset_uuid: api.settings.changeset.uuid,
  2924. 'return': api.settings.url['return']
  2925. }
  2926. );
  2927. // Include autosaved param to load autosave revision without prompting user to restore it.
  2928. if ( ! api.state( 'saved' ).get() ) {
  2929. queryParams.customize_autosaved = 'on';
  2930. }
  2931. urlParser.search = $.param( queryParams );
  2932. // Update loading message. Everything else is handled by reloading the page.
  2933. api.notifications.add( new api.OverlayNotification( 'theme_previewing', {
  2934. message: api.l10n.themePreviewWait,
  2935. type: 'info',
  2936. loading: true
  2937. } ) );
  2938. onceProcessingComplete = function() {
  2939. var request;
  2940. if ( api.state( 'processing' ).get() > 0 ) {
  2941. return;
  2942. }
  2943. api.state( 'processing' ).unbind( onceProcessingComplete );
  2944. request = api.requestChangesetUpdate( {}, { autosave: true } );
  2945. request.done( function() {
  2946. deferred.resolve();
  2947. $( window ).off( 'beforeunload.customize-confirm' );
  2948. location.replace( urlParser.href );
  2949. } );
  2950. request.fail( function() {
  2951. // @todo Show notification regarding failure.
  2952. api.notifications.remove( 'theme_previewing' );
  2953. deferred.reject();
  2954. } );
  2955. };
  2956. if ( 0 === api.state( 'processing' ).get() ) {
  2957. onceProcessingComplete();
  2958. } else {
  2959. api.state( 'processing' ).bind( onceProcessingComplete );
  2960. }
  2961. return deferred.promise();
  2962. },
  2963. /**
  2964. * Update a theme via wp.updates.
  2965. *
  2966. * @since 4.9.0
  2967. *
  2968. * @param {jQuery.Event} event - Event.
  2969. * @returns {void}
  2970. */
  2971. updateTheme: function( event ) {
  2972. wp.updates.maybeRequestFilesystemCredentials( event );
  2973. $( document ).one( 'wp-theme-update-success', function( e, response ) {
  2974. // Rerender the control to reflect the update.
  2975. api.control.each( function( control ) {
  2976. if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
  2977. control.params.theme.hasUpdate = false;
  2978. control.params.theme.version = response.newVersion;
  2979. setTimeout( function() {
  2980. control.rerenderAsInstalled( true );
  2981. }, 2000 );
  2982. }
  2983. });
  2984. } );
  2985. wp.updates.updateTheme( {
  2986. slug: $( event.target ).closest( '.notice' ).data( 'slug' )
  2987. } );
  2988. },
  2989. /**
  2990. * Delete a theme via wp.updates.
  2991. *
  2992. * @since 4.9.0
  2993. *
  2994. * @param {jQuery.Event} event - Event.
  2995. * @returns {void}
  2996. */
  2997. deleteTheme: function( event ) {
  2998. var theme, section;
  2999. theme = $( event.target ).data( 'slug' );
  3000. section = api.section( 'installed_themes' );
  3001. event.preventDefault();
  3002. // Temporary since supplying SFTP credentials does not work yet. See #42184.
  3003. if ( api.settings.theme._filesystemCredentialsNeeded ) {
  3004. return;
  3005. }
  3006. // Confirmation dialog for deleting a theme.
  3007. if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
  3008. return;
  3009. }
  3010. wp.updates.maybeRequestFilesystemCredentials( event );
  3011. $( document ).one( 'wp-theme-delete-success', function() {
  3012. var control = api.control( 'installed_theme_' + theme );
  3013. // Remove theme control.
  3014. control.container.remove();
  3015. api.control.remove( control.id );
  3016. // Update installed count.
  3017. section.loaded = section.loaded - 1;
  3018. section.updateCount();
  3019. // Rerender any other theme controls as uninstalled.
  3020. api.control.each( function( control ) {
  3021. if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
  3022. control.rerenderAsInstalled( false );
  3023. }
  3024. });
  3025. } );
  3026. wp.updates.deleteTheme( {
  3027. slug: theme
  3028. } );
  3029. // Close modal and focus the section.
  3030. section.closeDetails();
  3031. section.focus();
  3032. }
  3033. });
  3034. api.Control = api.Class.extend(/** @lends wp.customize.Control.prototype */{
  3035. defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  3036. /**
  3037. * Default params.
  3038. *
  3039. * @since 4.9.0
  3040. * @var {object}
  3041. */
  3042. defaults: {
  3043. label: '',
  3044. description: '',
  3045. active: true,
  3046. priority: 10
  3047. },
  3048. /**
  3049. * A Customizer Control.
  3050. *
  3051. * A control provides a UI element that allows a user to modify a Customizer Setting.
  3052. *
  3053. * @see PHP class WP_Customize_Control.
  3054. *
  3055. * @constructs wp.customize.Control
  3056. * @augments wp.customize.Class
  3057. *
  3058. * @borrows wp.customize~focus as this#focus
  3059. * @borrows wp.customize~Container#activate as this#activate
  3060. * @borrows wp.customize~Container#deactivate as this#deactivate
  3061. * @borrows wp.customize~Container#_toggleActive as this#_toggleActive
  3062. *
  3063. * @param {string} id - Unique identifier for the control instance.
  3064. * @param {object} options - Options hash for the control instance.
  3065. * @param {object} options.type - Type of control (e.g. text, radio, dropdown-pages, etc.)
  3066. * @param {string} [options.content] - The HTML content for the control or at least its container. This should normally be left blank and instead supplying a templateId.
  3067. * @param {string} [options.templateId] - Template ID for control's content.
  3068. * @param {string} [options.priority=10] - Order of priority to show the control within the section.
  3069. * @param {string} [options.active=true] - Whether the control is active.
  3070. * @param {string} options.section - The ID of the section the control belongs to.
  3071. * @param {mixed} [options.setting] - The ID of the main setting or an instance of this setting.
  3072. * @param {mixed} options.settings - An object with keys (e.g. default) that maps to setting IDs or Setting/Value objects, or an array of setting IDs or Setting/Value objects.
  3073. * @param {mixed} options.settings.default - The ID of the setting the control relates to.
  3074. * @param {string} options.settings.data - @todo Is this used?
  3075. * @param {string} options.label - Label.
  3076. * @param {string} options.description - Description.
  3077. * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances.
  3078. * @param {object} [options.params] - Deprecated wrapper for the above properties.
  3079. * @returns {void}
  3080. */
  3081. initialize: function( id, options ) {
  3082. var control = this, deferredSettingIds = [], settings, gatherSettings;
  3083. control.params = _.extend(
  3084. {},
  3085. control.defaults,
  3086. control.params || {}, // In case sub-class already defines.
  3087. options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat.
  3088. );
  3089. if ( ! api.Control.instanceCounter ) {
  3090. api.Control.instanceCounter = 0;
  3091. }
  3092. api.Control.instanceCounter++;
  3093. if ( ! control.params.instanceNumber ) {
  3094. control.params.instanceNumber = api.Control.instanceCounter;
  3095. }
  3096. // Look up the type if one was not supplied.
  3097. if ( ! control.params.type ) {
  3098. _.find( api.controlConstructor, function( Constructor, type ) {
  3099. if ( Constructor === control.constructor ) {
  3100. control.params.type = type;
  3101. return true;
  3102. }
  3103. return false;
  3104. } );
  3105. }
  3106. if ( ! control.params.content ) {
  3107. control.params.content = $( '<li></li>', {
  3108. id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ),
  3109. 'class': 'customize-control customize-control-' + control.params.type
  3110. } );
  3111. }
  3112. control.id = id;
  3113. control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709.
  3114. if ( control.params.content ) {
  3115. control.container = $( control.params.content );
  3116. } else {
  3117. control.container = $( control.selector ); // Likely dead, per above. See #28709.
  3118. }
  3119. if ( control.params.templateId ) {
  3120. control.templateSelector = control.params.templateId;
  3121. } else {
  3122. control.templateSelector = 'customize-control-' + control.params.type + '-content';
  3123. }
  3124. control.deferred = _.extend( control.deferred || {}, {
  3125. embedded: new $.Deferred()
  3126. } );
  3127. control.section = new api.Value();
  3128. control.priority = new api.Value();
  3129. control.active = new api.Value();
  3130. control.activeArgumentsQueue = [];
  3131. control.notifications = new api.Notifications({
  3132. alt: control.altNotice
  3133. });
  3134. control.elements = [];
  3135. control.active.bind( function ( active ) {
  3136. var args = control.activeArgumentsQueue.shift();
  3137. args = $.extend( {}, control.defaultActiveArguments, args );
  3138. control.onChangeActive( active, args );
  3139. } );
  3140. control.section.set( control.params.section );
  3141. control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
  3142. control.active.set( control.params.active );
  3143. api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
  3144. control.settings = {};
  3145. settings = {};
  3146. if ( control.params.setting ) {
  3147. settings['default'] = control.params.setting;
  3148. }
  3149. _.extend( settings, control.params.settings );
  3150. // Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects.
  3151. _.each( settings, function( value, key ) {
  3152. var setting;
  3153. if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) {
  3154. control.settings[ key ] = value;
  3155. } else if ( _.isString( value ) ) {
  3156. setting = api( value );
  3157. if ( setting ) {
  3158. control.settings[ key ] = setting;
  3159. } else {
  3160. deferredSettingIds.push( value );
  3161. }
  3162. }
  3163. } );
  3164. gatherSettings = function() {
  3165. // Fill-in all resolved settings.
  3166. _.each( settings, function ( settingId, key ) {
  3167. if ( ! control.settings[ key ] && _.isString( settingId ) ) {
  3168. control.settings[ key ] = api( settingId );
  3169. }
  3170. } );
  3171. // Make sure settings passed as array gets associated with default.
  3172. if ( control.settings[0] && ! control.settings['default'] ) {
  3173. control.settings['default'] = control.settings[0];
  3174. }
  3175. // Identify the main setting.
  3176. control.setting = control.settings['default'] || null;
  3177. control.linkElements(); // Link initial elements present in server-rendered content.
  3178. control.embed();
  3179. };
  3180. if ( 0 === deferredSettingIds.length ) {
  3181. gatherSettings();
  3182. } else {
  3183. api.apply( api, deferredSettingIds.concat( gatherSettings ) );
  3184. }
  3185. // After the control is embedded on the page, invoke the "ready" method.
  3186. control.deferred.embedded.done( function () {
  3187. control.linkElements(); // Link any additional elements after template is rendered by renderContent().
  3188. control.setupNotifications();
  3189. control.ready();
  3190. });
  3191. },
  3192. /**
  3193. * Link elements between settings and inputs.
  3194. *
  3195. * @since 4.7.0
  3196. * @access public
  3197. *
  3198. * @returns {void}
  3199. */
  3200. linkElements: function () {
  3201. var control = this, nodes, radios, element;
  3202. nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' );
  3203. radios = {};
  3204. nodes.each( function () {
  3205. var node = $( this ), name, setting;
  3206. if ( node.data( 'customizeSettingLinked' ) ) {
  3207. return;
  3208. }
  3209. node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
  3210. if ( node.is( ':radio' ) ) {
  3211. name = node.prop( 'name' );
  3212. if ( radios[name] ) {
  3213. return;
  3214. }
  3215. radios[name] = true;
  3216. node = nodes.filter( '[name="' + name + '"]' );
  3217. }
  3218. // Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
  3219. if ( node.data( 'customizeSettingLink' ) ) {
  3220. setting = api( node.data( 'customizeSettingLink' ) );
  3221. } else if ( node.data( 'customizeSettingKeyLink' ) ) {
  3222. setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ];
  3223. }
  3224. if ( setting ) {
  3225. element = new api.Element( node );
  3226. control.elements.push( element );
  3227. element.sync( setting );
  3228. element.set( setting() );
  3229. }
  3230. } );
  3231. },
  3232. /**
  3233. * Embed the control into the page.
  3234. */
  3235. embed: function () {
  3236. var control = this,
  3237. inject;
  3238. // Watch for changes to the section state
  3239. inject = function ( sectionId ) {
  3240. var parentContainer;
  3241. if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
  3242. return;
  3243. }
  3244. // Wait for the section to be registered
  3245. api.section( sectionId, function ( section ) {
  3246. // Wait for the section to be ready/initialized
  3247. section.deferred.embedded.done( function () {
  3248. parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  3249. if ( ! control.container.parent().is( parentContainer ) ) {
  3250. parentContainer.append( control.container );
  3251. control.renderContent();
  3252. }
  3253. control.deferred.embedded.resolve();
  3254. });
  3255. });
  3256. };
  3257. control.section.bind( inject );
  3258. inject( control.section.get() );
  3259. },
  3260. /**
  3261. * Triggered when the control's markup has been injected into the DOM.
  3262. *
  3263. * @returns {void}
  3264. */
  3265. ready: function() {
  3266. var control = this, newItem;
  3267. if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
  3268. newItem = control.container.find( '.new-content-item' );
  3269. newItem.hide(); // Hide in JS to preserve flex display when showing.
  3270. control.container.on( 'click', '.add-new-toggle', function( e ) {
  3271. $( e.currentTarget ).slideUp( 180 );
  3272. newItem.slideDown( 180 );
  3273. newItem.find( '.create-item-input' ).focus();
  3274. });
  3275. control.container.on( 'click', '.add-content', function() {
  3276. control.addNewPage();
  3277. });
  3278. control.container.on( 'keydown', '.create-item-input', function( e ) {
  3279. if ( 13 === e.which ) { // Enter
  3280. control.addNewPage();
  3281. }
  3282. });
  3283. }
  3284. },
  3285. /**
  3286. * Get the element inside of a control's container that contains the validation error message.
  3287. *
  3288. * Control subclasses may override this to return the proper container to render notifications into.
  3289. * Injects the notification container for existing controls that lack the necessary container,
  3290. * including special handling for nav menu items and widgets.
  3291. *
  3292. * @since 4.6.0
  3293. * @returns {jQuery} Setting validation message element.
  3294. */
  3295. getNotificationsContainerElement: function() {
  3296. var control = this, controlTitle, notificationsContainer;
  3297. notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
  3298. if ( notificationsContainer.length ) {
  3299. return notificationsContainer;
  3300. }
  3301. notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
  3302. if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
  3303. control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
  3304. } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
  3305. control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
  3306. } else {
  3307. controlTitle = control.container.find( '.customize-control-title' );
  3308. if ( controlTitle.length ) {
  3309. controlTitle.after( notificationsContainer );
  3310. } else {
  3311. control.container.prepend( notificationsContainer );
  3312. }
  3313. }
  3314. return notificationsContainer;
  3315. },
  3316. /**
  3317. * Set up notifications.
  3318. *
  3319. * @since 4.9.0
  3320. * @returns {void}
  3321. */
  3322. setupNotifications: function() {
  3323. var control = this, renderNotificationsIfVisible, onSectionAssigned;
  3324. // Add setting notifications to the control notification.
  3325. _.each( control.settings, function( setting ) {
  3326. if ( ! setting.notifications ) {
  3327. return;
  3328. }
  3329. setting.notifications.bind( 'add', function( settingNotification ) {
  3330. var params = _.extend(
  3331. {},
  3332. settingNotification,
  3333. {
  3334. setting: setting.id
  3335. }
  3336. );
  3337. control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) );
  3338. } );
  3339. setting.notifications.bind( 'remove', function( settingNotification ) {
  3340. control.notifications.remove( setting.id + ':' + settingNotification.code );
  3341. } );
  3342. } );
  3343. renderNotificationsIfVisible = function() {
  3344. var sectionId = control.section();
  3345. if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
  3346. control.notifications.render();
  3347. }
  3348. };
  3349. control.notifications.bind( 'rendered', function() {
  3350. var notifications = control.notifications.get();
  3351. control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
  3352. control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
  3353. } );
  3354. onSectionAssigned = function( newSectionId, oldSectionId ) {
  3355. if ( oldSectionId && api.section.has( oldSectionId ) ) {
  3356. api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible );
  3357. }
  3358. if ( newSectionId ) {
  3359. api.section( newSectionId, function( section ) {
  3360. section.expanded.bind( renderNotificationsIfVisible );
  3361. renderNotificationsIfVisible();
  3362. });
  3363. }
  3364. };
  3365. control.section.bind( onSectionAssigned );
  3366. onSectionAssigned( control.section.get() );
  3367. control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) );
  3368. },
  3369. /**
  3370. * Render notifications.
  3371. *
  3372. * Renders the `control.notifications` into the control's container.
  3373. * Control subclasses may override this method to do their own handling
  3374. * of rendering notifications.
  3375. *
  3376. * @deprecated in favor of `control.notifications.render()`
  3377. * @since 4.6.0
  3378. * @this {wp.customize.Control}
  3379. */
  3380. renderNotifications: function() {
  3381. var control = this, container, notifications, hasError = false;
  3382. if ( 'undefined' !== typeof console && console.warn ) {
  3383. console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' );
  3384. }
  3385. container = control.getNotificationsContainerElement();
  3386. if ( ! container || ! container.length ) {
  3387. return;
  3388. }
  3389. notifications = [];
  3390. control.notifications.each( function( notification ) {
  3391. notifications.push( notification );
  3392. if ( 'error' === notification.type ) {
  3393. hasError = true;
  3394. }
  3395. } );
  3396. if ( 0 === notifications.length ) {
  3397. container.stop().slideUp( 'fast' );
  3398. } else {
  3399. container.stop().slideDown( 'fast', null, function() {
  3400. $( this ).css( 'height', 'auto' );
  3401. } );
  3402. }
  3403. if ( ! control.notificationsTemplate ) {
  3404. control.notificationsTemplate = wp.template( 'customize-control-notifications' );
  3405. }
  3406. control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
  3407. control.container.toggleClass( 'has-error', hasError );
  3408. container.empty().append( $.trim(
  3409. control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
  3410. ) );
  3411. },
  3412. /**
  3413. * Normal controls do not expand, so just expand its parent
  3414. *
  3415. * @param {Object} [params]
  3416. */
  3417. expand: function ( params ) {
  3418. api.section( this.section() ).expand( params );
  3419. },
  3420. /*
  3421. * Documented using @borrows in the constructor.
  3422. */
  3423. focus: focus,
  3424. /**
  3425. * Update UI in response to a change in the control's active state.
  3426. * This does not change the active state, it merely handles the behavior
  3427. * for when it does change.
  3428. *
  3429. * @since 4.1.0
  3430. *
  3431. * @param {Boolean} active
  3432. * @param {Object} args
  3433. * @param {Number} args.duration
  3434. * @param {Function} args.completeCallback
  3435. */
  3436. onChangeActive: function ( active, args ) {
  3437. if ( args.unchanged ) {
  3438. if ( args.completeCallback ) {
  3439. args.completeCallback();
  3440. }
  3441. return;
  3442. }
  3443. if ( ! $.contains( document, this.container[0] ) ) {
  3444. // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
  3445. this.container.toggle( active );
  3446. if ( args.completeCallback ) {
  3447. args.completeCallback();
  3448. }
  3449. } else if ( active ) {
  3450. this.container.slideDown( args.duration, args.completeCallback );
  3451. } else {
  3452. this.container.slideUp( args.duration, args.completeCallback );
  3453. }
  3454. },
  3455. /**
  3456. * @deprecated 4.1.0 Use this.onChangeActive() instead.
  3457. */
  3458. toggle: function ( active ) {
  3459. return this.onChangeActive( active, this.defaultActiveArguments );
  3460. },
  3461. /*
  3462. * Documented using @borrows in the constructor
  3463. */
  3464. activate: Container.prototype.activate,
  3465. /*
  3466. * Documented using @borrows in the constructor
  3467. */
  3468. deactivate: Container.prototype.deactivate,
  3469. /*
  3470. * Documented using @borrows in the constructor
  3471. */
  3472. _toggleActive: Container.prototype._toggleActive,
  3473. // @todo This function appears to be dead code and can be removed.
  3474. dropdownInit: function() {
  3475. var control = this,
  3476. statuses = this.container.find('.dropdown-status'),
  3477. params = this.params,
  3478. toggleFreeze = false,
  3479. update = function( to ) {
  3480. if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) {
  3481. statuses.html( params.statuses[ to ] ).show();
  3482. } else {
  3483. statuses.hide();
  3484. }
  3485. };
  3486. // Support the .dropdown class to open/close complex elements
  3487. this.container.on( 'click keydown', '.dropdown', function( event ) {
  3488. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3489. return;
  3490. }
  3491. event.preventDefault();
  3492. if ( ! toggleFreeze ) {
  3493. control.container.toggleClass( 'open' );
  3494. }
  3495. if ( control.container.hasClass( 'open' ) ) {
  3496. control.container.parent().parent().find( 'li.library-selected' ).focus();
  3497. }
  3498. // Don't want to fire focus and click at same time
  3499. toggleFreeze = true;
  3500. setTimeout(function () {
  3501. toggleFreeze = false;
  3502. }, 400);
  3503. });
  3504. this.setting.bind( update );
  3505. update( this.setting() );
  3506. },
  3507. /**
  3508. * Render the control from its JS template, if it exists.
  3509. *
  3510. * The control's container must already exist in the DOM.
  3511. *
  3512. * @since 4.1.0
  3513. */
  3514. renderContent: function () {
  3515. var control = this, template, standardTypes, templateId, sectionId;
  3516. standardTypes = [
  3517. 'button',
  3518. 'checkbox',
  3519. 'date',
  3520. 'datetime-local',
  3521. 'email',
  3522. 'month',
  3523. 'number',
  3524. 'password',
  3525. 'radio',
  3526. 'range',
  3527. 'search',
  3528. 'select',
  3529. 'tel',
  3530. 'time',
  3531. 'text',
  3532. 'textarea',
  3533. 'week',
  3534. 'url'
  3535. ];
  3536. templateId = control.templateSelector;
  3537. // Use default content template when a standard HTML type is used, there isn't a more specific template existing, and the control container is empty.
  3538. if ( templateId === 'customize-control-' + control.params.type + '-content' &&
  3539. _.contains( standardTypes, control.params.type ) &&
  3540. ! document.getElementById( 'tmpl-' + templateId ) &&
  3541. 0 === control.container.children().length )
  3542. {
  3543. templateId = 'customize-control-default-content';
  3544. }
  3545. // Replace the container element's content with the control.
  3546. if ( document.getElementById( 'tmpl-' + templateId ) ) {
  3547. template = wp.template( templateId );
  3548. if ( template && control.container ) {
  3549. control.container.html( template( control.params ) );
  3550. }
  3551. }
  3552. // Re-render notifications after content has been re-rendered.
  3553. control.notifications.container = control.getNotificationsContainerElement();
  3554. sectionId = control.section();
  3555. if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
  3556. control.notifications.render();
  3557. }
  3558. },
  3559. /**
  3560. * Add a new page to a dropdown-pages control reusing menus code for this.
  3561. *
  3562. * @since 4.7.0
  3563. * @access private
  3564. * @returns {void}
  3565. */
  3566. addNewPage: function () {
  3567. var control = this, promise, toggle, container, input, title, select;
  3568. if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
  3569. return;
  3570. }
  3571. toggle = control.container.find( '.add-new-toggle' );
  3572. container = control.container.find( '.new-content-item' );
  3573. input = control.container.find( '.create-item-input' );
  3574. title = input.val();
  3575. select = control.container.find( 'select' );
  3576. if ( ! title ) {
  3577. input.addClass( 'invalid' );
  3578. return;
  3579. }
  3580. input.removeClass( 'invalid' );
  3581. input.attr( 'disabled', 'disabled' );
  3582. // The menus functions add the page, publish when appropriate, and also add the new page to the dropdown-pages controls.
  3583. promise = api.Menus.insertAutoDraftPost( {
  3584. post_title: title,
  3585. post_type: 'page'
  3586. } );
  3587. promise.done( function( data ) {
  3588. var availableItem, $content, itemTemplate;
  3589. // Prepare the new page as an available menu item.
  3590. // See api.Menus.submitNew().
  3591. availableItem = new api.Menus.AvailableItemModel( {
  3592. 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
  3593. 'title': title,
  3594. 'type': 'post_type',
  3595. 'type_label': api.Menus.data.l10n.page_label,
  3596. 'object': 'page',
  3597. 'object_id': data.post_id,
  3598. 'url': data.url
  3599. } );
  3600. // Add the new item to the list of available menu items.
  3601. api.Menus.availableMenuItemsPanel.collection.add( availableItem );
  3602. $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
  3603. itemTemplate = wp.template( 'available-menu-item' );
  3604. $content.prepend( itemTemplate( availableItem.attributes ) );
  3605. // Focus the select control.
  3606. select.focus();
  3607. control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
  3608. // Reset the create page form.
  3609. container.slideUp( 180 );
  3610. toggle.slideDown( 180 );
  3611. } );
  3612. promise.always( function() {
  3613. input.val( '' ).removeAttr( 'disabled' );
  3614. } );
  3615. }
  3616. });
  3617. /**
  3618. * A colorpicker control.
  3619. *
  3620. * @class wp.customize.ColorControl
  3621. * @augments wp.customize.Control
  3622. */
  3623. api.ColorControl = api.Control.extend(/** @lends wp.customize.ColorControl.prototype */{
  3624. ready: function() {
  3625. var control = this,
  3626. isHueSlider = this.params.mode === 'hue',
  3627. updating = false,
  3628. picker;
  3629. if ( isHueSlider ) {
  3630. picker = this.container.find( '.color-picker-hue' );
  3631. picker.val( control.setting() ).wpColorPicker({
  3632. change: function( event, ui ) {
  3633. updating = true;
  3634. control.setting( ui.color.h() );
  3635. updating = false;
  3636. }
  3637. });
  3638. } else {
  3639. picker = this.container.find( '.color-picker-hex' );
  3640. picker.val( control.setting() ).wpColorPicker({
  3641. change: function() {
  3642. updating = true;
  3643. control.setting.set( picker.wpColorPicker( 'color' ) );
  3644. updating = false;
  3645. },
  3646. clear: function() {
  3647. updating = true;
  3648. control.setting.set( '' );
  3649. updating = false;
  3650. }
  3651. });
  3652. }
  3653. control.setting.bind( function ( value ) {
  3654. // Bail if the update came from the control itself.
  3655. if ( updating ) {
  3656. return;
  3657. }
  3658. picker.val( value );
  3659. picker.wpColorPicker( 'color', value );
  3660. } );
  3661. // Collapse color picker when hitting Esc instead of collapsing the current section.
  3662. control.container.on( 'keydown', function( event ) {
  3663. var pickerContainer;
  3664. if ( 27 !== event.which ) { // Esc.
  3665. return;
  3666. }
  3667. pickerContainer = control.container.find( '.wp-picker-container' );
  3668. if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
  3669. picker.wpColorPicker( 'close' );
  3670. control.container.find( '.wp-color-result' ).focus();
  3671. event.stopPropagation(); // Prevent section from being collapsed.
  3672. }
  3673. } );
  3674. }
  3675. });
  3676. /**
  3677. * A control that implements the media modal.
  3678. *
  3679. * @class wp.customize.MediaControl
  3680. * @augments wp.customize.Control
  3681. */
  3682. api.MediaControl = api.Control.extend(/** @lends wp.customize.MediaControl.prototype */{
  3683. /**
  3684. * When the control's DOM structure is ready,
  3685. * set up internal event bindings.
  3686. */
  3687. ready: function() {
  3688. var control = this;
  3689. // Shortcut so that we don't have to use _.bind every time we add a callback.
  3690. _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
  3691. // Bind events, with delegation to facilitate re-rendering.
  3692. control.container.on( 'click keydown', '.upload-button', control.openFrame );
  3693. control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
  3694. control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
  3695. control.container.on( 'click keydown', '.default-button', control.restoreDefault );
  3696. control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
  3697. control.container.on( 'click keydown', '.remove-button', control.removeFile );
  3698. control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
  3699. // Resize the player controls when it becomes visible (ie when section is expanded)
  3700. api.section( control.section() ).container
  3701. .on( 'expanded', function() {
  3702. if ( control.player ) {
  3703. control.player.setControlsSize();
  3704. }
  3705. })
  3706. .on( 'collapsed', function() {
  3707. control.pausePlayer();
  3708. });
  3709. /**
  3710. * Set attachment data and render content.
  3711. *
  3712. * Note that BackgroundImage.prototype.ready applies this ready method
  3713. * to itself. Since BackgroundImage is an UploadControl, the value
  3714. * is the attachment URL instead of the attachment ID. In this case
  3715. * we skip fetching the attachment data because we have no ID available,
  3716. * and it is the responsibility of the UploadControl to set the control's
  3717. * attachmentData before calling the renderContent method.
  3718. *
  3719. * @param {number|string} value Attachment
  3720. */
  3721. function setAttachmentDataAndRenderContent( value ) {
  3722. var hasAttachmentData = $.Deferred();
  3723. if ( control.extended( api.UploadControl ) ) {
  3724. hasAttachmentData.resolve();
  3725. } else {
  3726. value = parseInt( value, 10 );
  3727. if ( _.isNaN( value ) || value <= 0 ) {
  3728. delete control.params.attachment;
  3729. hasAttachmentData.resolve();
  3730. } else if ( control.params.attachment && control.params.attachment.id === value ) {
  3731. hasAttachmentData.resolve();
  3732. }
  3733. }
  3734. // Fetch the attachment data.
  3735. if ( 'pending' === hasAttachmentData.state() ) {
  3736. wp.media.attachment( value ).fetch().done( function() {
  3737. control.params.attachment = this.attributes;
  3738. hasAttachmentData.resolve();
  3739. // Send attachment information to the preview for possible use in `postMessage` transport.
  3740. wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
  3741. } );
  3742. }
  3743. hasAttachmentData.done( function() {
  3744. control.renderContent();
  3745. } );
  3746. }
  3747. // Ensure attachment data is initially set (for dynamically-instantiated controls).
  3748. setAttachmentDataAndRenderContent( control.setting() );
  3749. // Update the attachment data and re-render the control when the setting changes.
  3750. control.setting.bind( setAttachmentDataAndRenderContent );
  3751. },
  3752. pausePlayer: function () {
  3753. this.player && this.player.pause();
  3754. },
  3755. cleanupPlayer: function () {
  3756. this.player && wp.media.mixin.removePlayer( this.player );
  3757. },
  3758. /**
  3759. * Open the media modal.
  3760. */
  3761. openFrame: function( event ) {
  3762. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3763. return;
  3764. }
  3765. event.preventDefault();
  3766. if ( ! this.frame ) {
  3767. this.initFrame();
  3768. }
  3769. this.frame.open();
  3770. },
  3771. /**
  3772. * Create a media modal select frame, and store it so the instance can be reused when needed.
  3773. */
  3774. initFrame: function() {
  3775. this.frame = wp.media({
  3776. button: {
  3777. text: this.params.button_labels.frame_button
  3778. },
  3779. states: [
  3780. new wp.media.controller.Library({
  3781. title: this.params.button_labels.frame_title,
  3782. library: wp.media.query({ type: this.params.mime_type }),
  3783. multiple: false,
  3784. date: false
  3785. })
  3786. ]
  3787. });
  3788. // When a file is selected, run a callback.
  3789. this.frame.on( 'select', this.select );
  3790. },
  3791. /**
  3792. * Callback handler for when an attachment is selected in the media modal.
  3793. * Gets the selected image information, and sets it within the control.
  3794. */
  3795. select: function() {
  3796. // Get the attachment from the modal frame.
  3797. var node,
  3798. attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  3799. mejsSettings = window._wpmejsSettings || {};
  3800. this.params.attachment = attachment;
  3801. // Set the Customizer setting; the callback takes care of rendering.
  3802. this.setting( attachment.id );
  3803. node = this.container.find( 'audio, video' ).get(0);
  3804. // Initialize audio/video previews.
  3805. if ( node ) {
  3806. this.player = new MediaElementPlayer( node, mejsSettings );
  3807. } else {
  3808. this.cleanupPlayer();
  3809. }
  3810. },
  3811. /**
  3812. * Reset the setting to the default value.
  3813. */
  3814. restoreDefault: function( event ) {
  3815. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3816. return;
  3817. }
  3818. event.preventDefault();
  3819. this.params.attachment = this.params.defaultAttachment;
  3820. this.setting( this.params.defaultAttachment.url );
  3821. },
  3822. /**
  3823. * Called when the "Remove" link is clicked. Empties the setting.
  3824. *
  3825. * @param {object} event jQuery Event object
  3826. */
  3827. removeFile: function( event ) {
  3828. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3829. return;
  3830. }
  3831. event.preventDefault();
  3832. this.params.attachment = {};
  3833. this.setting( '' );
  3834. this.renderContent(); // Not bound to setting change when emptying.
  3835. }
  3836. });
  3837. /**
  3838. * An upload control, which utilizes the media modal.
  3839. *
  3840. * @class wp.customize.UploadControl
  3841. * @augments wp.customize.MediaControl
  3842. */
  3843. api.UploadControl = api.MediaControl.extend(/** @lends wp.customize.UploadControl.prototype */{
  3844. /**
  3845. * Callback handler for when an attachment is selected in the media modal.
  3846. * Gets the selected image information, and sets it within the control.
  3847. */
  3848. select: function() {
  3849. // Get the attachment from the modal frame.
  3850. var node,
  3851. attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  3852. mejsSettings = window._wpmejsSettings || {};
  3853. this.params.attachment = attachment;
  3854. // Set the Customizer setting; the callback takes care of rendering.
  3855. this.setting( attachment.url );
  3856. node = this.container.find( 'audio, video' ).get(0);
  3857. // Initialize audio/video previews.
  3858. if ( node ) {
  3859. this.player = new MediaElementPlayer( node, mejsSettings );
  3860. } else {
  3861. this.cleanupPlayer();
  3862. }
  3863. },
  3864. // @deprecated
  3865. success: function() {},
  3866. // @deprecated
  3867. removerVisibility: function() {}
  3868. });
  3869. /**
  3870. * A control for uploading images.
  3871. *
  3872. * This control no longer needs to do anything more
  3873. * than what the upload control does in JS.
  3874. *
  3875. * @class wp.customize.ImageControl
  3876. * @augments wp.customize.UploadControl
  3877. */
  3878. api.ImageControl = api.UploadControl.extend(/** @lends wp.customize.ImageControl.prototype */{
  3879. // @deprecated
  3880. thumbnailSrc: function() {}
  3881. });
  3882. /**
  3883. * A control for uploading background images.
  3884. *
  3885. * @class wp.customize.BackgroundControl
  3886. * @augments wp.customize.UploadControl
  3887. */
  3888. api.BackgroundControl = api.UploadControl.extend(/** @lends wp.customize.BackgroundControl.prototype */{
  3889. /**
  3890. * When the control's DOM structure is ready,
  3891. * set up internal event bindings.
  3892. */
  3893. ready: function() {
  3894. api.UploadControl.prototype.ready.apply( this, arguments );
  3895. },
  3896. /**
  3897. * Callback handler for when an attachment is selected in the media modal.
  3898. * Does an additional AJAX request for setting the background context.
  3899. */
  3900. select: function() {
  3901. api.UploadControl.prototype.select.apply( this, arguments );
  3902. wp.ajax.post( 'custom-background-add', {
  3903. nonce: _wpCustomizeBackground.nonces.add,
  3904. wp_customize: 'on',
  3905. customize_theme: api.settings.theme.stylesheet,
  3906. attachment_id: this.params.attachment.id
  3907. } );
  3908. }
  3909. });
  3910. /**
  3911. * A control for positioning a background image.
  3912. *
  3913. * @since 4.7.0
  3914. *
  3915. * @class wp.customize.BackgroundPositionControl
  3916. * @augments wp.customize.Control
  3917. */
  3918. api.BackgroundPositionControl = api.Control.extend(/** @lends wp.customize.BackgroundPositionControl.prototype */{
  3919. /**
  3920. * Set up control UI once embedded in DOM and settings are created.
  3921. *
  3922. * @since 4.7.0
  3923. * @access public
  3924. */
  3925. ready: function() {
  3926. var control = this, updateRadios;
  3927. control.container.on( 'change', 'input[name="background-position"]', function() {
  3928. var position = $( this ).val().split( ' ' );
  3929. control.settings.x( position[0] );
  3930. control.settings.y( position[1] );
  3931. } );
  3932. updateRadios = _.debounce( function() {
  3933. var x, y, radioInput, inputValue;
  3934. x = control.settings.x.get();
  3935. y = control.settings.y.get();
  3936. inputValue = String( x ) + ' ' + String( y );
  3937. radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
  3938. radioInput.click();
  3939. } );
  3940. control.settings.x.bind( updateRadios );
  3941. control.settings.y.bind( updateRadios );
  3942. updateRadios(); // Set initial UI.
  3943. }
  3944. } );
  3945. /**
  3946. * A control for selecting and cropping an image.
  3947. *
  3948. * @class wp.customize.CroppedImageControl
  3949. * @augments wp.customize.MediaControl
  3950. */
  3951. api.CroppedImageControl = api.MediaControl.extend(/** @lends wp.customize.CroppedImageControl.prototype */{
  3952. /**
  3953. * Open the media modal to the library state.
  3954. */
  3955. openFrame: function( event ) {
  3956. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3957. return;
  3958. }
  3959. this.initFrame();
  3960. this.frame.setState( 'library' ).open();
  3961. },
  3962. /**
  3963. * Create a media modal select frame, and store it so the instance can be reused when needed.
  3964. */
  3965. initFrame: function() {
  3966. var l10n = _wpMediaViewsL10n;
  3967. this.frame = wp.media({
  3968. button: {
  3969. text: l10n.select,
  3970. close: false
  3971. },
  3972. states: [
  3973. new wp.media.controller.Library({
  3974. title: this.params.button_labels.frame_title,
  3975. library: wp.media.query({ type: 'image' }),
  3976. multiple: false,
  3977. date: false,
  3978. priority: 20,
  3979. suggestedWidth: this.params.width,
  3980. suggestedHeight: this.params.height
  3981. }),
  3982. new wp.media.controller.CustomizeImageCropper({
  3983. imgSelectOptions: this.calculateImageSelectOptions,
  3984. control: this
  3985. })
  3986. ]
  3987. });
  3988. this.frame.on( 'select', this.onSelect, this );
  3989. this.frame.on( 'cropped', this.onCropped, this );
  3990. this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  3991. },
  3992. /**
  3993. * After an image is selected in the media modal, switch to the cropper
  3994. * state if the image isn't the right size.
  3995. */
  3996. onSelect: function() {
  3997. var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  3998. if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  3999. this.setImageFromAttachment( attachment );
  4000. this.frame.close();
  4001. } else {
  4002. this.frame.setState( 'cropper' );
  4003. }
  4004. },
  4005. /**
  4006. * After the image has been cropped, apply the cropped image data to the setting.
  4007. *
  4008. * @param {object} croppedImage Cropped attachment data.
  4009. */
  4010. onCropped: function( croppedImage ) {
  4011. this.setImageFromAttachment( croppedImage );
  4012. },
  4013. /**
  4014. * Returns a set of options, computed from the attached image data and
  4015. * control-specific data, to be fed to the imgAreaSelect plugin in
  4016. * wp.media.view.Cropper.
  4017. *
  4018. * @param {wp.media.model.Attachment} attachment
  4019. * @param {wp.media.controller.Cropper} controller
  4020. * @returns {Object} Options
  4021. */
  4022. calculateImageSelectOptions: function( attachment, controller ) {
  4023. var control = controller.get( 'control' ),
  4024. flexWidth = !! parseInt( control.params.flex_width, 10 ),
  4025. flexHeight = !! parseInt( control.params.flex_height, 10 ),
  4026. realWidth = attachment.get( 'width' ),
  4027. realHeight = attachment.get( 'height' ),
  4028. xInit = parseInt( control.params.width, 10 ),
  4029. yInit = parseInt( control.params.height, 10 ),
  4030. ratio = xInit / yInit,
  4031. xImg = xInit,
  4032. yImg = yInit,
  4033. x1, y1, imgSelectOptions;
  4034. controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
  4035. if ( realWidth / realHeight > ratio ) {
  4036. yInit = realHeight;
  4037. xInit = yInit * ratio;
  4038. } else {
  4039. xInit = realWidth;
  4040. yInit = xInit / ratio;
  4041. }
  4042. x1 = ( realWidth - xInit ) / 2;
  4043. y1 = ( realHeight - yInit ) / 2;
  4044. imgSelectOptions = {
  4045. handles: true,
  4046. keys: true,
  4047. instance: true,
  4048. persistent: true,
  4049. imageWidth: realWidth,
  4050. imageHeight: realHeight,
  4051. minWidth: xImg > xInit ? xInit : xImg,
  4052. minHeight: yImg > yInit ? yInit : yImg,
  4053. x1: x1,
  4054. y1: y1,
  4055. x2: xInit + x1,
  4056. y2: yInit + y1
  4057. };
  4058. if ( flexHeight === false && flexWidth === false ) {
  4059. imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  4060. }
  4061. if ( true === flexHeight ) {
  4062. delete imgSelectOptions.minHeight;
  4063. imgSelectOptions.maxWidth = realWidth;
  4064. }
  4065. if ( true === flexWidth ) {
  4066. delete imgSelectOptions.minWidth;
  4067. imgSelectOptions.maxHeight = realHeight;
  4068. }
  4069. return imgSelectOptions;
  4070. },
  4071. /**
  4072. * Return whether the image must be cropped, based on required dimensions.
  4073. *
  4074. * @param {bool} flexW
  4075. * @param {bool} flexH
  4076. * @param {int} dstW
  4077. * @param {int} dstH
  4078. * @param {int} imgW
  4079. * @param {int} imgH
  4080. * @return {bool}
  4081. */
  4082. mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
  4083. if ( true === flexW && true === flexH ) {
  4084. return false;
  4085. }
  4086. if ( true === flexW && dstH === imgH ) {
  4087. return false;
  4088. }
  4089. if ( true === flexH && dstW === imgW ) {
  4090. return false;
  4091. }
  4092. if ( dstW === imgW && dstH === imgH ) {
  4093. return false;
  4094. }
  4095. if ( imgW <= dstW ) {
  4096. return false;
  4097. }
  4098. return true;
  4099. },
  4100. /**
  4101. * If cropping was skipped, apply the image data directly to the setting.
  4102. */
  4103. onSkippedCrop: function() {
  4104. var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  4105. this.setImageFromAttachment( attachment );
  4106. },
  4107. /**
  4108. * Updates the setting and re-renders the control UI.
  4109. *
  4110. * @param {object} attachment
  4111. */
  4112. setImageFromAttachment: function( attachment ) {
  4113. this.params.attachment = attachment;
  4114. // Set the Customizer setting; the callback takes care of rendering.
  4115. this.setting( attachment.id );
  4116. }
  4117. });
  4118. /**
  4119. * A control for selecting and cropping Site Icons.
  4120. *
  4121. * @class wp.customize.SiteIconControl
  4122. * @augments wp.customize.CroppedImageControl
  4123. */
  4124. api.SiteIconControl = api.CroppedImageControl.extend(/** @lends wp.customize.SiteIconControl.prototype */{
  4125. /**
  4126. * Create a media modal select frame, and store it so the instance can be reused when needed.
  4127. */
  4128. initFrame: function() {
  4129. var l10n = _wpMediaViewsL10n;
  4130. this.frame = wp.media({
  4131. button: {
  4132. text: l10n.select,
  4133. close: false
  4134. },
  4135. states: [
  4136. new wp.media.controller.Library({
  4137. title: this.params.button_labels.frame_title,
  4138. library: wp.media.query({ type: 'image' }),
  4139. multiple: false,
  4140. date: false,
  4141. priority: 20,
  4142. suggestedWidth: this.params.width,
  4143. suggestedHeight: this.params.height
  4144. }),
  4145. new wp.media.controller.SiteIconCropper({
  4146. imgSelectOptions: this.calculateImageSelectOptions,
  4147. control: this
  4148. })
  4149. ]
  4150. });
  4151. this.frame.on( 'select', this.onSelect, this );
  4152. this.frame.on( 'cropped', this.onCropped, this );
  4153. this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  4154. },
  4155. /**
  4156. * After an image is selected in the media modal, switch to the cropper
  4157. * state if the image isn't the right size.
  4158. */
  4159. onSelect: function() {
  4160. var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  4161. controller = this;
  4162. if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  4163. wp.ajax.post( 'crop-image', {
  4164. nonce: attachment.nonces.edit,
  4165. id: attachment.id,
  4166. context: 'site-icon',
  4167. cropDetails: {
  4168. x1: 0,
  4169. y1: 0,
  4170. width: this.params.width,
  4171. height: this.params.height,
  4172. dst_width: this.params.width,
  4173. dst_height: this.params.height
  4174. }
  4175. } ).done( function( croppedImage ) {
  4176. controller.setImageFromAttachment( croppedImage );
  4177. controller.frame.close();
  4178. } ).fail( function() {
  4179. controller.frame.trigger('content:error:crop');
  4180. } );
  4181. } else {
  4182. this.frame.setState( 'cropper' );
  4183. }
  4184. },
  4185. /**
  4186. * Updates the setting and re-renders the control UI.
  4187. *
  4188. * @param {object} attachment
  4189. */
  4190. setImageFromAttachment: function( attachment ) {
  4191. var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
  4192. icon;
  4193. _.each( sizes, function( size ) {
  4194. if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
  4195. icon = attachment.sizes[ size ];
  4196. }
  4197. } );
  4198. this.params.attachment = attachment;
  4199. // Set the Customizer setting; the callback takes care of rendering.
  4200. this.setting( attachment.id );
  4201. if ( ! icon ) {
  4202. return;
  4203. }
  4204. // Update the icon in-browser.
  4205. link = $( 'link[rel="icon"][sizes="32x32"]' );
  4206. link.attr( 'href', icon.url );
  4207. },
  4208. /**
  4209. * Called when the "Remove" link is clicked. Empties the setting.
  4210. *
  4211. * @param {object} event jQuery Event object
  4212. */
  4213. removeFile: function( event ) {
  4214. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4215. return;
  4216. }
  4217. event.preventDefault();
  4218. this.params.attachment = {};
  4219. this.setting( '' );
  4220. this.renderContent(); // Not bound to setting change when emptying.
  4221. $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
  4222. }
  4223. });
  4224. /**
  4225. * @class wp.customize.HeaderControl
  4226. * @augments wp.customize.Control
  4227. */
  4228. api.HeaderControl = api.Control.extend(/** @lends wp.customize.HeaderControl.prototype */{
  4229. ready: function() {
  4230. this.btnRemove = $('#customize-control-header_image .actions .remove');
  4231. this.btnNew = $('#customize-control-header_image .actions .new');
  4232. _.bindAll(this, 'openMedia', 'removeImage');
  4233. this.btnNew.on( 'click', this.openMedia );
  4234. this.btnRemove.on( 'click', this.removeImage );
  4235. api.HeaderTool.currentHeader = this.getInitialHeaderImage();
  4236. new api.HeaderTool.CurrentView({
  4237. model: api.HeaderTool.currentHeader,
  4238. el: '#customize-control-header_image .current .container'
  4239. });
  4240. new api.HeaderTool.ChoiceListView({
  4241. collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
  4242. el: '#customize-control-header_image .choices .uploaded .list'
  4243. });
  4244. new api.HeaderTool.ChoiceListView({
  4245. collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
  4246. el: '#customize-control-header_image .choices .default .list'
  4247. });
  4248. api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
  4249. api.HeaderTool.UploadsList,
  4250. api.HeaderTool.DefaultsList
  4251. ]);
  4252. // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
  4253. wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
  4254. wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
  4255. },
  4256. /**
  4257. * Returns a new instance of api.HeaderTool.ImageModel based on the currently
  4258. * saved header image (if any).
  4259. *
  4260. * @since 4.2.0
  4261. *
  4262. * @returns {Object} Options
  4263. */
  4264. getInitialHeaderImage: function() {
  4265. if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
  4266. return new api.HeaderTool.ImageModel();
  4267. }
  4268. // Get the matching uploaded image object.
  4269. var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
  4270. return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
  4271. } );
  4272. // Fall back to raw current header image.
  4273. if ( ! currentHeaderObject ) {
  4274. currentHeaderObject = {
  4275. url: api.get().header_image,
  4276. thumbnail_url: api.get().header_image,
  4277. attachment_id: api.get().header_image_data.attachment_id
  4278. };
  4279. }
  4280. return new api.HeaderTool.ImageModel({
  4281. header: currentHeaderObject,
  4282. choice: currentHeaderObject.url.split( '/' ).pop()
  4283. });
  4284. },
  4285. /**
  4286. * Returns a set of options, computed from the attached image data and
  4287. * theme-specific data, to be fed to the imgAreaSelect plugin in
  4288. * wp.media.view.Cropper.
  4289. *
  4290. * @param {wp.media.model.Attachment} attachment
  4291. * @param {wp.media.controller.Cropper} controller
  4292. * @returns {Object} Options
  4293. */
  4294. calculateImageSelectOptions: function(attachment, controller) {
  4295. var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
  4296. yInit = parseInt(_wpCustomizeHeader.data.height, 10),
  4297. flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
  4298. flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
  4299. ratio, xImg, yImg, realHeight, realWidth,
  4300. imgSelectOptions;
  4301. realWidth = attachment.get('width');
  4302. realHeight = attachment.get('height');
  4303. this.headerImage = new api.HeaderTool.ImageModel();
  4304. this.headerImage.set({
  4305. themeWidth: xInit,
  4306. themeHeight: yInit,
  4307. themeFlexWidth: flexWidth,
  4308. themeFlexHeight: flexHeight,
  4309. imageWidth: realWidth,
  4310. imageHeight: realHeight
  4311. });
  4312. controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
  4313. ratio = xInit / yInit;
  4314. xImg = realWidth;
  4315. yImg = realHeight;
  4316. if ( xImg / yImg > ratio ) {
  4317. yInit = yImg;
  4318. xInit = yInit * ratio;
  4319. } else {
  4320. xInit = xImg;
  4321. yInit = xInit / ratio;
  4322. }
  4323. imgSelectOptions = {
  4324. handles: true,
  4325. keys: true,
  4326. instance: true,
  4327. persistent: true,
  4328. imageWidth: realWidth,
  4329. imageHeight: realHeight,
  4330. x1: 0,
  4331. y1: 0,
  4332. x2: xInit,
  4333. y2: yInit
  4334. };
  4335. if (flexHeight === false && flexWidth === false) {
  4336. imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  4337. }
  4338. if (flexHeight === false ) {
  4339. imgSelectOptions.maxHeight = yInit;
  4340. }
  4341. if (flexWidth === false ) {
  4342. imgSelectOptions.maxWidth = xInit;
  4343. }
  4344. return imgSelectOptions;
  4345. },
  4346. /**
  4347. * Sets up and opens the Media Manager in order to select an image.
  4348. * Depending on both the size of the image and the properties of the
  4349. * current theme, a cropping step after selection may be required or
  4350. * skippable.
  4351. *
  4352. * @param {event} event
  4353. */
  4354. openMedia: function(event) {
  4355. var l10n = _wpMediaViewsL10n;
  4356. event.preventDefault();
  4357. this.frame = wp.media({
  4358. button: {
  4359. text: l10n.selectAndCrop,
  4360. close: false
  4361. },
  4362. states: [
  4363. new wp.media.controller.Library({
  4364. title: l10n.chooseImage,
  4365. library: wp.media.query({ type: 'image' }),
  4366. multiple: false,
  4367. date: false,
  4368. priority: 20,
  4369. suggestedWidth: _wpCustomizeHeader.data.width,
  4370. suggestedHeight: _wpCustomizeHeader.data.height
  4371. }),
  4372. new wp.media.controller.Cropper({
  4373. imgSelectOptions: this.calculateImageSelectOptions
  4374. })
  4375. ]
  4376. });
  4377. this.frame.on('select', this.onSelect, this);
  4378. this.frame.on('cropped', this.onCropped, this);
  4379. this.frame.on('skippedcrop', this.onSkippedCrop, this);
  4380. this.frame.open();
  4381. },
  4382. /**
  4383. * After an image is selected in the media modal,
  4384. * switch to the cropper state.
  4385. */
  4386. onSelect: function() {
  4387. this.frame.setState('cropper');
  4388. },
  4389. /**
  4390. * After the image has been cropped, apply the cropped image data to the setting.
  4391. *
  4392. * @param {object} croppedImage Cropped attachment data.
  4393. */
  4394. onCropped: function(croppedImage) {
  4395. var url = croppedImage.url,
  4396. attachmentId = croppedImage.attachment_id,
  4397. w = croppedImage.width,
  4398. h = croppedImage.height;
  4399. this.setImageFromURL(url, attachmentId, w, h);
  4400. },
  4401. /**
  4402. * If cropping was skipped, apply the image data directly to the setting.
  4403. *
  4404. * @param {object} selection
  4405. */
  4406. onSkippedCrop: function(selection) {
  4407. var url = selection.get('url'),
  4408. w = selection.get('width'),
  4409. h = selection.get('height');
  4410. this.setImageFromURL(url, selection.id, w, h);
  4411. },
  4412. /**
  4413. * Creates a new wp.customize.HeaderTool.ImageModel from provided
  4414. * header image data and inserts it into the user-uploaded headers
  4415. * collection.
  4416. *
  4417. * @param {String} url
  4418. * @param {Number} attachmentId
  4419. * @param {Number} width
  4420. * @param {Number} height
  4421. */
  4422. setImageFromURL: function(url, attachmentId, width, height) {
  4423. var choice, data = {};
  4424. data.url = url;
  4425. data.thumbnail_url = url;
  4426. data.timestamp = _.now();
  4427. if (attachmentId) {
  4428. data.attachment_id = attachmentId;
  4429. }
  4430. if (width) {
  4431. data.width = width;
  4432. }
  4433. if (height) {
  4434. data.height = height;
  4435. }
  4436. choice = new api.HeaderTool.ImageModel({
  4437. header: data,
  4438. choice: url.split('/').pop()
  4439. });
  4440. api.HeaderTool.UploadsList.add(choice);
  4441. api.HeaderTool.currentHeader.set(choice.toJSON());
  4442. choice.save();
  4443. choice.importImage();
  4444. },
  4445. /**
  4446. * Triggers the necessary events to deselect an image which was set as
  4447. * the currently selected one.
  4448. */
  4449. removeImage: function() {
  4450. api.HeaderTool.currentHeader.trigger('hide');
  4451. api.HeaderTool.CombinedList.trigger('control:removeImage');
  4452. }
  4453. });
  4454. /**
  4455. * wp.customize.ThemeControl
  4456. *
  4457. * @class wp.customize.ThemeControl
  4458. * @augments wp.customize.Control
  4459. */
  4460. api.ThemeControl = api.Control.extend(/** @lends wp.customize.ThemeControl.prototype */{
  4461. touchDrag: false,
  4462. screenshotRendered: false,
  4463. /**
  4464. * @since 4.2.0
  4465. */
  4466. ready: function() {
  4467. var control = this, panel = api.panel( 'themes' );
  4468. function disableSwitchButtons() {
  4469. return ! panel.canSwitchTheme( control.params.theme.id );
  4470. }
  4471. // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  4472. function disableInstallButtons() {
  4473. return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
  4474. }
  4475. function updateButtons() {
  4476. control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
  4477. control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
  4478. }
  4479. api.state( 'selectedChangesetStatus' ).bind( updateButtons );
  4480. api.state( 'changesetStatus' ).bind( updateButtons );
  4481. updateButtons();
  4482. control.container.on( 'touchmove', '.theme', function() {
  4483. control.touchDrag = true;
  4484. });
  4485. // Bind details view trigger.
  4486. control.container.on( 'click keydown touchend', '.theme', function( event ) {
  4487. var section;
  4488. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4489. return;
  4490. }
  4491. // Bail if the user scrolled on a touch device.
  4492. if ( control.touchDrag === true ) {
  4493. return control.touchDrag = false;
  4494. }
  4495. // Prevent the modal from showing when the user clicks the action button.
  4496. if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
  4497. return;
  4498. }
  4499. event.preventDefault(); // Keep this AFTER the key filter above
  4500. section = api.section( control.section() );
  4501. section.showDetails( control.params.theme, function() {
  4502. // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  4503. if ( api.settings.theme._filesystemCredentialsNeeded ) {
  4504. section.overlay.find( '.theme-actions .delete-theme' ).remove();
  4505. }
  4506. } );
  4507. });
  4508. control.container.on( 'render-screenshot', function() {
  4509. var $screenshot = $( this ).find( 'img' ),
  4510. source = $screenshot.data( 'src' );
  4511. if ( source ) {
  4512. $screenshot.attr( 'src', source );
  4513. }
  4514. control.screenshotRendered = true;
  4515. });
  4516. },
  4517. /**
  4518. * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
  4519. *
  4520. * @since 4.2.0
  4521. * @param {Array} terms - An array of terms to search for.
  4522. * @returns {boolean} Whether a theme control was activated or not.
  4523. */
  4524. filter: function( terms ) {
  4525. var control = this,
  4526. matchCount = 0,
  4527. haystack = control.params.theme.name + ' ' +
  4528. control.params.theme.description + ' ' +
  4529. control.params.theme.tags + ' ' +
  4530. control.params.theme.author + ' ';
  4531. haystack = haystack.toLowerCase().replace( '-', ' ' );
  4532. // Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
  4533. if ( ! _.isArray( terms ) ) {
  4534. terms = [ terms ];
  4535. }
  4536. // Always give exact name matches highest ranking.
  4537. if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
  4538. matchCount = 100;
  4539. } else {
  4540. // Search for and weight (by 10) complete term matches.
  4541. matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
  4542. // Search for each term individually (as whole-word and partial match) and sum weighted match counts.
  4543. _.each( terms, function( term ) {
  4544. matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
  4545. matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
  4546. });
  4547. // Upper limit on match ranking.
  4548. if ( matchCount > 99 ) {
  4549. matchCount = 99;
  4550. }
  4551. }
  4552. if ( 0 !== matchCount ) {
  4553. control.activate();
  4554. control.params.priority = 101 - matchCount; // Sort results by match count.
  4555. return true;
  4556. } else {
  4557. control.deactivate(); // Hide control
  4558. control.params.priority = 101;
  4559. return false;
  4560. }
  4561. },
  4562. /**
  4563. * Rerender the theme from its JS template with the installed type.
  4564. *
  4565. * @since 4.9.0
  4566. *
  4567. * @returns {void}
  4568. */
  4569. rerenderAsInstalled: function( installed ) {
  4570. var control = this, section;
  4571. if ( installed ) {
  4572. control.params.theme.type = 'installed';
  4573. } else {
  4574. section = api.section( control.params.section );
  4575. control.params.theme.type = section.params.action;
  4576. }
  4577. control.renderContent(); // Replaces existing content.
  4578. control.container.trigger( 'render-screenshot' );
  4579. }
  4580. });
  4581. /**
  4582. * Class wp.customize.CodeEditorControl
  4583. *
  4584. * @since 4.9.0
  4585. *
  4586. * @class wp.customize.CodeEditorControl
  4587. * @augments wp.customize.Control
  4588. */
  4589. api.CodeEditorControl = api.Control.extend(/** @lends wp.customize.CodeEditorControl.prototype */{
  4590. /**
  4591. * Initialize.
  4592. *
  4593. * @since 4.9.0
  4594. * @param {string} id - Unique identifier for the control instance.
  4595. * @param {object} options - Options hash for the control instance.
  4596. * @returns {void}
  4597. */
  4598. initialize: function( id, options ) {
  4599. var control = this;
  4600. control.deferred = _.extend( control.deferred || {}, {
  4601. codemirror: $.Deferred()
  4602. } );
  4603. api.Control.prototype.initialize.call( control, id, options );
  4604. // Note that rendering is debounced so the props will be used when rendering happens after add event.
  4605. control.notifications.bind( 'add', function( notification ) {
  4606. // Skip if control notification is not from setting csslint_error notification.
  4607. if ( notification.code !== control.setting.id + ':csslint_error' ) {
  4608. return;
  4609. }
  4610. // Customize the template and behavior of csslint_error notifications.
  4611. notification.templateId = 'customize-code-editor-lint-error-notification';
  4612. notification.render = (function( render ) {
  4613. return function() {
  4614. var li = render.call( this );
  4615. li.find( 'input[type=checkbox]' ).on( 'click', function() {
  4616. control.setting.notifications.remove( 'csslint_error' );
  4617. } );
  4618. return li;
  4619. };
  4620. })( notification.render );
  4621. } );
  4622. },
  4623. /**
  4624. * Initialize the editor when the containing section is ready and expanded.
  4625. *
  4626. * @since 4.9.0
  4627. * @returns {void}
  4628. */
  4629. ready: function() {
  4630. var control = this;
  4631. if ( ! control.section() ) {
  4632. control.initEditor();
  4633. return;
  4634. }
  4635. // Wait to initialize editor until section is embedded and expanded.
  4636. api.section( control.section(), function( section ) {
  4637. section.deferred.embedded.done( function() {
  4638. var onceExpanded;
  4639. if ( section.expanded() ) {
  4640. control.initEditor();
  4641. } else {
  4642. onceExpanded = function( isExpanded ) {
  4643. if ( isExpanded ) {
  4644. control.initEditor();
  4645. section.expanded.unbind( onceExpanded );
  4646. }
  4647. };
  4648. section.expanded.bind( onceExpanded );
  4649. }
  4650. } );
  4651. } );
  4652. },
  4653. /**
  4654. * Initialize editor.
  4655. *
  4656. * @since 4.9.0
  4657. * @returns {void}
  4658. */
  4659. initEditor: function() {
  4660. var control = this, element, editorSettings = false;
  4661. // Obtain editorSettings for instantiation.
  4662. if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) {
  4663. // Obtain default editor settings.
  4664. editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {};
  4665. editorSettings.codemirror = _.extend(
  4666. {},
  4667. editorSettings.codemirror,
  4668. {
  4669. indentUnit: 2,
  4670. tabSize: 2
  4671. }
  4672. );
  4673. // Merge editor_settings param on top of defaults.
  4674. if ( _.isObject( control.params.editor_settings ) ) {
  4675. _.each( control.params.editor_settings, function( value, key ) {
  4676. if ( _.isObject( value ) ) {
  4677. editorSettings[ key ] = _.extend(
  4678. {},
  4679. editorSettings[ key ],
  4680. value
  4681. );
  4682. }
  4683. } );
  4684. }
  4685. }
  4686. element = new api.Element( control.container.find( 'textarea' ) );
  4687. control.elements.push( element );
  4688. element.sync( control.setting );
  4689. element.set( control.setting() );
  4690. if ( editorSettings ) {
  4691. control.initSyntaxHighlightingEditor( editorSettings );
  4692. } else {
  4693. control.initPlainTextareaEditor();
  4694. }
  4695. },
  4696. /**
  4697. * Make sure editor gets focused when control is focused.
  4698. *
  4699. * @since 4.9.0
  4700. * @param {Object} [params] - Focus params.
  4701. * @param {Function} [params.completeCallback] - Function to call when expansion is complete.
  4702. * @returns {void}
  4703. */
  4704. focus: function( params ) {
  4705. var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
  4706. originalCompleteCallback = extendedParams.completeCallback;
  4707. extendedParams.completeCallback = function() {
  4708. if ( originalCompleteCallback ) {
  4709. originalCompleteCallback();
  4710. }
  4711. if ( control.editor ) {
  4712. control.editor.codemirror.focus();
  4713. }
  4714. };
  4715. api.Control.prototype.focus.call( control, extendedParams );
  4716. },
  4717. /**
  4718. * Initialize syntax-highlighting editor.
  4719. *
  4720. * @since 4.9.0
  4721. * @param {object} codeEditorSettings - Code editor settings.
  4722. * @returns {void}
  4723. */
  4724. initSyntaxHighlightingEditor: function( codeEditorSettings ) {
  4725. var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
  4726. settings = _.extend( {}, codeEditorSettings, {
  4727. onTabNext: _.bind( control.onTabNext, control ),
  4728. onTabPrevious: _.bind( control.onTabPrevious, control ),
  4729. onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
  4730. });
  4731. control.editor = wp.codeEditor.initialize( $textarea, settings );
  4732. // Improve the editor accessibility.
  4733. $( control.editor.codemirror.display.lineDiv )
  4734. .attr({
  4735. role: 'textbox',
  4736. 'aria-multiline': 'true',
  4737. 'aria-label': control.params.label,
  4738. 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
  4739. });
  4740. // Focus the editor when clicking on its label.
  4741. control.container.find( 'label' ).on( 'click', function() {
  4742. control.editor.codemirror.focus();
  4743. });
  4744. /*
  4745. * When the CodeMirror instance changes, mirror to the textarea,
  4746. * where we have our "true" change event handler bound.
  4747. */
  4748. control.editor.codemirror.on( 'change', function( codemirror ) {
  4749. suspendEditorUpdate = true;
  4750. $textarea.val( codemirror.getValue() ).trigger( 'change' );
  4751. suspendEditorUpdate = false;
  4752. });
  4753. // Update CodeMirror when the setting is changed by another plugin.
  4754. control.setting.bind( function( value ) {
  4755. if ( ! suspendEditorUpdate ) {
  4756. control.editor.codemirror.setValue( value );
  4757. }
  4758. });
  4759. // Prevent collapsing section when hitting Esc to tab out of editor.
  4760. control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
  4761. var escKeyCode = 27;
  4762. if ( escKeyCode === event.keyCode ) {
  4763. event.stopPropagation();
  4764. }
  4765. });
  4766. control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] );
  4767. },
  4768. /**
  4769. * Handle tabbing to the field after the editor.
  4770. *
  4771. * @since 4.9.0
  4772. * @returns {void}
  4773. */
  4774. onTabNext: function onTabNext() {
  4775. var control = this, controls, controlIndex, section;
  4776. section = api.section( control.section() );
  4777. controls = section.controls();
  4778. controlIndex = controls.indexOf( control );
  4779. if ( controls.length === controlIndex + 1 ) {
  4780. $( '#customize-footer-actions .collapse-sidebar' ).focus();
  4781. } else {
  4782. controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
  4783. }
  4784. },
  4785. /**
  4786. * Handle tabbing to the field before the editor.
  4787. *
  4788. * @since 4.9.0
  4789. * @returns {void}
  4790. */
  4791. onTabPrevious: function onTabPrevious() {
  4792. var control = this, controls, controlIndex, section;
  4793. section = api.section( control.section() );
  4794. controls = section.controls();
  4795. controlIndex = controls.indexOf( control );
  4796. if ( 0 === controlIndex ) {
  4797. section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
  4798. } else {
  4799. controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
  4800. }
  4801. },
  4802. /**
  4803. * Update error notice.
  4804. *
  4805. * @since 4.9.0
  4806. * @param {Array} errorAnnotations - Error annotations.
  4807. * @returns {void}
  4808. */
  4809. onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
  4810. var control = this, message;
  4811. control.setting.notifications.remove( 'csslint_error' );
  4812. if ( 0 !== errorAnnotations.length ) {
  4813. if ( 1 === errorAnnotations.length ) {
  4814. message = api.l10n.customCssError.singular.replace( '%d', '1' );
  4815. } else {
  4816. message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
  4817. }
  4818. control.setting.notifications.add( new api.Notification( 'csslint_error', {
  4819. message: message,
  4820. type: 'error'
  4821. } ) );
  4822. }
  4823. },
  4824. /**
  4825. * Initialize plain-textarea editor when syntax highlighting is disabled.
  4826. *
  4827. * @since 4.9.0
  4828. * @returns {void}
  4829. */
  4830. initPlainTextareaEditor: function() {
  4831. var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
  4832. $textarea.on( 'blur', function onBlur() {
  4833. $textarea.data( 'next-tab-blurs', false );
  4834. } );
  4835. $textarea.on( 'keydown', function onKeydown( event ) {
  4836. var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
  4837. if ( escKeyCode === event.keyCode ) {
  4838. if ( ! $textarea.data( 'next-tab-blurs' ) ) {
  4839. $textarea.data( 'next-tab-blurs', true );
  4840. event.stopPropagation(); // Prevent collapsing the section.
  4841. }
  4842. return;
  4843. }
  4844. // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
  4845. if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
  4846. return;
  4847. }
  4848. // Prevent capturing Tab characters if Esc was pressed.
  4849. if ( $textarea.data( 'next-tab-blurs' ) ) {
  4850. return;
  4851. }
  4852. selectionStart = textarea.selectionStart;
  4853. selectionEnd = textarea.selectionEnd;
  4854. value = textarea.value;
  4855. if ( selectionStart >= 0 ) {
  4856. textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
  4857. $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
  4858. }
  4859. event.stopPropagation();
  4860. event.preventDefault();
  4861. });
  4862. control.deferred.codemirror.rejectWith( control );
  4863. }
  4864. });
  4865. /**
  4866. * Class wp.customize.DateTimeControl.
  4867. *
  4868. * @since 4.9.0
  4869. * @class wp.customize.DateTimeControl
  4870. * @augments wp.customize.Control
  4871. */
  4872. api.DateTimeControl = api.Control.extend(/** @lends wp.customize.DateTimeControl.prototype */{
  4873. /**
  4874. * Initialize behaviors.
  4875. *
  4876. * @since 4.9.0
  4877. * @returns {void}
  4878. */
  4879. ready: function ready() {
  4880. var control = this;
  4881. control.inputElements = {};
  4882. control.invalidDate = false;
  4883. _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' );
  4884. if ( ! control.setting ) {
  4885. throw new Error( 'Missing setting' );
  4886. }
  4887. control.container.find( '.date-input' ).each( function() {
  4888. var input = $( this ), component, element;
  4889. component = input.data( 'component' );
  4890. element = new api.Element( input );
  4891. control.inputElements[ component ] = element;
  4892. control.elements.push( element );
  4893. // Add invalid date error once user changes (and has blurred the input).
  4894. input.on( 'change', function() {
  4895. if ( control.invalidDate ) {
  4896. control.notifications.add( new api.Notification( 'invalid_date', {
  4897. message: api.l10n.invalidDate
  4898. } ) );
  4899. }
  4900. } );
  4901. // Remove the error immediately after validity change.
  4902. input.on( 'input', _.debounce( function() {
  4903. if ( ! control.invalidDate ) {
  4904. control.notifications.remove( 'invalid_date' );
  4905. }
  4906. } ) );
  4907. // Add zero-padding when blurring field.
  4908. input.on( 'blur', _.debounce( function() {
  4909. if ( ! control.invalidDate ) {
  4910. control.populateDateInputs();
  4911. }
  4912. } ) );
  4913. } );
  4914. control.inputElements.month.bind( control.updateDaysForMonth );
  4915. control.inputElements.year.bind( control.updateDaysForMonth );
  4916. control.populateDateInputs();
  4917. control.setting.bind( control.populateDateInputs );
  4918. // Start populating setting after inputs have been populated.
  4919. _.each( control.inputElements, function( element ) {
  4920. element.bind( control.populateSetting );
  4921. } );
  4922. },
  4923. /**
  4924. * Parse datetime string.
  4925. *
  4926. * @since 4.9.0
  4927. *
  4928. * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format.
  4929. * @returns {object|null} Returns object containing date components or null if parse error.
  4930. */
  4931. parseDateTime: function parseDateTime( datetime ) {
  4932. var control = this, matches, date, midDayHour = 12;
  4933. if ( datetime ) {
  4934. matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ );
  4935. }
  4936. if ( ! matches ) {
  4937. return null;
  4938. }
  4939. matches.shift();
  4940. date = {
  4941. year: matches.shift(),
  4942. month: matches.shift(),
  4943. day: matches.shift(),
  4944. hour: matches.shift() || '00',
  4945. minute: matches.shift() || '00',
  4946. second: matches.shift() || '00'
  4947. };
  4948. if ( control.params.includeTime && control.params.twelveHourFormat ) {
  4949. date.hour = parseInt( date.hour, 10 );
  4950. date.meridian = date.hour >= midDayHour ? 'pm' : 'am';
  4951. date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
  4952. delete date.second; // @todo Why only if twelveHourFormat?
  4953. }
  4954. return date;
  4955. },
  4956. /**
  4957. * Validates if input components have valid date and time.
  4958. *
  4959. * @since 4.9.0
  4960. * @return {boolean} If date input fields has error.
  4961. */
  4962. validateInputs: function validateInputs() {
  4963. var control = this, components, validityInput;
  4964. control.invalidDate = false;
  4965. components = [ 'year', 'day' ];
  4966. if ( control.params.includeTime ) {
  4967. components.push( 'hour', 'minute' );
  4968. }
  4969. _.find( components, function( component ) {
  4970. var element, max, min, value;
  4971. element = control.inputElements[ component ];
  4972. validityInput = element.element.get( 0 );
  4973. max = parseInt( element.element.attr( 'max' ), 10 );
  4974. min = parseInt( element.element.attr( 'min' ), 10 );
  4975. value = parseInt( element(), 10 );
  4976. control.invalidDate = isNaN( value ) || value > max || value < min;
  4977. if ( ! control.invalidDate ) {
  4978. validityInput.setCustomValidity( '' );
  4979. }
  4980. return control.invalidDate;
  4981. } );
  4982. if ( control.inputElements.meridian && ! control.invalidDate ) {
  4983. validityInput = control.inputElements.meridian.element.get( 0 );
  4984. if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) {
  4985. control.invalidDate = true;
  4986. } else {
  4987. validityInput.setCustomValidity( '' );
  4988. }
  4989. }
  4990. if ( control.invalidDate ) {
  4991. validityInput.setCustomValidity( api.l10n.invalidValue );
  4992. } else {
  4993. validityInput.setCustomValidity( '' );
  4994. }
  4995. if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) {
  4996. _.result( validityInput, 'reportValidity' );
  4997. }
  4998. return control.invalidDate;
  4999. },
  5000. /**
  5001. * Updates number of days according to the month and year selected.
  5002. *
  5003. * @since 4.9.0
  5004. * @return {void}
  5005. */
  5006. updateDaysForMonth: function updateDaysForMonth() {
  5007. var control = this, daysInMonth, year, month, day;
  5008. month = parseInt( control.inputElements.month(), 10 );
  5009. year = parseInt( control.inputElements.year(), 10 );
  5010. day = parseInt( control.inputElements.day(), 10 );
  5011. if ( month && year ) {
  5012. daysInMonth = new Date( year, month, 0 ).getDate();
  5013. control.inputElements.day.element.attr( 'max', daysInMonth );
  5014. if ( day > daysInMonth ) {
  5015. control.inputElements.day( String( daysInMonth ) );
  5016. }
  5017. }
  5018. },
  5019. /**
  5020. * Populate setting value from the inputs.
  5021. *
  5022. * @since 4.9.0
  5023. * @returns {boolean} If setting updated.
  5024. */
  5025. populateSetting: function populateSetting() {
  5026. var control = this, date;
  5027. if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
  5028. return false;
  5029. }
  5030. date = control.convertInputDateToString();
  5031. control.setting.set( date );
  5032. return true;
  5033. },
  5034. /**
  5035. * Converts input values to string in Y-m-d H:i:s format.
  5036. *
  5037. * @since 4.9.0
  5038. * @return {string} Date string.
  5039. */
  5040. convertInputDateToString: function convertInputDateToString() {
  5041. var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
  5042. getElementValue, pad;
  5043. pad = function( number, padding ) {
  5044. var zeros;
  5045. if ( String( number ).length < padding ) {
  5046. zeros = padding - String( number ).length;
  5047. number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
  5048. }
  5049. return number;
  5050. };
  5051. getElementValue = function( component ) {
  5052. var value = parseInt( control.inputElements[ component ].get(), 10 );
  5053. if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
  5054. value = pad( value, 2 );
  5055. } else if ( 'year' === component ) {
  5056. value = pad( value, 4 );
  5057. }
  5058. return value;
  5059. };
  5060. dateFormat = [ 'year', '-', 'month', '-', 'day' ];
  5061. if ( control.params.includeTime ) {
  5062. hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour();
  5063. dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] );
  5064. }
  5065. _.each( dateFormat, function( component ) {
  5066. date += control.inputElements[ component ] ? getElementValue( component ) : component;
  5067. } );
  5068. return date;
  5069. },
  5070. /**
  5071. * Check if the date is in the future.
  5072. *
  5073. * @since 4.9.0
  5074. * @returns {boolean} True if future date.
  5075. */
  5076. isFutureDate: function isFutureDate() {
  5077. var control = this;
  5078. return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
  5079. },
  5080. /**
  5081. * Convert hour in twelve hour format to twenty four hour format.
  5082. *
  5083. * @since 4.9.0
  5084. * @param {string} hourInTwelveHourFormat - Hour in twelve hour format.
  5085. * @param {string} meridian - Either 'am' or 'pm'.
  5086. * @returns {string} Hour in twenty four hour format.
  5087. */
  5088. convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) {
  5089. var hourInTwentyFourHourFormat, hour, midDayHour = 12;
  5090. hour = parseInt( hourInTwelveHourFormat, 10 );
  5091. if ( isNaN( hour ) ) {
  5092. return '';
  5093. }
  5094. if ( 'pm' === meridian && hour < midDayHour ) {
  5095. hourInTwentyFourHourFormat = hour + midDayHour;
  5096. } else if ( 'am' === meridian && midDayHour === hour ) {
  5097. hourInTwentyFourHourFormat = hour - midDayHour;
  5098. } else {
  5099. hourInTwentyFourHourFormat = hour;
  5100. }
  5101. return String( hourInTwentyFourHourFormat );
  5102. },
  5103. /**
  5104. * Populates date inputs in date fields.
  5105. *
  5106. * @since 4.9.0
  5107. * @returns {boolean} Whether the inputs were populated.
  5108. */
  5109. populateDateInputs: function populateDateInputs() {
  5110. var control = this, parsed;
  5111. parsed = control.parseDateTime( control.setting.get() );
  5112. if ( ! parsed ) {
  5113. return false;
  5114. }
  5115. _.each( control.inputElements, function( element, component ) {
  5116. var value = parsed[ component ]; // This will be zero-padded string.
  5117. // Set month and meridian regardless of focused state since they are dropdowns.
  5118. if ( 'month' === component || 'meridian' === component ) {
  5119. // Options in dropdowns are not zero-padded.
  5120. value = value.replace( /^0/, '' );
  5121. element.set( value );
  5122. } else {
  5123. value = parseInt( value, 10 );
  5124. if ( ! element.element.is( document.activeElement ) ) {
  5125. // Populate element with zero-padded value if not focused.
  5126. element.set( parsed[ component ] );
  5127. } else if ( value !== parseInt( element(), 10 ) ) {
  5128. // Forcibly update the value if its underlying value changed, regardless of zero-padding.
  5129. element.set( String( value ) );
  5130. }
  5131. }
  5132. } );
  5133. return true;
  5134. },
  5135. /**
  5136. * Toggle future date notification for date control.
  5137. *
  5138. * @since 4.9.0
  5139. * @param {boolean} notify Add or remove the notification.
  5140. * @return {wp.customize.DateTimeControl}
  5141. */
  5142. toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
  5143. var control = this, notificationCode, notification;
  5144. notificationCode = 'not_future_date';
  5145. if ( notify ) {
  5146. notification = new api.Notification( notificationCode, {
  5147. type: 'error',
  5148. message: api.l10n.futureDateError
  5149. } );
  5150. control.notifications.add( notification );
  5151. } else {
  5152. control.notifications.remove( notificationCode );
  5153. }
  5154. return control;
  5155. }
  5156. });
  5157. /**
  5158. * Class PreviewLinkControl.
  5159. *
  5160. * @since 4.9.0
  5161. * @class wp.customize.PreviewLinkControl
  5162. * @augments wp.customize.Control
  5163. */
  5164. api.PreviewLinkControl = api.Control.extend(/** @lends wp.customize.PreviewLinkControl.prototype */{
  5165. defaults: _.extend( {}, api.Control.prototype.defaults, {
  5166. templateId: 'customize-preview-link-control'
  5167. } ),
  5168. /**
  5169. * Initialize behaviors.
  5170. *
  5171. * @since 4.9.0
  5172. * @returns {void}
  5173. */
  5174. ready: function ready() {
  5175. var control = this, element, component, node, url, input, button;
  5176. _.bindAll( control, 'updatePreviewLink' );
  5177. if ( ! control.setting ) {
  5178. control.setting = new api.Value();
  5179. }
  5180. control.previewElements = {};
  5181. control.container.find( '.preview-control-element' ).each( function() {
  5182. node = $( this );
  5183. component = node.data( 'component' );
  5184. element = new api.Element( node );
  5185. control.previewElements[ component ] = element;
  5186. control.elements.push( element );
  5187. } );
  5188. url = control.previewElements.url;
  5189. input = control.previewElements.input;
  5190. button = control.previewElements.button;
  5191. input.link( control.setting );
  5192. url.link( control.setting );
  5193. url.bind( function( value ) {
  5194. url.element.parent().attr( {
  5195. href: value,
  5196. target: api.settings.changeset.uuid
  5197. } );
  5198. } );
  5199. api.bind( 'ready', control.updatePreviewLink );
  5200. api.state( 'saved' ).bind( control.updatePreviewLink );
  5201. api.state( 'changesetStatus' ).bind( control.updatePreviewLink );
  5202. api.state( 'activated' ).bind( control.updatePreviewLink );
  5203. api.previewer.previewUrl.bind( control.updatePreviewLink );
  5204. button.element.on( 'click', function( event ) {
  5205. event.preventDefault();
  5206. if ( control.setting() ) {
  5207. input.element.select();
  5208. document.execCommand( 'copy' );
  5209. button( button.element.data( 'copied-text' ) );
  5210. }
  5211. } );
  5212. url.element.parent().on( 'click', function( event ) {
  5213. if ( $( this ).hasClass( 'disabled' ) ) {
  5214. event.preventDefault();
  5215. }
  5216. } );
  5217. button.element.on( 'mouseenter', function() {
  5218. if ( control.setting() ) {
  5219. button( button.element.data( 'copy-text' ) );
  5220. }
  5221. } );
  5222. },
  5223. /**
  5224. * Updates Preview Link
  5225. *
  5226. * @since 4.9.0
  5227. * @return {void}
  5228. */
  5229. updatePreviewLink: function updatePreviewLink() {
  5230. var control = this, unsavedDirtyValues;
  5231. unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get();
  5232. control.toggleSaveNotification( unsavedDirtyValues );
  5233. control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues );
  5234. control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
  5235. control.setting.set( api.previewer.getFrontendPreviewUrl() );
  5236. },
  5237. /**
  5238. * Toggles save notification.
  5239. *
  5240. * @since 4.9.0
  5241. * @param {boolean} notify Add or remove notification.
  5242. * @return {void}
  5243. */
  5244. toggleSaveNotification: function toggleSaveNotification( notify ) {
  5245. var control = this, notificationCode, notification;
  5246. notificationCode = 'changes_not_saved';
  5247. if ( notify ) {
  5248. notification = new api.Notification( notificationCode, {
  5249. type: 'info',
  5250. message: api.l10n.saveBeforeShare
  5251. } );
  5252. control.notifications.add( notification );
  5253. } else {
  5254. control.notifications.remove( notificationCode );
  5255. }
  5256. }
  5257. });
  5258. /**
  5259. * Change objects contained within the main customize object to Settings.
  5260. *
  5261. * @alias wp.customize.defaultConstructor
  5262. */
  5263. api.defaultConstructor = api.Setting;
  5264. /**
  5265. * Callback for resolved controls.
  5266. *
  5267. * @callback wp.customize.deferredControlsCallback
  5268. * @param {wp.customize.Control[]} controls Resolved controls.
  5269. */
  5270. /**
  5271. * Collection of all registered controls.
  5272. *
  5273. * @alias wp.customize.control
  5274. *
  5275. * @since 3.4.0
  5276. *
  5277. * @type {Function}
  5278. * @param {...string} ids - One or more ids for controls to obtain.
  5279. * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist.
  5280. * @returns {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param), or promise resolving to requested controls.
  5281. *
  5282. * @example <caption>Loop over all registered controls.</caption>
  5283. * wp.customize.control.each( function( control ) { ... } );
  5284. *
  5285. * @example <caption>Getting `background_color` control instance.</caption>
  5286. * control = wp.customize.control( 'background_color' );
  5287. *
  5288. * @example <caption>Check if control exists.</caption>
  5289. * hasControl = wp.customize.control.has( 'background_color' );
  5290. *
  5291. * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption>
  5292. * wp.customize.control( 'background_color', function( control ) { ... } );
  5293. *
  5294. * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption>
  5295. * promise = wp.customize.control( 'blogname', 'blogdescription' );
  5296. * promise.done( function( titleControl, taglineControl ) { ... } );
  5297. *
  5298. * @example <caption>Get title and tagline controls when they both exist, using callback.</caption>
  5299. * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } );
  5300. *
  5301. * @example <caption>Getting setting value for `background_color` control.</caption>
  5302. * value = wp.customize.control( 'background_color ').setting.get();
  5303. * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same.
  5304. *
  5305. * @example <caption>Add new control for site title.</caption>
  5306. * wp.customize.control.add( new wp.customize.Control( 'other_blogname', {
  5307. * setting: 'blogname',
  5308. * type: 'text',
  5309. * label: 'Site title',
  5310. * section: 'other_site_identify'
  5311. * } ) );
  5312. *
  5313. * @example <caption>Remove control.</caption>
  5314. * wp.customize.control.remove( 'other_blogname' );
  5315. *
  5316. * @example <caption>Listen for control being added.</caption>
  5317. * wp.customize.control.bind( 'add', function( addedControl ) { ... } )
  5318. *
  5319. * @example <caption>Listen for control being removed.</caption>
  5320. * wp.customize.control.bind( 'removed', function( removedControl ) { ... } )
  5321. */
  5322. api.control = new api.Values({ defaultConstructor: api.Control });
  5323. /**
  5324. * Callback for resolved sections.
  5325. *
  5326. * @callback wp.customize.deferredSectionsCallback
  5327. * @param {wp.customize.Section[]} sections Resolved sections.
  5328. */
  5329. /**
  5330. * Collection of all registered sections.
  5331. *
  5332. * @alias wp.customize.section
  5333. *
  5334. * @since 3.4.0
  5335. *
  5336. * @type {Function}
  5337. * @param {...string} ids - One or more ids for sections to obtain.
  5338. * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist.
  5339. * @returns {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param), or promise resolving to requested sections.
  5340. *
  5341. * @example <caption>Loop over all registered sections.</caption>
  5342. * wp.customize.section.each( function( section ) { ... } )
  5343. *
  5344. * @example <caption>Getting `title_tagline` section instance.</caption>
  5345. * section = wp.customize.section( 'title_tagline' )
  5346. *
  5347. * @example <caption>Expand dynamically-created section when it exists.</caption>
  5348. * wp.customize.section( 'dynamically_created', function( section ) {
  5349. * section.expand();
  5350. * } );
  5351. *
  5352. * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  5353. */
  5354. api.section = new api.Values({ defaultConstructor: api.Section });
  5355. /**
  5356. * Callback for resolved panels.
  5357. *
  5358. * @callback wp.customize.deferredPanelsCallback
  5359. * @param {wp.customize.Panel[]} panels Resolved panels.
  5360. */
  5361. /**
  5362. * Collection of all registered panels.
  5363. *
  5364. * @alias wp.customize.panel
  5365. *
  5366. * @since 4.0.0
  5367. *
  5368. * @type {Function}
  5369. * @param {...string} ids - One or more ids for panels to obtain.
  5370. * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist.
  5371. * @returns {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param), or promise resolving to requested panels.
  5372. *
  5373. * @example <caption>Loop over all registered panels.</caption>
  5374. * wp.customize.panel.each( function( panel ) { ... } )
  5375. *
  5376. * @example <caption>Getting nav_menus panel instance.</caption>
  5377. * panel = wp.customize.panel( 'nav_menus' );
  5378. *
  5379. * @example <caption>Expand dynamically-created panel when it exists.</caption>
  5380. * wp.customize.panel( 'dynamically_created', function( panel ) {
  5381. * panel.expand();
  5382. * } );
  5383. *
  5384. * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  5385. */
  5386. api.panel = new api.Values({ defaultConstructor: api.Panel });
  5387. /**
  5388. * Callback for resolved notifications.
  5389. *
  5390. * @callback wp.customize.deferredNotificationsCallback
  5391. * @param {wp.customize.Notification[]} notifications Resolved notifications.
  5392. */
  5393. /**
  5394. * Collection of all global notifications.
  5395. *
  5396. * @alias wp.customize.notifications
  5397. *
  5398. * @since 4.9.0
  5399. *
  5400. * @type {Function}
  5401. * @param {...string} codes - One or more codes for notifications to obtain.
  5402. * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist.
  5403. * @returns {wp.customize.Notification|undefined|jQuery.promise} notification instance or undefined (if function called with one code param), or promise resolving to requested notifications.
  5404. *
  5405. * @example <caption>Check if existing notification</caption>
  5406. * exists = wp.customize.notifications.has( 'a_new_day_arrived' );
  5407. *
  5408. * @example <caption>Obtain existing notification</caption>
  5409. * notification = wp.customize.notifications( 'a_new_day_arrived' );
  5410. *
  5411. * @example <caption>Obtain notification that may not exist yet.</caption>
  5412. * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } );
  5413. *
  5414. * @example <caption>Add a warning notification.</caption>
  5415. * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', {
  5416. * type: 'warning',
  5417. * message: 'Midnight has almost arrived!',
  5418. * dismissible: true
  5419. * } ) );
  5420. *
  5421. * @example <caption>Remove a notification.</caption>
  5422. * wp.customize.notifications.remove( 'a_new_day_arrived' );
  5423. *
  5424. * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  5425. */
  5426. api.notifications = new api.Notifications();
  5427. api.PreviewFrame = api.Messenger.extend(/** @lends wp.customize.PreviewFrame.prototype */{
  5428. sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
  5429. /**
  5430. * An object that fetches a preview in the background of the document, which
  5431. * allows for seamless replacement of an existing preview.
  5432. *
  5433. * @constructs wp.customize.PreviewFrame
  5434. * @augments wp.customize.Messenger
  5435. *
  5436. * @param {object} params.container
  5437. * @param {object} params.previewUrl
  5438. * @param {object} params.query
  5439. * @param {object} options
  5440. */
  5441. initialize: function( params, options ) {
  5442. var deferred = $.Deferred();
  5443. /*
  5444. * Make the instance of the PreviewFrame the promise object
  5445. * so other objects can easily interact with it.
  5446. */
  5447. deferred.promise( this );
  5448. this.container = params.container;
  5449. $.extend( params, { channel: api.PreviewFrame.uuid() });
  5450. api.Messenger.prototype.initialize.call( this, params, options );
  5451. this.add( 'previewUrl', params.previewUrl );
  5452. this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
  5453. this.run( deferred );
  5454. },
  5455. /**
  5456. * Run the preview request.
  5457. *
  5458. * @param {object} deferred jQuery Deferred object to be resolved with
  5459. * the request.
  5460. */
  5461. run: function( deferred ) {
  5462. var previewFrame = this,
  5463. loaded = false,
  5464. ready = false,
  5465. readyData = null,
  5466. hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
  5467. urlParser,
  5468. params,
  5469. form;
  5470. if ( previewFrame._ready ) {
  5471. previewFrame.unbind( 'ready', previewFrame._ready );
  5472. }
  5473. previewFrame._ready = function( data ) {
  5474. ready = true;
  5475. readyData = data;
  5476. previewFrame.container.addClass( 'iframe-ready' );
  5477. if ( ! data ) {
  5478. return;
  5479. }
  5480. if ( loaded ) {
  5481. deferred.resolveWith( previewFrame, [ data ] );
  5482. }
  5483. };
  5484. previewFrame.bind( 'ready', previewFrame._ready );
  5485. urlParser = document.createElement( 'a' );
  5486. urlParser.href = previewFrame.previewUrl();
  5487. params = _.extend(
  5488. api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  5489. {
  5490. customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
  5491. customize_theme: previewFrame.query.customize_theme,
  5492. customize_messenger_channel: previewFrame.query.customize_messenger_channel
  5493. }
  5494. );
  5495. if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
  5496. params.customize_autosaved = 'on';
  5497. }
  5498. urlParser.search = $.param( params );
  5499. previewFrame.iframe = $( '<iframe />', {
  5500. title: api.l10n.previewIframeTitle,
  5501. name: 'customize-' + previewFrame.channel()
  5502. } );
  5503. previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
  5504. previewFrame.iframe.attr( 'sandbox', 'allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts' );
  5505. if ( ! hasPendingChangesetUpdate ) {
  5506. previewFrame.iframe.attr( 'src', urlParser.href );
  5507. } else {
  5508. previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
  5509. }
  5510. previewFrame.iframe.appendTo( previewFrame.container );
  5511. previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
  5512. /*
  5513. * Submit customized data in POST request to preview frame window since
  5514. * there are setting value changes not yet written to changeset.
  5515. */
  5516. if ( hasPendingChangesetUpdate ) {
  5517. form = $( '<form>', {
  5518. action: urlParser.href,
  5519. target: previewFrame.iframe.attr( 'name' ),
  5520. method: 'post',
  5521. hidden: 'hidden'
  5522. } );
  5523. form.append( $( '<input>', {
  5524. type: 'hidden',
  5525. name: '_method',
  5526. value: 'GET'
  5527. } ) );
  5528. _.each( previewFrame.query, function( value, key ) {
  5529. form.append( $( '<input>', {
  5530. type: 'hidden',
  5531. name: key,
  5532. value: value
  5533. } ) );
  5534. } );
  5535. previewFrame.container.append( form );
  5536. form.submit();
  5537. form.remove(); // No need to keep the form around after submitted.
  5538. }
  5539. previewFrame.bind( 'iframe-loading-error', function( error ) {
  5540. previewFrame.iframe.remove();
  5541. // Check if the user is not logged in.
  5542. if ( 0 === error ) {
  5543. previewFrame.login( deferred );
  5544. return;
  5545. }
  5546. // Check for cheaters.
  5547. if ( -1 === error ) {
  5548. deferred.rejectWith( previewFrame, [ 'cheatin' ] );
  5549. return;
  5550. }
  5551. deferred.rejectWith( previewFrame, [ 'request failure' ] );
  5552. } );
  5553. previewFrame.iframe.one( 'load', function() {
  5554. loaded = true;
  5555. if ( ready ) {
  5556. deferred.resolveWith( previewFrame, [ readyData ] );
  5557. } else {
  5558. setTimeout( function() {
  5559. deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
  5560. }, previewFrame.sensitivity );
  5561. }
  5562. });
  5563. },
  5564. login: function( deferred ) {
  5565. var self = this,
  5566. reject;
  5567. reject = function() {
  5568. deferred.rejectWith( self, [ 'logged out' ] );
  5569. };
  5570. if ( this.triedLogin ) {
  5571. return reject();
  5572. }
  5573. // Check if we have an admin cookie.
  5574. $.get( api.settings.url.ajax, {
  5575. action: 'logged-in'
  5576. }).fail( reject ).done( function( response ) {
  5577. var iframe;
  5578. if ( '1' !== response ) {
  5579. reject();
  5580. }
  5581. iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
  5582. iframe.appendTo( self.container );
  5583. iframe.on( 'load', function() {
  5584. self.triedLogin = true;
  5585. iframe.remove();
  5586. self.run( deferred );
  5587. });
  5588. });
  5589. },
  5590. destroy: function() {
  5591. api.Messenger.prototype.destroy.call( this );
  5592. if ( this.iframe ) {
  5593. this.iframe.remove();
  5594. }
  5595. delete this.iframe;
  5596. delete this.targetWindow;
  5597. }
  5598. });
  5599. (function(){
  5600. var id = 0;
  5601. /**
  5602. * Return an incremented ID for a preview messenger channel.
  5603. *
  5604. * This function is named "uuid" for historical reasons, but it is a
  5605. * misnomer as it is not an actual UUID, and it is not universally unique.
  5606. * This is not to be confused with `api.settings.changeset.uuid`.
  5607. *
  5608. * @return {string}
  5609. */
  5610. api.PreviewFrame.uuid = function() {
  5611. return 'preview-' + String( id++ );
  5612. };
  5613. }());
  5614. /**
  5615. * Set the document title of the customizer.
  5616. *
  5617. * @alias wp.customize.setDocumentTitle
  5618. *
  5619. * @since 4.1.0
  5620. *
  5621. * @param {string} documentTitle
  5622. */
  5623. api.setDocumentTitle = function ( documentTitle ) {
  5624. var tmpl, title;
  5625. tmpl = api.settings.documentTitleTmpl;
  5626. title = tmpl.replace( '%s', documentTitle );
  5627. document.title = title;
  5628. api.trigger( 'title', title );
  5629. };
  5630. api.Previewer = api.Messenger.extend(/** @lends wp.customize.Previewer.prototype */{
  5631. refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
  5632. /**
  5633. * @constructs wp.customize.Previewer
  5634. * @augments wp.customize.Messenger
  5635. *
  5636. * @param {array} params.allowedUrls
  5637. * @param {string} params.container A selector or jQuery element for the preview
  5638. * frame to be placed.
  5639. * @param {string} params.form
  5640. * @param {string} params.previewUrl The URL to preview.
  5641. * @param {object} options
  5642. */
  5643. initialize: function( params, options ) {
  5644. var previewer = this,
  5645. urlParser = document.createElement( 'a' );
  5646. $.extend( previewer, options || {} );
  5647. previewer.deferred = {
  5648. active: $.Deferred()
  5649. };
  5650. // Debounce to prevent hammering server and then wait for any pending update requests.
  5651. previewer.refresh = _.debounce(
  5652. ( function( originalRefresh ) {
  5653. return function() {
  5654. var isProcessingComplete, refreshOnceProcessingComplete;
  5655. isProcessingComplete = function() {
  5656. return 0 === api.state( 'processing' ).get();
  5657. };
  5658. if ( isProcessingComplete() ) {
  5659. originalRefresh.call( previewer );
  5660. } else {
  5661. refreshOnceProcessingComplete = function() {
  5662. if ( isProcessingComplete() ) {
  5663. originalRefresh.call( previewer );
  5664. api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
  5665. }
  5666. };
  5667. api.state( 'processing' ).bind( refreshOnceProcessingComplete );
  5668. }
  5669. };
  5670. }( previewer.refresh ) ),
  5671. previewer.refreshBuffer
  5672. );
  5673. previewer.container = api.ensure( params.container );
  5674. previewer.allowedUrls = params.allowedUrls;
  5675. params.url = window.location.href;
  5676. api.Messenger.prototype.initialize.call( previewer, params );
  5677. urlParser.href = previewer.origin();
  5678. previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
  5679. // Limit the URL to internal, front-end links.
  5680. //
  5681. // If the front end and the admin are served from the same domain, load the
  5682. // preview over ssl if the Customizer is being loaded over ssl. This avoids
  5683. // insecure content warnings. This is not attempted if the admin and front end
  5684. // are on different domains to avoid the case where the front end doesn't have
  5685. // ssl certs.
  5686. previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
  5687. var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
  5688. urlParser = document.createElement( 'a' );
  5689. urlParser.href = to;
  5690. // Abort if URL is for admin or (static) files in wp-includes or wp-content.
  5691. if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
  5692. return null;
  5693. }
  5694. // Remove state query params.
  5695. if ( urlParser.search.length > 1 ) {
  5696. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  5697. delete queryParams.customize_changeset_uuid;
  5698. delete queryParams.customize_theme;
  5699. delete queryParams.customize_messenger_channel;
  5700. delete queryParams.customize_autosaved;
  5701. if ( _.isEmpty( queryParams ) ) {
  5702. urlParser.search = '';
  5703. } else {
  5704. urlParser.search = $.param( queryParams );
  5705. }
  5706. }
  5707. parsedCandidateUrls.push( urlParser );
  5708. // Prepend list with URL that matches the scheme/protocol of the iframe.
  5709. if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
  5710. urlParser = document.createElement( 'a' );
  5711. urlParser.href = parsedCandidateUrls[0].href;
  5712. urlParser.protocol = previewer.scheme.get() + ':';
  5713. parsedCandidateUrls.unshift( urlParser );
  5714. }
  5715. // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
  5716. parsedAllowedUrl = document.createElement( 'a' );
  5717. _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
  5718. return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
  5719. parsedAllowedUrl.href = allowedUrl;
  5720. if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
  5721. result = parsedCandidateUrl.href;
  5722. return true;
  5723. }
  5724. } ) );
  5725. } );
  5726. return result;
  5727. });
  5728. previewer.bind( 'ready', previewer.ready );
  5729. // Start listening for keep-alive messages when iframe first loads.
  5730. previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
  5731. previewer.bind( 'synced', function() {
  5732. previewer.send( 'active' );
  5733. } );
  5734. // Refresh the preview when the URL is changed (but not yet).
  5735. previewer.previewUrl.bind( previewer.refresh );
  5736. previewer.scroll = 0;
  5737. previewer.bind( 'scroll', function( distance ) {
  5738. previewer.scroll = distance;
  5739. });
  5740. // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
  5741. previewer.bind( 'url', function( url ) {
  5742. var onUrlChange, urlChanged = false;
  5743. previewer.scroll = 0;
  5744. onUrlChange = function() {
  5745. urlChanged = true;
  5746. };
  5747. previewer.previewUrl.bind( onUrlChange );
  5748. previewer.previewUrl.set( url );
  5749. previewer.previewUrl.unbind( onUrlChange );
  5750. if ( ! urlChanged ) {
  5751. previewer.refresh();
  5752. }
  5753. } );
  5754. // Update the document title when the preview changes.
  5755. previewer.bind( 'documentTitle', function ( title ) {
  5756. api.setDocumentTitle( title );
  5757. } );
  5758. },
  5759. /**
  5760. * Handle the preview receiving the ready message.
  5761. *
  5762. * @since 4.7.0
  5763. * @access public
  5764. *
  5765. * @param {object} data - Data from preview.
  5766. * @param {string} data.currentUrl - Current URL.
  5767. * @param {object} data.activePanels - Active panels.
  5768. * @param {object} data.activeSections Active sections.
  5769. * @param {object} data.activeControls Active controls.
  5770. * @returns {void}
  5771. */
  5772. ready: function( data ) {
  5773. var previewer = this, synced = {}, constructs;
  5774. synced.settings = api.get();
  5775. synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
  5776. if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
  5777. synced.scroll = previewer.scroll;
  5778. }
  5779. synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
  5780. previewer.send( 'sync', synced );
  5781. // Set the previewUrl without causing the url to set the iframe.
  5782. if ( data.currentUrl ) {
  5783. previewer.previewUrl.unbind( previewer.refresh );
  5784. previewer.previewUrl.set( data.currentUrl );
  5785. previewer.previewUrl.bind( previewer.refresh );
  5786. }
  5787. /*
  5788. * Walk over all panels, sections, and controls and set their
  5789. * respective active states to true if the preview explicitly
  5790. * indicates as such.
  5791. */
  5792. constructs = {
  5793. panel: data.activePanels,
  5794. section: data.activeSections,
  5795. control: data.activeControls
  5796. };
  5797. _( constructs ).each( function ( activeConstructs, type ) {
  5798. api[ type ].each( function ( construct, id ) {
  5799. var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
  5800. /*
  5801. * If the construct was created statically in PHP (not dynamically in JS)
  5802. * then consider a missing (undefined) value in the activeConstructs to
  5803. * mean it should be deactivated (since it is gone). But if it is
  5804. * dynamically created then only toggle activation if the value is defined,
  5805. * as this means that the construct was also then correspondingly
  5806. * created statically in PHP and the active callback is available.
  5807. * Otherwise, dynamically-created constructs should normally have
  5808. * their active states toggled in JS rather than from PHP.
  5809. */
  5810. if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
  5811. if ( activeConstructs[ id ] ) {
  5812. construct.activate();
  5813. } else {
  5814. construct.deactivate();
  5815. }
  5816. }
  5817. } );
  5818. } );
  5819. if ( data.settingValidities ) {
  5820. api._handleSettingValidities( {
  5821. settingValidities: data.settingValidities,
  5822. focusInvalidControl: false
  5823. } );
  5824. }
  5825. },
  5826. /**
  5827. * Keep the preview alive by listening for ready and keep-alive messages.
  5828. *
  5829. * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
  5830. *
  5831. * @since 4.7.0
  5832. * @access public
  5833. *
  5834. * @returns {void}
  5835. */
  5836. keepPreviewAlive: function keepPreviewAlive() {
  5837. var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
  5838. /**
  5839. * Schedule a preview keep-alive check.
  5840. *
  5841. * Note that if a page load takes longer than keepAliveCheck milliseconds,
  5842. * the keep-alive messages will still be getting sent from the previous
  5843. * URL.
  5844. */
  5845. scheduleKeepAliveCheck = function() {
  5846. timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
  5847. };
  5848. /**
  5849. * Set the previewerAlive state to true when receiving a message from the preview.
  5850. */
  5851. keepAliveTick = function() {
  5852. api.state( 'previewerAlive' ).set( true );
  5853. clearTimeout( timeoutId );
  5854. scheduleKeepAliveCheck();
  5855. };
  5856. /**
  5857. * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
  5858. *
  5859. * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
  5860. * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
  5861. * transport to use refresh instead, causing the preview frame also to be replaced with the current
  5862. * allowed preview URL.
  5863. */
  5864. handleMissingKeepAlive = function() {
  5865. api.state( 'previewerAlive' ).set( false );
  5866. };
  5867. scheduleKeepAliveCheck();
  5868. previewer.bind( 'ready', keepAliveTick );
  5869. previewer.bind( 'keep-alive', keepAliveTick );
  5870. },
  5871. /**
  5872. * Query string data sent with each preview request.
  5873. *
  5874. * @abstract
  5875. */
  5876. query: function() {},
  5877. abort: function() {
  5878. if ( this.loading ) {
  5879. this.loading.destroy();
  5880. delete this.loading;
  5881. }
  5882. },
  5883. /**
  5884. * Refresh the preview seamlessly.
  5885. *
  5886. * @since 3.4.0
  5887. * @access public
  5888. * @returns {void}
  5889. */
  5890. refresh: function() {
  5891. var previewer = this, onSettingChange;
  5892. // Display loading indicator
  5893. previewer.send( 'loading-initiated' );
  5894. previewer.abort();
  5895. previewer.loading = new api.PreviewFrame({
  5896. url: previewer.url(),
  5897. previewUrl: previewer.previewUrl(),
  5898. query: previewer.query( { excludeCustomizedSaved: true } ) || {},
  5899. container: previewer.container
  5900. });
  5901. previewer.settingsModifiedWhileLoading = {};
  5902. onSettingChange = function( setting ) {
  5903. previewer.settingsModifiedWhileLoading[ setting.id ] = true;
  5904. };
  5905. api.bind( 'change', onSettingChange );
  5906. previewer.loading.always( function() {
  5907. api.unbind( 'change', onSettingChange );
  5908. } );
  5909. previewer.loading.done( function( readyData ) {
  5910. var loadingFrame = this, onceSynced;
  5911. previewer.preview = loadingFrame;
  5912. previewer.targetWindow( loadingFrame.targetWindow() );
  5913. previewer.channel( loadingFrame.channel() );
  5914. onceSynced = function() {
  5915. loadingFrame.unbind( 'synced', onceSynced );
  5916. if ( previewer._previousPreview ) {
  5917. previewer._previousPreview.destroy();
  5918. }
  5919. previewer._previousPreview = previewer.preview;
  5920. previewer.deferred.active.resolve();
  5921. delete previewer.loading;
  5922. };
  5923. loadingFrame.bind( 'synced', onceSynced );
  5924. // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
  5925. previewer.trigger( 'ready', readyData );
  5926. });
  5927. previewer.loading.fail( function( reason ) {
  5928. previewer.send( 'loading-failed' );
  5929. if ( 'logged out' === reason ) {
  5930. if ( previewer.preview ) {
  5931. previewer.preview.destroy();
  5932. delete previewer.preview;
  5933. }
  5934. previewer.login().done( previewer.refresh );
  5935. }
  5936. if ( 'cheatin' === reason ) {
  5937. previewer.cheatin();
  5938. }
  5939. });
  5940. },
  5941. login: function() {
  5942. var previewer = this,
  5943. deferred, messenger, iframe;
  5944. if ( this._login ) {
  5945. return this._login;
  5946. }
  5947. deferred = $.Deferred();
  5948. this._login = deferred.promise();
  5949. messenger = new api.Messenger({
  5950. channel: 'login',
  5951. url: api.settings.url.login
  5952. });
  5953. iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
  5954. messenger.targetWindow( iframe[0].contentWindow );
  5955. messenger.bind( 'login', function () {
  5956. var refreshNonces = previewer.refreshNonces();
  5957. refreshNonces.always( function() {
  5958. iframe.remove();
  5959. messenger.destroy();
  5960. delete previewer._login;
  5961. });
  5962. refreshNonces.done( function() {
  5963. deferred.resolve();
  5964. });
  5965. refreshNonces.fail( function() {
  5966. previewer.cheatin();
  5967. deferred.reject();
  5968. });
  5969. });
  5970. return this._login;
  5971. },
  5972. cheatin: function() {
  5973. $( document.body ).empty().addClass( 'cheatin' ).append(
  5974. '<h1>' + api.l10n.notAllowedHeading + '</h1>' +
  5975. '<p>' + api.l10n.notAllowed + '</p>'
  5976. );
  5977. },
  5978. refreshNonces: function() {
  5979. var request, deferred = $.Deferred();
  5980. deferred.promise();
  5981. request = wp.ajax.post( 'customize_refresh_nonces', {
  5982. wp_customize: 'on',
  5983. customize_theme: api.settings.theme.stylesheet
  5984. });
  5985. request.done( function( response ) {
  5986. api.trigger( 'nonce-refresh', response );
  5987. deferred.resolve();
  5988. });
  5989. request.fail( function() {
  5990. deferred.reject();
  5991. });
  5992. return deferred;
  5993. }
  5994. });
  5995. api.settingConstructor = {};
  5996. api.controlConstructor = {
  5997. color: api.ColorControl,
  5998. media: api.MediaControl,
  5999. upload: api.UploadControl,
  6000. image: api.ImageControl,
  6001. cropped_image: api.CroppedImageControl,
  6002. site_icon: api.SiteIconControl,
  6003. header: api.HeaderControl,
  6004. background: api.BackgroundControl,
  6005. background_position: api.BackgroundPositionControl,
  6006. theme: api.ThemeControl,
  6007. date_time: api.DateTimeControl,
  6008. code_editor: api.CodeEditorControl
  6009. };
  6010. api.panelConstructor = {
  6011. themes: api.ThemesPanel
  6012. };
  6013. api.sectionConstructor = {
  6014. themes: api.ThemesSection,
  6015. outer: api.OuterSection
  6016. };
  6017. /**
  6018. * Handle setting_validities in an error response for the customize-save request.
  6019. *
  6020. * Add notifications to the settings and focus on the first control that has an invalid setting.
  6021. *
  6022. * @alias wp.customize._handleSettingValidities
  6023. *
  6024. * @since 4.6.0
  6025. * @private
  6026. *
  6027. * @param {object} args
  6028. * @param {object} args.settingValidities
  6029. * @param {boolean} [args.focusInvalidControl=false]
  6030. * @returns {void}
  6031. */
  6032. api._handleSettingValidities = function handleSettingValidities( args ) {
  6033. var invalidSettingControls, invalidSettings = [], wasFocused = false;
  6034. // Find the controls that correspond to each invalid setting.
  6035. _.each( args.settingValidities, function( validity, settingId ) {
  6036. var setting = api( settingId );
  6037. if ( setting ) {
  6038. // Add notifications for invalidities.
  6039. if ( _.isObject( validity ) ) {
  6040. _.each( validity, function( params, code ) {
  6041. var notification, existingNotification, needsReplacement = false;
  6042. notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
  6043. // Remove existing notification if already exists for code but differs in parameters.
  6044. existingNotification = setting.notifications( notification.code );
  6045. if ( existingNotification ) {
  6046. needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
  6047. }
  6048. if ( needsReplacement ) {
  6049. setting.notifications.remove( code );
  6050. }
  6051. if ( ! setting.notifications.has( notification.code ) ) {
  6052. setting.notifications.add( notification );
  6053. }
  6054. invalidSettings.push( setting.id );
  6055. } );
  6056. }
  6057. // Remove notification errors that are no longer valid.
  6058. setting.notifications.each( function( notification ) {
  6059. if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
  6060. setting.notifications.remove( notification.code );
  6061. }
  6062. } );
  6063. }
  6064. } );
  6065. if ( args.focusInvalidControl ) {
  6066. invalidSettingControls = api.findControlsForSettings( invalidSettings );
  6067. // Focus on the first control that is inside of an expanded section (one that is visible).
  6068. _( _.values( invalidSettingControls ) ).find( function( controls ) {
  6069. return _( controls ).find( function( control ) {
  6070. var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
  6071. if ( isExpanded && control.expanded ) {
  6072. isExpanded = control.expanded();
  6073. }
  6074. if ( isExpanded ) {
  6075. control.focus();
  6076. wasFocused = true;
  6077. }
  6078. return wasFocused;
  6079. } );
  6080. } );
  6081. // Focus on the first invalid control.
  6082. if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
  6083. _.values( invalidSettingControls )[0][0].focus();
  6084. }
  6085. }
  6086. };
  6087. /**
  6088. * Find all controls associated with the given settings.
  6089. *
  6090. * @alias wp.customize.findControlsForSettings
  6091. *
  6092. * @since 4.6.0
  6093. * @param {string[]} settingIds Setting IDs.
  6094. * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
  6095. */
  6096. api.findControlsForSettings = function findControlsForSettings( settingIds ) {
  6097. var controls = {}, settingControls;
  6098. _.each( _.unique( settingIds ), function( settingId ) {
  6099. var setting = api( settingId );
  6100. if ( setting ) {
  6101. settingControls = setting.findControls();
  6102. if ( settingControls && settingControls.length > 0 ) {
  6103. controls[ settingId ] = settingControls;
  6104. }
  6105. }
  6106. } );
  6107. return controls;
  6108. };
  6109. /**
  6110. * Sort panels, sections, controls by priorities. Hide empty sections and panels.
  6111. *
  6112. * @alias wp.customize.reflowPaneContents
  6113. *
  6114. * @since 4.1.0
  6115. */
  6116. api.reflowPaneContents = _.bind( function () {
  6117. var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
  6118. if ( document.activeElement ) {
  6119. activeElement = $( document.activeElement );
  6120. }
  6121. // Sort the sections within each panel
  6122. api.panel.each( function ( panel ) {
  6123. if ( 'themes' === panel.id ) {
  6124. return; // Don't reflow theme sections, as doing so moves them after the themes container.
  6125. }
  6126. var sections = panel.sections(),
  6127. sectionHeadContainers = _.pluck( sections, 'headContainer' );
  6128. rootNodes.push( panel );
  6129. appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
  6130. if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
  6131. _( sections ).each( function ( section ) {
  6132. appendContainer.append( section.headContainer );
  6133. } );
  6134. wasReflowed = true;
  6135. }
  6136. } );
  6137. // Sort the controls within each section
  6138. api.section.each( function ( section ) {
  6139. var controls = section.controls(),
  6140. controlContainers = _.pluck( controls, 'container' );
  6141. if ( ! section.panel() ) {
  6142. rootNodes.push( section );
  6143. }
  6144. appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  6145. if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
  6146. _( controls ).each( function ( control ) {
  6147. appendContainer.append( control.container );
  6148. } );
  6149. wasReflowed = true;
  6150. }
  6151. } );
  6152. // Sort the root panels and sections
  6153. rootNodes.sort( api.utils.prioritySort );
  6154. rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
  6155. appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  6156. if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
  6157. _( rootNodes ).each( function ( rootNode ) {
  6158. appendContainer.append( rootNode.headContainer );
  6159. } );
  6160. wasReflowed = true;
  6161. }
  6162. // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
  6163. api.panel.each( function ( panel ) {
  6164. var value = panel.active();
  6165. panel.active.callbacks.fireWith( panel.active, [ value, value ] );
  6166. } );
  6167. api.section.each( function ( section ) {
  6168. var value = section.active();
  6169. section.active.callbacks.fireWith( section.active, [ value, value ] );
  6170. } );
  6171. // Restore focus if there was a reflow and there was an active (focused) element
  6172. if ( wasReflowed && activeElement ) {
  6173. activeElement.focus();
  6174. }
  6175. api.trigger( 'pane-contents-reflowed' );
  6176. }, api );
  6177. // Define state values.
  6178. api.state = new api.Values();
  6179. _.each( [
  6180. 'saved',
  6181. 'saving',
  6182. 'trashing',
  6183. 'activated',
  6184. 'processing',
  6185. 'paneVisible',
  6186. 'expandedPanel',
  6187. 'expandedSection',
  6188. 'changesetDate',
  6189. 'selectedChangesetDate',
  6190. 'changesetStatus',
  6191. 'selectedChangesetStatus',
  6192. 'remainingTimeToPublish',
  6193. 'previewerAlive',
  6194. 'editShortcutVisibility',
  6195. 'changesetLocked',
  6196. 'previewedDevice'
  6197. ], function( name ) {
  6198. api.state.create( name );
  6199. });
  6200. $( function() {
  6201. api.settings = window._wpCustomizeSettings;
  6202. api.l10n = window._wpCustomizeControlsL10n;
  6203. // Check if we can run the Customizer.
  6204. if ( ! api.settings ) {
  6205. return;
  6206. }
  6207. // Bail if any incompatibilities are found.
  6208. if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
  6209. return;
  6210. }
  6211. if ( null === api.PreviewFrame.prototype.sensitivity ) {
  6212. api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
  6213. }
  6214. if ( null === api.Previewer.prototype.refreshBuffer ) {
  6215. api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
  6216. }
  6217. var parent,
  6218. body = $( document.body ),
  6219. overlay = body.children( '.wp-full-overlay' ),
  6220. title = $( '#customize-info .panel-title.site-title' ),
  6221. closeBtn = $( '.customize-controls-close' ),
  6222. saveBtn = $( '#save' ),
  6223. btnWrapper = $( '#customize-save-button-wrapper' ),
  6224. publishSettingsBtn = $( '#publish-settings' ),
  6225. footerActions = $( '#customize-footer-actions' );
  6226. // Add publish settings section in JS instead of PHP since the Customizer depends on it to function.
  6227. api.bind( 'ready', function() {
  6228. api.section.add( new api.OuterSection( 'publish_settings', {
  6229. title: api.l10n.publishSettings,
  6230. priority: 0,
  6231. active: api.settings.theme.active
  6232. } ) );
  6233. } );
  6234. // Set up publish settings section and its controls.
  6235. api.section( 'publish_settings', function( section ) {
  6236. var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000;
  6237. trashControl = new api.Control( 'trash_changeset', {
  6238. type: 'button',
  6239. section: section.id,
  6240. priority: 30,
  6241. input_attrs: {
  6242. 'class': 'button-link button-link-delete',
  6243. value: api.l10n.discardChanges
  6244. }
  6245. } );
  6246. api.control.add( trashControl );
  6247. trashControl.deferred.embedded.done( function() {
  6248. trashControl.container.find( '.button-link' ).on( 'click', function() {
  6249. if ( confirm( api.l10n.trashConfirm ) ) {
  6250. wp.customize.previewer.trash();
  6251. }
  6252. } );
  6253. } );
  6254. api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', {
  6255. section: section.id,
  6256. priority: 100
  6257. } ) );
  6258. /**
  6259. * Return whether the pubish settings section should be active.
  6260. *
  6261. * @return {boolean} Is section active.
  6262. */
  6263. isSectionActive = function() {
  6264. if ( ! api.state( 'activated' ).get() ) {
  6265. return false;
  6266. }
  6267. if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
  6268. return false;
  6269. }
  6270. if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) {
  6271. return false;
  6272. }
  6273. return true;
  6274. };
  6275. // Make sure publish settings are not available while the theme is not active and the customizer is in a published state.
  6276. section.active.validate = isSectionActive;
  6277. updateSectionActive = function() {
  6278. section.active.set( isSectionActive() );
  6279. };
  6280. api.state( 'activated' ).bind( updateSectionActive );
  6281. api.state( 'trashing' ).bind( updateSectionActive );
  6282. api.state( 'saved' ).bind( updateSectionActive );
  6283. api.state( 'changesetStatus' ).bind( updateSectionActive );
  6284. updateSectionActive();
  6285. // Bind visibility of the publish settings button to whether the section is active.
  6286. updateButtonsState = function() {
  6287. publishSettingsBtn.toggle( section.active.get() );
  6288. saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
  6289. };
  6290. updateButtonsState();
  6291. section.active.bind( updateButtonsState );
  6292. function highlightScheduleButton() {
  6293. if ( ! cancelScheduleButtonReminder ) {
  6294. cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, {
  6295. delay: 1000,
  6296. // Only abort the reminder when the save button is focused.
  6297. // If the user clicks the settings button to toggle the
  6298. // settings closed, we'll still remind them.
  6299. focusTarget: saveBtn
  6300. } );
  6301. }
  6302. }
  6303. function cancelHighlightScheduleButton() {
  6304. if ( cancelScheduleButtonReminder ) {
  6305. cancelScheduleButtonReminder();
  6306. cancelScheduleButtonReminder = null;
  6307. }
  6308. }
  6309. api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton );
  6310. section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
  6311. section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
  6312. publishSettingsBtn.prop( 'disabled', false );
  6313. publishSettingsBtn.on( 'click', function( event ) {
  6314. event.preventDefault();
  6315. section.expanded.set( ! section.expanded.get() );
  6316. } );
  6317. section.expanded.bind( function( isExpanded ) {
  6318. var defaultChangesetStatus;
  6319. publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
  6320. publishSettingsBtn.toggleClass( 'active', isExpanded );
  6321. if ( isExpanded ) {
  6322. cancelHighlightScheduleButton();
  6323. return;
  6324. }
  6325. defaultChangesetStatus = api.state( 'changesetStatus' ).get();
  6326. if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
  6327. defaultChangesetStatus = 'publish';
  6328. }
  6329. if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
  6330. highlightScheduleButton();
  6331. } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
  6332. highlightScheduleButton();
  6333. }
  6334. } );
  6335. statusControl = new api.Control( 'changeset_status', {
  6336. priority: 10,
  6337. type: 'radio',
  6338. section: 'publish_settings',
  6339. setting: api.state( 'selectedChangesetStatus' ),
  6340. templateId: 'customize-selected-changeset-status-control',
  6341. label: api.l10n.action,
  6342. choices: api.settings.changeset.statusChoices
  6343. } );
  6344. api.control.add( statusControl );
  6345. dateControl = new api.DateTimeControl( 'changeset_scheduled_date', {
  6346. priority: 20,
  6347. section: 'publish_settings',
  6348. setting: api.state( 'selectedChangesetDate' ),
  6349. minYear: ( new Date() ).getFullYear(),
  6350. allowPastDate: false,
  6351. includeTime: true,
  6352. twelveHourFormat: /a/i.test( api.settings.timeFormat ),
  6353. description: api.l10n.scheduleDescription
  6354. } );
  6355. dateControl.notifications.alt = true;
  6356. api.control.add( dateControl );
  6357. publishWhenTime = function() {
  6358. api.state( 'selectedChangesetStatus' ).set( 'publish' );
  6359. api.previewer.save();
  6360. };
  6361. // Start countdown for when the dateTime arrives, or clear interval when it is .
  6362. updateTimeArrivedPoller = function() {
  6363. var shouldPoll = (
  6364. 'future' === api.state( 'changesetStatus' ).get() &&
  6365. 'future' === api.state( 'selectedChangesetStatus' ).get() &&
  6366. api.state( 'changesetDate' ).get() &&
  6367. api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
  6368. api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
  6369. );
  6370. if ( shouldPoll && ! pollInterval ) {
  6371. pollInterval = setInterval( function() {
  6372. var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
  6373. api.state( 'remainingTimeToPublish' ).set( remainingTime );
  6374. if ( remainingTime <= 0 ) {
  6375. clearInterval( pollInterval );
  6376. pollInterval = 0;
  6377. publishWhenTime();
  6378. }
  6379. }, timeArrivedPollingInterval );
  6380. } else if ( ! shouldPoll && pollInterval ) {
  6381. clearInterval( pollInterval );
  6382. pollInterval = 0;
  6383. }
  6384. };
  6385. api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
  6386. api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
  6387. api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
  6388. api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
  6389. updateTimeArrivedPoller();
  6390. // Ensure dateControl only appears when selected status is future.
  6391. dateControl.active.validate = function() {
  6392. return 'future' === api.state( 'selectedChangesetStatus' ).get();
  6393. };
  6394. toggleDateControl = function( value ) {
  6395. dateControl.active.set( 'future' === value );
  6396. };
  6397. toggleDateControl( api.state( 'selectedChangesetStatus' ).get() );
  6398. api.state( 'selectedChangesetStatus' ).bind( toggleDateControl );
  6399. // Show notification on date control when status is future but it isn't a future date.
  6400. api.state( 'saving' ).bind( function( isSaving ) {
  6401. if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
  6402. dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
  6403. }
  6404. } );
  6405. } );
  6406. // Prevent the form from saving when enter is pressed on an input or select element.
  6407. $('#customize-controls').on( 'keydown', function( e ) {
  6408. var isEnter = ( 13 === e.which ),
  6409. $el = $( e.target );
  6410. if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
  6411. e.preventDefault();
  6412. }
  6413. });
  6414. // Expand/Collapse the main customizer customize info.
  6415. $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
  6416. var section = $( this ).closest( '.accordion-section' ),
  6417. content = section.find( '.customize-panel-description:first' );
  6418. if ( section.hasClass( 'cannot-expand' ) ) {
  6419. return;
  6420. }
  6421. if ( section.hasClass( 'open' ) ) {
  6422. section.toggleClass( 'open' );
  6423. content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() {
  6424. content.trigger( 'toggled' );
  6425. } );
  6426. $( this ).attr( 'aria-expanded', false );
  6427. } else {
  6428. content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() {
  6429. content.trigger( 'toggled' );
  6430. } );
  6431. section.toggleClass( 'open' );
  6432. $( this ).attr( 'aria-expanded', true );
  6433. }
  6434. });
  6435. /**
  6436. * Initialize Previewer
  6437. *
  6438. * @alias wp.customize.previewer
  6439. */
  6440. api.previewer = new api.Previewer({
  6441. container: '#customize-preview',
  6442. form: '#customize-controls',
  6443. previewUrl: api.settings.url.preview,
  6444. allowedUrls: api.settings.url.allowed
  6445. },/** @lends wp.customize.previewer */{
  6446. nonce: api.settings.nonce,
  6447. /**
  6448. * Build the query to send along with the Preview request.
  6449. *
  6450. * @since 3.4.0
  6451. * @since 4.7.0 Added options param.
  6452. * @access public
  6453. *
  6454. * @param {object} [options] Options.
  6455. * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
  6456. * @return {object} Query vars.
  6457. */
  6458. query: function( options ) {
  6459. var queryVars = {
  6460. wp_customize: 'on',
  6461. customize_theme: api.settings.theme.stylesheet,
  6462. nonce: this.nonce.preview,
  6463. customize_changeset_uuid: api.settings.changeset.uuid
  6464. };
  6465. if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
  6466. queryVars.customize_autosaved = 'on';
  6467. }
  6468. /*
  6469. * Exclude customized data if requested especially for calls to requestChangesetUpdate.
  6470. * Changeset updates are differential and so it is a performance waste to send all of
  6471. * the dirty settings with each update.
  6472. */
  6473. queryVars.customized = JSON.stringify( api.dirtyValues( {
  6474. unsaved: options && options.excludeCustomizedSaved
  6475. } ) );
  6476. return queryVars;
  6477. },
  6478. /**
  6479. * Save (and publish) the customizer changeset.
  6480. *
  6481. * Updates to the changeset are transactional. If any of the settings
  6482. * are invalid then none of them will be written into the changeset.
  6483. * A revision will be made for the changeset post if revisions support
  6484. * has been added to the post type.
  6485. *
  6486. * @since 3.4.0
  6487. * @since 4.7.0 Added args param and return value.
  6488. *
  6489. * @param {object} [args] Args.
  6490. * @param {string} [args.status=publish] Status.
  6491. * @param {string} [args.date] Date, in local time in MySQL format.
  6492. * @param {string} [args.title] Title
  6493. * @returns {jQuery.promise} Promise.
  6494. */
  6495. save: function( args ) {
  6496. var previewer = this,
  6497. deferred = $.Deferred(),
  6498. changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
  6499. selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
  6500. processing = api.state( 'processing' ),
  6501. submitWhenDoneProcessing,
  6502. submit,
  6503. modifiedWhileSaving = {},
  6504. invalidSettings = [],
  6505. invalidControls = [],
  6506. invalidSettingLessControls = [];
  6507. if ( args && args.status ) {
  6508. changesetStatus = args.status;
  6509. }
  6510. if ( api.state( 'saving' ).get() ) {
  6511. deferred.reject( 'already_saving' );
  6512. deferred.promise();
  6513. }
  6514. api.state( 'saving' ).set( true );
  6515. function captureSettingModifiedDuringSave( setting ) {
  6516. modifiedWhileSaving[ setting.id ] = true;
  6517. }
  6518. submit = function () {
  6519. var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error';
  6520. api.bind( 'change', captureSettingModifiedDuringSave );
  6521. api.notifications.remove( errorCode );
  6522. /*
  6523. * Block saving if there are any settings that are marked as
  6524. * invalid from the client (not from the server). Focus on
  6525. * the control.
  6526. */
  6527. api.each( function( setting ) {
  6528. setting.notifications.each( function( notification ) {
  6529. if ( 'error' === notification.type && ! notification.fromServer ) {
  6530. invalidSettings.push( setting.id );
  6531. if ( ! settingInvalidities[ setting.id ] ) {
  6532. settingInvalidities[ setting.id ] = {};
  6533. }
  6534. settingInvalidities[ setting.id ][ notification.code ] = notification;
  6535. }
  6536. } );
  6537. } );
  6538. // Find all invalid setting less controls with notification type error.
  6539. api.control.each( function( control ) {
  6540. if ( ! control.setting || ! control.setting.id && control.active.get() ) {
  6541. control.notifications.each( function( notification ) {
  6542. if ( 'error' === notification.type ) {
  6543. invalidSettingLessControls.push( [ control ] );
  6544. }
  6545. } );
  6546. }
  6547. } );
  6548. invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
  6549. if ( ! _.isEmpty( invalidControls ) ) {
  6550. invalidControls[0][0].focus();
  6551. api.unbind( 'change', captureSettingModifiedDuringSave );
  6552. if ( invalidSettings.length ) {
  6553. api.notifications.add( new api.Notification( errorCode, {
  6554. message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
  6555. type: 'error',
  6556. dismissible: true,
  6557. saveFailure: true
  6558. } ) );
  6559. }
  6560. deferred.rejectWith( previewer, [
  6561. { setting_invalidities: settingInvalidities }
  6562. ] );
  6563. api.state( 'saving' ).set( false );
  6564. return deferred.promise();
  6565. }
  6566. /*
  6567. * Note that excludeCustomizedSaved is intentionally false so that the entire
  6568. * set of customized data will be included if bypassed changeset update.
  6569. */
  6570. query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
  6571. nonce: previewer.nonce.save,
  6572. customize_changeset_status: changesetStatus
  6573. } );
  6574. if ( args && args.date ) {
  6575. query.customize_changeset_date = args.date;
  6576. } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
  6577. query.customize_changeset_date = selectedChangesetDate;
  6578. }
  6579. if ( args && args.title ) {
  6580. query.customize_changeset_title = args.title;
  6581. }
  6582. // Allow plugins to modify the params included with the save request.
  6583. api.trigger( 'save-request-params', query );
  6584. /*
  6585. * Note that the dirty customized values will have already been set in the
  6586. * changeset and so technically query.customized could be deleted. However,
  6587. * it is remaining here to make sure that any settings that got updated
  6588. * quietly which may have not triggered an update request will also get
  6589. * included in the values that get saved to the changeset. This will ensure
  6590. * that values that get injected via the saved event will be included in
  6591. * the changeset. This also ensures that setting values that were invalid
  6592. * will get re-validated, perhaps in the case of settings that are invalid
  6593. * due to dependencies on other settings.
  6594. */
  6595. request = wp.ajax.post( 'customize_save', query );
  6596. api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  6597. api.trigger( 'save', request );
  6598. request.always( function () {
  6599. api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  6600. api.state( 'saving' ).set( false );
  6601. api.unbind( 'change', captureSettingModifiedDuringSave );
  6602. } );
  6603. // Remove notifications that were added due to save failures.
  6604. api.notifications.each( function( notification ) {
  6605. if ( notification.saveFailure ) {
  6606. api.notifications.remove( notification.code );
  6607. }
  6608. });
  6609. request.fail( function ( response ) {
  6610. var notification, notificationArgs;
  6611. notificationArgs = {
  6612. type: 'error',
  6613. dismissible: true,
  6614. fromServer: true,
  6615. saveFailure: true
  6616. };
  6617. if ( '0' === response ) {
  6618. response = 'not_logged_in';
  6619. } else if ( '-1' === response ) {
  6620. // Back-compat in case any other check_ajax_referer() call is dying
  6621. response = 'invalid_nonce';
  6622. }
  6623. if ( 'invalid_nonce' === response ) {
  6624. previewer.cheatin();
  6625. } else if ( 'not_logged_in' === response ) {
  6626. previewer.preview.iframe.hide();
  6627. previewer.login().done( function() {
  6628. previewer.save();
  6629. previewer.preview.iframe.show();
  6630. } );
  6631. } else if ( response.code ) {
  6632. if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
  6633. api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
  6634. } else if ( 'changeset_locked' !== response.code ) {
  6635. notification = new api.Notification( response.code, _.extend( notificationArgs, {
  6636. message: response.message
  6637. } ) );
  6638. }
  6639. } else {
  6640. notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
  6641. message: api.l10n.unknownRequestFail
  6642. } ) );
  6643. }
  6644. if ( notification ) {
  6645. api.notifications.add( notification );
  6646. }
  6647. if ( response.setting_validities ) {
  6648. api._handleSettingValidities( {
  6649. settingValidities: response.setting_validities,
  6650. focusInvalidControl: true
  6651. } );
  6652. }
  6653. deferred.rejectWith( previewer, [ response ] );
  6654. api.trigger( 'error', response );
  6655. // Start a new changeset if the underlying changeset was published.
  6656. if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
  6657. api.settings.changeset.uuid = response.next_changeset_uuid;
  6658. api.state( 'changesetStatus' ).set( '' );
  6659. if ( api.settings.changeset.branching ) {
  6660. parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  6661. }
  6662. api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
  6663. }
  6664. } );
  6665. request.done( function( response ) {
  6666. previewer.send( 'saved', response );
  6667. api.state( 'changesetStatus' ).set( response.changeset_status );
  6668. if ( response.changeset_date ) {
  6669. api.state( 'changesetDate' ).set( response.changeset_date );
  6670. }
  6671. if ( 'publish' === response.changeset_status ) {
  6672. // Mark all published as clean if they haven't been modified during the request.
  6673. api.each( function( setting ) {
  6674. /*
  6675. * Note that the setting revision will be undefined in the case of setting
  6676. * values that are marked as dirty when the customizer is loaded, such as
  6677. * when applying starter content. All other dirty settings will have an
  6678. * associated revision due to their modification triggering a change event.
  6679. */
  6680. if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
  6681. setting._dirty = false;
  6682. }
  6683. } );
  6684. api.state( 'changesetStatus' ).set( '' );
  6685. api.settings.changeset.uuid = response.next_changeset_uuid;
  6686. if ( api.settings.changeset.branching ) {
  6687. parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  6688. }
  6689. }
  6690. // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
  6691. api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
  6692. if ( response.setting_validities ) {
  6693. api._handleSettingValidities( {
  6694. settingValidities: response.setting_validities,
  6695. focusInvalidControl: true
  6696. } );
  6697. }
  6698. deferred.resolveWith( previewer, [ response ] );
  6699. api.trigger( 'saved', response );
  6700. // Restore the global dirty state if any settings were modified during save.
  6701. if ( ! _.isEmpty( modifiedWhileSaving ) ) {
  6702. api.state( 'saved' ).set( false );
  6703. }
  6704. } );
  6705. };
  6706. if ( 0 === processing() ) {
  6707. submit();
  6708. } else {
  6709. submitWhenDoneProcessing = function () {
  6710. if ( 0 === processing() ) {
  6711. api.state.unbind( 'change', submitWhenDoneProcessing );
  6712. submit();
  6713. }
  6714. };
  6715. api.state.bind( 'change', submitWhenDoneProcessing );
  6716. }
  6717. return deferred.promise();
  6718. },
  6719. /**
  6720. * Trash the current changes.
  6721. *
  6722. * Revert the Customizer to it's previously-published state.
  6723. *
  6724. * @since 4.9.0
  6725. *
  6726. * @returns {jQuery.promise} Promise.
  6727. */
  6728. trash: function trash() {
  6729. var request, success, fail;
  6730. api.state( 'trashing' ).set( true );
  6731. api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  6732. request = wp.ajax.post( 'customize_trash', {
  6733. customize_changeset_uuid: api.settings.changeset.uuid,
  6734. nonce: api.settings.nonce.trash
  6735. } );
  6736. api.notifications.add( new api.OverlayNotification( 'changeset_trashing', {
  6737. type: 'info',
  6738. message: api.l10n.revertingChanges,
  6739. loading: true
  6740. } ) );
  6741. success = function() {
  6742. var urlParser = document.createElement( 'a' ), queryParams;
  6743. api.state( 'changesetStatus' ).set( 'trash' );
  6744. api.each( function( setting ) {
  6745. setting._dirty = false;
  6746. } );
  6747. api.state( 'saved' ).set( true );
  6748. // Go back to Customizer without changeset.
  6749. urlParser.href = location.href;
  6750. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  6751. delete queryParams.changeset_uuid;
  6752. queryParams['return'] = api.settings.url['return'];
  6753. urlParser.search = $.param( queryParams );
  6754. location.replace( urlParser.href );
  6755. };
  6756. fail = function( code, message ) {
  6757. var notificationCode = code || 'unknown_error';
  6758. api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  6759. api.state( 'trashing' ).set( false );
  6760. api.notifications.remove( 'changeset_trashing' );
  6761. api.notifications.add( new api.Notification( notificationCode, {
  6762. message: message || api.l10n.unknownError,
  6763. dismissible: true,
  6764. type: 'error'
  6765. } ) );
  6766. };
  6767. request.done( function( response ) {
  6768. success( response.message );
  6769. } );
  6770. request.fail( function( response ) {
  6771. var code = response.code || 'trashing_failed';
  6772. if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
  6773. success( response.message );
  6774. } else {
  6775. fail( code, response.message );
  6776. }
  6777. } );
  6778. },
  6779. /**
  6780. * Builds the front preview url with the current state of customizer.
  6781. *
  6782. * @since 4.9
  6783. *
  6784. * @return {string} Preview url.
  6785. */
  6786. getFrontendPreviewUrl: function() {
  6787. var previewer = this, params, urlParser;
  6788. urlParser = document.createElement( 'a' );
  6789. urlParser.href = previewer.previewUrl.get();
  6790. params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  6791. if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
  6792. params.customize_changeset_uuid = api.settings.changeset.uuid;
  6793. }
  6794. if ( ! api.state( 'activated' ).get() ) {
  6795. params.customize_theme = api.settings.theme.stylesheet;
  6796. }
  6797. urlParser.search = $.param( params );
  6798. return urlParser.href;
  6799. }
  6800. });
  6801. // Ensure preview nonce is included with every customized request, to allow post data to be read.
  6802. $.ajaxPrefilter( function injectPreviewNonce( options ) {
  6803. if ( ! /wp_customize=on/.test( options.data ) ) {
  6804. return;
  6805. }
  6806. options.data += '&' + $.param({
  6807. customize_preview_nonce: api.settings.nonce.preview
  6808. });
  6809. });
  6810. // Refresh the nonces if the preview sends updated nonces over.
  6811. api.previewer.bind( 'nonce', function( nonce ) {
  6812. $.extend( this.nonce, nonce );
  6813. });
  6814. // Refresh the nonces if login sends updated nonces over.
  6815. api.bind( 'nonce-refresh', function( nonce ) {
  6816. $.extend( api.settings.nonce, nonce );
  6817. $.extend( api.previewer.nonce, nonce );
  6818. api.previewer.send( 'nonce-refresh', nonce );
  6819. });
  6820. // Create Settings
  6821. $.each( api.settings.settings, function( id, data ) {
  6822. var Constructor = api.settingConstructor[ data.type ] || api.Setting;
  6823. api.add( new Constructor( id, data.value, {
  6824. transport: data.transport,
  6825. previewer: api.previewer,
  6826. dirty: !! data.dirty
  6827. } ) );
  6828. });
  6829. // Create Panels
  6830. $.each( api.settings.panels, function ( id, data ) {
  6831. var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
  6832. options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
  6833. api.panel.add( new Constructor( id, options ) );
  6834. });
  6835. // Create Sections
  6836. $.each( api.settings.sections, function ( id, data ) {
  6837. var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
  6838. options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
  6839. api.section.add( new Constructor( id, options ) );
  6840. });
  6841. // Create Controls
  6842. $.each( api.settings.controls, function( id, data ) {
  6843. var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
  6844. options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
  6845. api.control.add( new Constructor( id, options ) );
  6846. });
  6847. // Focus the autofocused element
  6848. _.each( [ 'panel', 'section', 'control' ], function( type ) {
  6849. var id = api.settings.autofocus[ type ];
  6850. if ( ! id ) {
  6851. return;
  6852. }
  6853. /*
  6854. * Defer focus until:
  6855. * 1. The panel, section, or control exists (especially for dynamically-created ones).
  6856. * 2. The instance is embedded in the document (and so is focusable).
  6857. * 3. The preview has finished loading so that the active states have been set.
  6858. */
  6859. api[ type ]( id, function( instance ) {
  6860. instance.deferred.embedded.done( function() {
  6861. api.previewer.deferred.active.done( function() {
  6862. instance.focus();
  6863. });
  6864. });
  6865. });
  6866. });
  6867. api.bind( 'ready', api.reflowPaneContents );
  6868. $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
  6869. var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
  6870. values.bind( 'add', debouncedReflowPaneContents );
  6871. values.bind( 'change', debouncedReflowPaneContents );
  6872. values.bind( 'remove', debouncedReflowPaneContents );
  6873. } );
  6874. // Set up global notifications area.
  6875. api.bind( 'ready', function setUpGlobalNotificationsArea() {
  6876. var sidebar, containerHeight, containerInitialTop;
  6877. api.notifications.container = $( '#customize-notifications-area' );
  6878. api.notifications.bind( 'change', _.debounce( function() {
  6879. api.notifications.render();
  6880. } ) );
  6881. sidebar = $( '.wp-full-overlay-sidebar-content' );
  6882. api.notifications.bind( 'rendered', function updateSidebarTop() {
  6883. sidebar.css( 'top', '' );
  6884. if ( 0 !== api.notifications.count() ) {
  6885. containerHeight = api.notifications.container.outerHeight() + 1;
  6886. containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
  6887. sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
  6888. }
  6889. api.notifications.trigger( 'sidebarTopUpdated' );
  6890. });
  6891. api.notifications.render();
  6892. });
  6893. // Save and activated states
  6894. (function( state ) {
  6895. var saved = state.instance( 'saved' ),
  6896. saving = state.instance( 'saving' ),
  6897. trashing = state.instance( 'trashing' ),
  6898. activated = state.instance( 'activated' ),
  6899. processing = state.instance( 'processing' ),
  6900. paneVisible = state.instance( 'paneVisible' ),
  6901. expandedPanel = state.instance( 'expandedPanel' ),
  6902. expandedSection = state.instance( 'expandedSection' ),
  6903. changesetStatus = state.instance( 'changesetStatus' ),
  6904. selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
  6905. changesetDate = state.instance( 'changesetDate' ),
  6906. selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
  6907. previewerAlive = state.instance( 'previewerAlive' ),
  6908. editShortcutVisibility = state.instance( 'editShortcutVisibility' ),
  6909. changesetLocked = state.instance( 'changesetLocked' ),
  6910. populateChangesetUuidParam, defaultSelectedChangesetStatus;
  6911. state.bind( 'change', function() {
  6912. var canSave;
  6913. if ( ! activated() ) {
  6914. saveBtn.val( api.l10n.activate );
  6915. closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  6916. } else if ( '' === changesetStatus.get() && saved() ) {
  6917. if ( api.settings.changeset.currentUserCanPublish ) {
  6918. saveBtn.val( api.l10n.published );
  6919. } else {
  6920. saveBtn.val( api.l10n.saved );
  6921. }
  6922. closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
  6923. } else {
  6924. if ( 'draft' === selectedChangesetStatus() ) {
  6925. if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
  6926. saveBtn.val( api.l10n.draftSaved );
  6927. } else {
  6928. saveBtn.val( api.l10n.saveDraft );
  6929. }
  6930. } else if ( 'future' === selectedChangesetStatus() ) {
  6931. if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
  6932. if ( changesetDate.get() !== selectedChangesetDate.get() ) {
  6933. saveBtn.val( api.l10n.schedule );
  6934. } else {
  6935. saveBtn.val( api.l10n.scheduled );
  6936. }
  6937. } else {
  6938. saveBtn.val( api.l10n.schedule );
  6939. }
  6940. } else if ( api.settings.changeset.currentUserCanPublish ) {
  6941. saveBtn.val( api.l10n.publish );
  6942. }
  6943. closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  6944. }
  6945. /*
  6946. * Save (publish) button should be enabled if saving is not currently happening,
  6947. * and if the theme is not active or the changeset exists but is not published.
  6948. */
  6949. canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
  6950. saveBtn.prop( 'disabled', ! canSave );
  6951. });
  6952. selectedChangesetStatus.validate = function( status ) {
  6953. if ( '' === status || 'auto-draft' === status ) {
  6954. return null;
  6955. }
  6956. return status;
  6957. };
  6958. defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft';
  6959. // Set default states.
  6960. changesetStatus( api.settings.changeset.status );
  6961. changesetLocked( Boolean( api.settings.changeset.lockUser ) );
  6962. changesetDate( api.settings.changeset.publishDate );
  6963. selectedChangesetDate( api.settings.changeset.publishDate );
  6964. selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status );
  6965. selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
  6966. saved( true );
  6967. if ( '' === changesetStatus() ) { // Handle case for loading starter content.
  6968. api.each( function( setting ) {
  6969. if ( setting._dirty ) {
  6970. saved( false );
  6971. }
  6972. } );
  6973. }
  6974. saving( false );
  6975. activated( api.settings.theme.active );
  6976. processing( 0 );
  6977. paneVisible( true );
  6978. expandedPanel( false );
  6979. expandedSection( false );
  6980. previewerAlive( true );
  6981. editShortcutVisibility( 'visible' );
  6982. api.bind( 'change', function() {
  6983. if ( state( 'saved' ).get() ) {
  6984. state( 'saved' ).set( false );
  6985. }
  6986. });
  6987. // Populate changeset UUID param when state becomes dirty.
  6988. if ( api.settings.changeset.branching ) {
  6989. saved.bind( function( isSaved ) {
  6990. if ( ! isSaved ) {
  6991. populateChangesetUuidParam( true );
  6992. }
  6993. });
  6994. }
  6995. saving.bind( function( isSaving ) {
  6996. body.toggleClass( 'saving', isSaving );
  6997. } );
  6998. trashing.bind( function( isTrashing ) {
  6999. body.toggleClass( 'trashing', isTrashing );
  7000. } );
  7001. api.bind( 'saved', function( response ) {
  7002. state('saved').set( true );
  7003. if ( 'publish' === response.changeset_status ) {
  7004. state( 'activated' ).set( true );
  7005. }
  7006. });
  7007. activated.bind( function( to ) {
  7008. if ( to ) {
  7009. api.trigger( 'activated' );
  7010. }
  7011. });
  7012. /**
  7013. * Populate URL with UUID via `history.replaceState()`.
  7014. *
  7015. * @since 4.7.0
  7016. * @access private
  7017. *
  7018. * @param {boolean} isIncluded Is UUID included.
  7019. * @returns {void}
  7020. */
  7021. populateChangesetUuidParam = function( isIncluded ) {
  7022. var urlParser, queryParams;
  7023. // Abort on IE9 which doesn't support history management.
  7024. if ( ! history.replaceState ) {
  7025. return;
  7026. }
  7027. urlParser = document.createElement( 'a' );
  7028. urlParser.href = location.href;
  7029. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  7030. if ( isIncluded ) {
  7031. if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
  7032. return;
  7033. }
  7034. queryParams.changeset_uuid = api.settings.changeset.uuid;
  7035. } else {
  7036. if ( ! queryParams.changeset_uuid ) {
  7037. return;
  7038. }
  7039. delete queryParams.changeset_uuid;
  7040. }
  7041. urlParser.search = $.param( queryParams );
  7042. history.replaceState( {}, document.title, urlParser.href );
  7043. };
  7044. // Show changeset UUID in URL when in branching mode and there is a saved changeset.
  7045. if ( api.settings.changeset.branching ) {
  7046. changesetStatus.bind( function( newStatus ) {
  7047. populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
  7048. } );
  7049. }
  7050. }( api.state ) );
  7051. /**
  7052. * Handles lock notice and take over request.
  7053. *
  7054. * @since 4.9.0
  7055. */
  7056. ( function checkAndDisplayLockNotice() {
  7057. var LockedNotification = api.OverlayNotification.extend(/** @lends wp.customize~LockedNotification.prototype */{
  7058. /**
  7059. * Template ID.
  7060. *
  7061. * @type {string}
  7062. */
  7063. templateId: 'customize-changeset-locked-notification',
  7064. /**
  7065. * Lock user.
  7066. *
  7067. * @type {object}
  7068. */
  7069. lockUser: null,
  7070. /**
  7071. * A notification that is displayed in a full-screen overlay with information about the locked changeset.
  7072. *
  7073. * @constructs wp.customize~LockedNotification
  7074. * @augments wp.customize.OverlayNotification
  7075. *
  7076. * @since 4.9.0
  7077. *
  7078. * @param {string} [code] - Code.
  7079. * @param {object} [params] - Params.
  7080. */
  7081. initialize: function( code, params ) {
  7082. var notification = this, _code, _params;
  7083. _code = code || 'changeset_locked';
  7084. _params = _.extend(
  7085. {
  7086. type: 'warning',
  7087. containerClasses: '',
  7088. lockUser: {}
  7089. },
  7090. params
  7091. );
  7092. _params.containerClasses += ' notification-changeset-locked';
  7093. api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
  7094. },
  7095. /**
  7096. * Render notification.
  7097. *
  7098. * @since 4.9.0
  7099. *
  7100. * @return {jQuery} Notification container.
  7101. */
  7102. render: function() {
  7103. var notification = this, li, data, takeOverButton, request;
  7104. data = _.extend(
  7105. {
  7106. allowOverride: false,
  7107. returnUrl: api.settings.url['return'],
  7108. previewUrl: api.previewer.previewUrl.get(),
  7109. frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
  7110. },
  7111. this
  7112. );
  7113. li = api.OverlayNotification.prototype.render.call( data );
  7114. // Try to autosave the changeset now.
  7115. api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
  7116. if ( ! response.autosaved ) {
  7117. li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
  7118. }
  7119. } );
  7120. takeOverButton = li.find( '.customize-notice-take-over-button' );
  7121. takeOverButton.on( 'click', function( event ) {
  7122. event.preventDefault();
  7123. if ( request ) {
  7124. return;
  7125. }
  7126. takeOverButton.addClass( 'disabled' );
  7127. request = wp.ajax.post( 'customize_override_changeset_lock', {
  7128. wp_customize: 'on',
  7129. customize_theme: api.settings.theme.stylesheet,
  7130. customize_changeset_uuid: api.settings.changeset.uuid,
  7131. nonce: api.settings.nonce.override_lock
  7132. } );
  7133. request.done( function() {
  7134. api.notifications.remove( notification.code ); // Remove self.
  7135. api.state( 'changesetLocked' ).set( false );
  7136. } );
  7137. request.fail( function( response ) {
  7138. var message = response.message || api.l10n.unknownRequestFail;
  7139. li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
  7140. request.always( function() {
  7141. takeOverButton.removeClass( 'disabled' );
  7142. } );
  7143. } );
  7144. request.always( function() {
  7145. request = null;
  7146. } );
  7147. } );
  7148. return li;
  7149. }
  7150. });
  7151. /**
  7152. * Start lock.
  7153. *
  7154. * @since 4.9.0
  7155. *
  7156. * @param {object} [args] - Args.
  7157. * @param {object} [args.lockUser] - Lock user data.
  7158. * @param {boolean} [args.allowOverride=false] - Whether override is allowed.
  7159. * @returns {void}
  7160. */
  7161. function startLock( args ) {
  7162. if ( args && args.lockUser ) {
  7163. api.settings.changeset.lockUser = args.lockUser;
  7164. }
  7165. api.state( 'changesetLocked' ).set( true );
  7166. api.notifications.add( new LockedNotification( 'changeset_locked', {
  7167. lockUser: api.settings.changeset.lockUser,
  7168. allowOverride: Boolean( args && args.allowOverride )
  7169. } ) );
  7170. }
  7171. // Show initial notification.
  7172. if ( api.settings.changeset.lockUser ) {
  7173. startLock( { allowOverride: true } );
  7174. }
  7175. // Check for lock when sending heartbeat requests.
  7176. $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
  7177. data.check_changeset_lock = true;
  7178. data.changeset_uuid = api.settings.changeset.uuid;
  7179. } );
  7180. // Handle heartbeat ticks.
  7181. $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
  7182. var notification, code = 'changeset_locked';
  7183. if ( ! data.customize_changeset_lock_user ) {
  7184. return;
  7185. }
  7186. // Update notification when a different user takes over.
  7187. notification = api.notifications( code );
  7188. if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
  7189. api.notifications.remove( code );
  7190. }
  7191. startLock( {
  7192. lockUser: data.customize_changeset_lock_user
  7193. } );
  7194. } );
  7195. // Handle locking in response to changeset save errors.
  7196. api.bind( 'error', function( response ) {
  7197. if ( 'changeset_locked' === response.code && response.lock_user ) {
  7198. startLock( {
  7199. lockUser: response.lock_user
  7200. } );
  7201. }
  7202. } );
  7203. } )();
  7204. // Set up initial notifications.
  7205. (function() {
  7206. var removedQueryParams = [], autosaveDismissed = false;
  7207. /**
  7208. * Obtain the URL to restore the autosave.
  7209. *
  7210. * @returns {string} Customizer URL.
  7211. */
  7212. function getAutosaveRestorationUrl() {
  7213. var urlParser, queryParams;
  7214. urlParser = document.createElement( 'a' );
  7215. urlParser.href = location.href;
  7216. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  7217. if ( api.settings.changeset.latestAutoDraftUuid ) {
  7218. queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
  7219. } else {
  7220. queryParams.customize_autosaved = 'on';
  7221. }
  7222. queryParams['return'] = api.settings.url['return'];
  7223. urlParser.search = $.param( queryParams );
  7224. return urlParser.href;
  7225. }
  7226. /**
  7227. * Remove parameter from the URL.
  7228. *
  7229. * @param {Array} params - Parameter names to remove.
  7230. * @returns {void}
  7231. */
  7232. function stripParamsFromLocation( params ) {
  7233. var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
  7234. urlParser.href = location.href;
  7235. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  7236. _.each( params, function( param ) {
  7237. if ( 'undefined' !== typeof queryParams[ param ] ) {
  7238. strippedParams += 1;
  7239. delete queryParams[ param ];
  7240. }
  7241. } );
  7242. if ( 0 === strippedParams ) {
  7243. return;
  7244. }
  7245. urlParser.search = $.param( queryParams );
  7246. history.replaceState( {}, document.title, urlParser.href );
  7247. }
  7248. /**
  7249. * Dismiss autosave.
  7250. *
  7251. * @returns {void}
  7252. */
  7253. function dismissAutosave() {
  7254. if ( autosaveDismissed ) {
  7255. return;
  7256. }
  7257. wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
  7258. wp_customize: 'on',
  7259. customize_theme: api.settings.theme.stylesheet,
  7260. customize_changeset_uuid: api.settings.changeset.uuid,
  7261. nonce: api.settings.nonce.dismiss_autosave_or_lock,
  7262. dismiss_autosave: true
  7263. } );
  7264. autosaveDismissed = true;
  7265. }
  7266. /**
  7267. * Add notification regarding the availability of an autosave to restore.
  7268. *
  7269. * @returns {void}
  7270. */
  7271. function addAutosaveRestoreNotification() {
  7272. var code = 'autosave_available', onStateChange;
  7273. // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
  7274. api.notifications.add( new api.Notification( code, {
  7275. message: api.l10n.autosaveNotice,
  7276. type: 'warning',
  7277. dismissible: true,
  7278. render: function() {
  7279. var li = api.Notification.prototype.render.call( this ), link;
  7280. // Handle clicking on restoration link.
  7281. link = li.find( 'a' );
  7282. link.prop( 'href', getAutosaveRestorationUrl() );
  7283. link.on( 'click', function( event ) {
  7284. event.preventDefault();
  7285. location.replace( getAutosaveRestorationUrl() );
  7286. } );
  7287. // Handle dismissal of notice.
  7288. li.find( '.notice-dismiss' ).on( 'click', dismissAutosave );
  7289. return li;
  7290. }
  7291. } ) );
  7292. // Remove the notification once the user starts making changes.
  7293. onStateChange = function() {
  7294. dismissAutosave();
  7295. api.notifications.remove( code );
  7296. api.unbind( 'change', onStateChange );
  7297. api.state( 'changesetStatus' ).unbind( onStateChange );
  7298. };
  7299. api.bind( 'change', onStateChange );
  7300. api.state( 'changesetStatus' ).bind( onStateChange );
  7301. }
  7302. if ( api.settings.changeset.autosaved ) {
  7303. api.state( 'saved' ).set( false );
  7304. removedQueryParams.push( 'customize_autosaved' );
  7305. }
  7306. if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) {
  7307. removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft.
  7308. }
  7309. if ( removedQueryParams.length > 0 ) {
  7310. stripParamsFromLocation( removedQueryParams );
  7311. }
  7312. if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
  7313. addAutosaveRestoreNotification();
  7314. }
  7315. })();
  7316. // Check if preview url is valid and load the preview frame.
  7317. if ( api.previewer.previewUrl() ) {
  7318. api.previewer.refresh();
  7319. } else {
  7320. api.previewer.previewUrl( api.settings.url.home );
  7321. }
  7322. // Button bindings.
  7323. saveBtn.click( function( event ) {
  7324. api.previewer.save();
  7325. event.preventDefault();
  7326. }).keydown( function( event ) {
  7327. if ( 9 === event.which ) { // Tab.
  7328. return;
  7329. }
  7330. if ( 13 === event.which ) { // Enter.
  7331. api.previewer.save();
  7332. }
  7333. event.preventDefault();
  7334. });
  7335. closeBtn.keydown( function( event ) {
  7336. if ( 9 === event.which ) { // Tab.
  7337. return;
  7338. }
  7339. if ( 13 === event.which ) { // Enter.
  7340. this.click();
  7341. }
  7342. event.preventDefault();
  7343. });
  7344. $( '.collapse-sidebar' ).on( 'click', function() {
  7345. api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  7346. });
  7347. api.state( 'paneVisible' ).bind( function( paneVisible ) {
  7348. overlay.toggleClass( 'preview-only', ! paneVisible );
  7349. overlay.toggleClass( 'expanded', paneVisible );
  7350. overlay.toggleClass( 'collapsed', ! paneVisible );
  7351. if ( ! paneVisible ) {
  7352. $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
  7353. } else {
  7354. $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
  7355. }
  7356. });
  7357. // Keyboard shortcuts - esc to exit section/panel.
  7358. body.on( 'keydown', function( event ) {
  7359. var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
  7360. if ( 27 !== event.which ) { // Esc.
  7361. return;
  7362. }
  7363. /*
  7364. * Abort if the event target is not the body (the default) and not inside of #customize-controls.
  7365. * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
  7366. */
  7367. if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
  7368. return;
  7369. }
  7370. // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
  7371. api.control.each( function( control ) {
  7372. if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
  7373. expandedControls.push( control );
  7374. }
  7375. });
  7376. api.section.each( function( section ) {
  7377. if ( section.expanded() ) {
  7378. expandedSections.push( section );
  7379. }
  7380. });
  7381. api.panel.each( function( panel ) {
  7382. if ( panel.expanded() ) {
  7383. expandedPanels.push( panel );
  7384. }
  7385. });
  7386. // Skip collapsing expanded controls if there are no expanded sections.
  7387. if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
  7388. expandedControls.length = 0;
  7389. }
  7390. // Collapse the most granular expanded object.
  7391. collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
  7392. if ( collapsedObject ) {
  7393. if ( 'themes' === collapsedObject.params.type ) {
  7394. // Themes panel or section.
  7395. if ( body.hasClass( 'modal-open' ) ) {
  7396. collapsedObject.closeDetails();
  7397. } else if ( api.panel.has( 'themes' ) ) {
  7398. // If we're collapsing a section, collapse the panel also.
  7399. api.panel( 'themes' ).collapse();
  7400. }
  7401. return;
  7402. }
  7403. collapsedObject.collapse();
  7404. event.preventDefault();
  7405. }
  7406. });
  7407. $( '.customize-controls-preview-toggle' ).on( 'click', function() {
  7408. api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  7409. });
  7410. /*
  7411. * Sticky header feature.
  7412. */
  7413. (function initStickyHeaders() {
  7414. var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
  7415. changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
  7416. activeHeader, lastScrollTop;
  7417. /**
  7418. * Determine which panel or section is currently expanded.
  7419. *
  7420. * @since 4.7.0
  7421. * @access private
  7422. *
  7423. * @param {wp.customize.Panel|wp.customize.Section} container Construct.
  7424. * @returns {void}
  7425. */
  7426. changeContainer = function( container ) {
  7427. var newInstance = container,
  7428. expandedSection = api.state( 'expandedSection' ).get(),
  7429. expandedPanel = api.state( 'expandedPanel' ).get(),
  7430. headerElement;
  7431. if ( activeHeader && activeHeader.element ) {
  7432. // Release previously active header element.
  7433. releaseStickyHeader( activeHeader.element );
  7434. // Remove event listener in the previous panel or section.
  7435. activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
  7436. }
  7437. if ( ! newInstance ) {
  7438. if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
  7439. newInstance = expandedPanel;
  7440. } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
  7441. newInstance = expandedSection;
  7442. } else {
  7443. activeHeader = false;
  7444. return;
  7445. }
  7446. }
  7447. headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
  7448. if ( headerElement.length ) {
  7449. activeHeader = {
  7450. instance: newInstance,
  7451. element: headerElement,
  7452. parent: headerElement.closest( '.customize-pane-child' ),
  7453. height: headerElement.outerHeight()
  7454. };
  7455. // Update header height whenever help text is expanded or collapsed.
  7456. activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
  7457. if ( expandedSection ) {
  7458. resetStickyHeader( activeHeader.element, activeHeader.parent );
  7459. }
  7460. } else {
  7461. activeHeader = false;
  7462. }
  7463. };
  7464. api.state( 'expandedSection' ).bind( changeContainer );
  7465. api.state( 'expandedPanel' ).bind( changeContainer );
  7466. // Throttled scroll event handler.
  7467. parentContainer.on( 'scroll', _.throttle( function() {
  7468. if ( ! activeHeader ) {
  7469. return;
  7470. }
  7471. var scrollTop = parentContainer.scrollTop(),
  7472. scrollDirection;
  7473. if ( ! lastScrollTop ) {
  7474. scrollDirection = 1;
  7475. } else {
  7476. if ( scrollTop === lastScrollTop ) {
  7477. scrollDirection = 0;
  7478. } else if ( scrollTop > lastScrollTop ) {
  7479. scrollDirection = 1;
  7480. } else {
  7481. scrollDirection = -1;
  7482. }
  7483. }
  7484. lastScrollTop = scrollTop;
  7485. if ( 0 !== scrollDirection ) {
  7486. positionStickyHeader( activeHeader, scrollTop, scrollDirection );
  7487. }
  7488. }, 8 ) );
  7489. // Update header position on sidebar layout change.
  7490. api.notifications.bind( 'sidebarTopUpdated', function() {
  7491. if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
  7492. activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
  7493. }
  7494. });
  7495. // Release header element if it is sticky.
  7496. releaseStickyHeader = function( headerElement ) {
  7497. if ( ! headerElement.hasClass( 'is-sticky' ) ) {
  7498. return;
  7499. }
  7500. headerElement
  7501. .removeClass( 'is-sticky' )
  7502. .addClass( 'maybe-sticky is-in-view' )
  7503. .css( 'top', parentContainer.scrollTop() + 'px' );
  7504. };
  7505. // Reset position of the sticky header.
  7506. resetStickyHeader = function( headerElement, headerParent ) {
  7507. if ( headerElement.hasClass( 'is-in-view' ) ) {
  7508. headerElement
  7509. .removeClass( 'maybe-sticky is-in-view' )
  7510. .css( {
  7511. width: '',
  7512. top: ''
  7513. } );
  7514. headerParent.css( 'padding-top', '' );
  7515. }
  7516. };
  7517. /**
  7518. * Update active header height.
  7519. *
  7520. * @since 4.7.0
  7521. * @access private
  7522. *
  7523. * @returns {void}
  7524. */
  7525. updateHeaderHeight = function() {
  7526. activeHeader.height = activeHeader.element.outerHeight();
  7527. };
  7528. /**
  7529. * Reposition header on throttled `scroll` event.
  7530. *
  7531. * @since 4.7.0
  7532. * @access private
  7533. *
  7534. * @param {object} header - Header.
  7535. * @param {number} scrollTop - Scroll top.
  7536. * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
  7537. * @returns {void}
  7538. */
  7539. positionStickyHeader = function( header, scrollTop, scrollDirection ) {
  7540. var headerElement = header.element,
  7541. headerParent = header.parent,
  7542. headerHeight = header.height,
  7543. headerTop = parseInt( headerElement.css( 'top' ), 10 ),
  7544. maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
  7545. isSticky = headerElement.hasClass( 'is-sticky' ),
  7546. isInView = headerElement.hasClass( 'is-in-view' ),
  7547. isScrollingUp = ( -1 === scrollDirection );
  7548. // When scrolling down, gradually hide sticky header.
  7549. if ( ! isScrollingUp ) {
  7550. if ( isSticky ) {
  7551. headerTop = scrollTop;
  7552. headerElement
  7553. .removeClass( 'is-sticky' )
  7554. .css( {
  7555. top: headerTop + 'px',
  7556. width: ''
  7557. } );
  7558. }
  7559. if ( isInView && scrollTop > headerTop + headerHeight ) {
  7560. headerElement.removeClass( 'is-in-view' );
  7561. headerParent.css( 'padding-top', '' );
  7562. }
  7563. return;
  7564. }
  7565. // Scrolling up.
  7566. if ( ! maybeSticky && scrollTop >= headerHeight ) {
  7567. maybeSticky = true;
  7568. headerElement.addClass( 'maybe-sticky' );
  7569. } else if ( 0 === scrollTop ) {
  7570. // Reset header in base position.
  7571. headerElement
  7572. .removeClass( 'maybe-sticky is-in-view is-sticky' )
  7573. .css( {
  7574. top: '',
  7575. width: ''
  7576. } );
  7577. headerParent.css( 'padding-top', '' );
  7578. return;
  7579. }
  7580. if ( isInView && ! isSticky ) {
  7581. // Header is in the view but is not yet sticky.
  7582. if ( headerTop >= scrollTop ) {
  7583. // Header is fully visible.
  7584. headerElement
  7585. .addClass( 'is-sticky' )
  7586. .css( {
  7587. top: parentContainer.css( 'top' ),
  7588. width: headerParent.outerWidth() + 'px'
  7589. } );
  7590. }
  7591. } else if ( maybeSticky && ! isInView ) {
  7592. // Header is out of the view.
  7593. headerElement
  7594. .addClass( 'is-in-view' )
  7595. .css( 'top', ( scrollTop - headerHeight ) + 'px' );
  7596. headerParent.css( 'padding-top', headerHeight + 'px' );
  7597. }
  7598. };
  7599. }());
  7600. // Previewed device bindings. (The api.previewedDevice property is how this Value was first introduced, but since it has moved to api.state.)
  7601. api.previewedDevice = api.state( 'previewedDevice' );
  7602. // Set the default device.
  7603. api.bind( 'ready', function() {
  7604. _.find( api.settings.previewableDevices, function( value, key ) {
  7605. if ( true === value['default'] ) {
  7606. api.previewedDevice.set( key );
  7607. return true;
  7608. }
  7609. } );
  7610. } );
  7611. // Set the toggled device.
  7612. footerActions.find( '.devices button' ).on( 'click', function( event ) {
  7613. api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
  7614. });
  7615. // Bind device changes.
  7616. api.previewedDevice.bind( function( newDevice ) {
  7617. var overlay = $( '.wp-full-overlay' ),
  7618. devices = '';
  7619. footerActions.find( '.devices button' )
  7620. .removeClass( 'active' )
  7621. .attr( 'aria-pressed', false );
  7622. footerActions.find( '.devices .preview-' + newDevice )
  7623. .addClass( 'active' )
  7624. .attr( 'aria-pressed', true );
  7625. $.each( api.settings.previewableDevices, function( device ) {
  7626. devices += ' preview-' + device;
  7627. } );
  7628. overlay
  7629. .removeClass( devices )
  7630. .addClass( 'preview-' + newDevice );
  7631. } );
  7632. // Bind site title display to the corresponding field.
  7633. if ( title.length ) {
  7634. api( 'blogname', function( setting ) {
  7635. var updateTitle = function() {
  7636. title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
  7637. };
  7638. setting.bind( updateTitle );
  7639. updateTitle();
  7640. } );
  7641. }
  7642. /*
  7643. * Create a postMessage connection with a parent frame,
  7644. * in case the Customizer frame was opened with the Customize loader.
  7645. *
  7646. * @see wp.customize.Loader
  7647. */
  7648. parent = new api.Messenger({
  7649. url: api.settings.url.parent,
  7650. channel: 'loader'
  7651. });
  7652. // Handle exiting of Customizer.
  7653. (function() {
  7654. var isInsideIframe = false;
  7655. function isCleanState() {
  7656. var defaultChangesetStatus;
  7657. /*
  7658. * Handle special case of previewing theme switch since some settings (for nav menus and widgets)
  7659. * are pre-dirty and non-active themes can only ever be auto-drafts.
  7660. */
  7661. if ( ! api.state( 'activated' ).get() ) {
  7662. return 0 === api._latestRevision;
  7663. }
  7664. // Dirty if the changeset status has been changed but not saved yet.
  7665. defaultChangesetStatus = api.state( 'changesetStatus' ).get();
  7666. if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
  7667. defaultChangesetStatus = 'publish';
  7668. }
  7669. if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
  7670. return false;
  7671. }
  7672. // Dirty if scheduled but the changeset date hasn't been saved yet.
  7673. if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
  7674. return false;
  7675. }
  7676. return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
  7677. }
  7678. /*
  7679. * If we receive a 'back' event, we're inside an iframe.
  7680. * Send any clicks to the 'Return' link to the parent page.
  7681. */
  7682. parent.bind( 'back', function() {
  7683. isInsideIframe = true;
  7684. });
  7685. function startPromptingBeforeUnload() {
  7686. api.unbind( 'change', startPromptingBeforeUnload );
  7687. api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
  7688. api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
  7689. // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
  7690. $( window ).on( 'beforeunload.customize-confirm', function() {
  7691. if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
  7692. setTimeout( function() {
  7693. overlay.removeClass( 'customize-loading' );
  7694. }, 1 );
  7695. return api.l10n.saveAlert;
  7696. }
  7697. });
  7698. }
  7699. api.bind( 'change', startPromptingBeforeUnload );
  7700. api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
  7701. api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
  7702. function requestClose() {
  7703. var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
  7704. if ( isCleanState() ) {
  7705. dismissLock = true;
  7706. } else if ( confirm( api.l10n.saveAlert ) ) {
  7707. dismissLock = true;
  7708. // Mark all settings as clean to prevent another call to requestChangesetUpdate.
  7709. api.each( function( setting ) {
  7710. setting._dirty = false;
  7711. });
  7712. $( document ).off( 'visibilitychange.wp-customize-changeset-update' );
  7713. $( window ).off( 'beforeunload.wp-customize-changeset-update' );
  7714. closeBtn.css( 'cursor', 'progress' );
  7715. if ( '' !== api.state( 'changesetStatus' ).get() ) {
  7716. dismissAutoSave = true;
  7717. }
  7718. } else {
  7719. clearedToClose.reject();
  7720. }
  7721. if ( dismissLock || dismissAutoSave ) {
  7722. wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
  7723. timeout: 500, // Don't wait too long.
  7724. data: {
  7725. wp_customize: 'on',
  7726. customize_theme: api.settings.theme.stylesheet,
  7727. customize_changeset_uuid: api.settings.changeset.uuid,
  7728. nonce: api.settings.nonce.dismiss_autosave_or_lock,
  7729. dismiss_autosave: dismissAutoSave,
  7730. dismiss_lock: dismissLock
  7731. }
  7732. } ).always( function() {
  7733. clearedToClose.resolve();
  7734. } );
  7735. }
  7736. return clearedToClose.promise();
  7737. }
  7738. parent.bind( 'confirm-close', function() {
  7739. requestClose().done( function() {
  7740. parent.send( 'confirmed-close', true );
  7741. } ).fail( function() {
  7742. parent.send( 'confirmed-close', false );
  7743. } );
  7744. } );
  7745. closeBtn.on( 'click.customize-controls-close', function( event ) {
  7746. event.preventDefault();
  7747. if ( isInsideIframe ) {
  7748. parent.send( 'close' ); // See confirm-close logic above.
  7749. } else {
  7750. requestClose().done( function() {
  7751. $( window ).off( 'beforeunload.customize-confirm' );
  7752. window.location.href = closeBtn.prop( 'href' );
  7753. } );
  7754. }
  7755. });
  7756. })();
  7757. // Pass events through to the parent.
  7758. $.each( [ 'saved', 'change' ], function ( i, event ) {
  7759. api.bind( event, function() {
  7760. parent.send( event );
  7761. });
  7762. } );
  7763. // Pass titles to the parent
  7764. api.bind( 'title', function( newTitle ) {
  7765. parent.send( 'title', newTitle );
  7766. });
  7767. if ( api.settings.changeset.branching ) {
  7768. parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  7769. }
  7770. // Initialize the connection with the parent frame.
  7771. parent.send( 'ready' );
  7772. // Control visibility for default controls
  7773. $.each({
  7774. 'background_image': {
  7775. controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
  7776. callback: function( to ) { return !! to; }
  7777. },
  7778. 'show_on_front': {
  7779. controls: [ 'page_on_front', 'page_for_posts' ],
  7780. callback: function( to ) { return 'page' === to; }
  7781. },
  7782. 'header_textcolor': {
  7783. controls: [ 'header_textcolor' ],
  7784. callback: function( to ) { return 'blank' !== to; }
  7785. }
  7786. }, function( settingId, o ) {
  7787. api( settingId, function( setting ) {
  7788. $.each( o.controls, function( i, controlId ) {
  7789. api.control( controlId, function( control ) {
  7790. var visibility = function( to ) {
  7791. control.container.toggle( o.callback( to ) );
  7792. };
  7793. visibility( setting.get() );
  7794. setting.bind( visibility );
  7795. });
  7796. });
  7797. });
  7798. });
  7799. api.control( 'background_preset', function( control ) {
  7800. var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
  7801. visibility = { // position, size, repeat, attachment
  7802. 'default': [ false, false, false, false ],
  7803. 'fill': [ true, false, false, false ],
  7804. 'fit': [ true, false, true, false ],
  7805. 'repeat': [ true, false, false, true ],
  7806. 'custom': [ true, true, true, true ]
  7807. };
  7808. defaultValues = [
  7809. _wpCustomizeBackground.defaults['default-position-x'],
  7810. _wpCustomizeBackground.defaults['default-position-y'],
  7811. _wpCustomizeBackground.defaults['default-size'],
  7812. _wpCustomizeBackground.defaults['default-repeat'],
  7813. _wpCustomizeBackground.defaults['default-attachment']
  7814. ];
  7815. values = { // position_x, position_y, size, repeat, attachment
  7816. 'default': defaultValues,
  7817. 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
  7818. 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
  7819. 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
  7820. };
  7821. // @todo These should actually toggle the active state, but without the preview overriding the state in data.activeControls.
  7822. toggleVisibility = function( preset ) {
  7823. _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
  7824. var control = api.control( controlId );
  7825. if ( control ) {
  7826. control.container.toggle( visibility[ preset ][ i ] );
  7827. }
  7828. } );
  7829. };
  7830. updateSettings = function( preset ) {
  7831. _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
  7832. var setting = api( settingId );
  7833. if ( setting ) {
  7834. setting.set( values[ preset ][ i ] );
  7835. }
  7836. } );
  7837. };
  7838. preset = control.setting.get();
  7839. toggleVisibility( preset );
  7840. control.setting.bind( 'change', function( preset ) {
  7841. toggleVisibility( preset );
  7842. if ( 'custom' !== preset ) {
  7843. updateSettings( preset );
  7844. }
  7845. } );
  7846. } );
  7847. api.control( 'background_repeat', function( control ) {
  7848. control.elements[0].unsync( api( 'background_repeat' ) );
  7849. control.element = new api.Element( control.container.find( 'input' ) );
  7850. control.element.set( 'no-repeat' !== control.setting() );
  7851. control.element.bind( function( to ) {
  7852. control.setting.set( to ? 'repeat' : 'no-repeat' );
  7853. } );
  7854. control.setting.bind( function( to ) {
  7855. control.element.set( 'no-repeat' !== to );
  7856. } );
  7857. } );
  7858. api.control( 'background_attachment', function( control ) {
  7859. control.elements[0].unsync( api( 'background_attachment' ) );
  7860. control.element = new api.Element( control.container.find( 'input' ) );
  7861. control.element.set( 'fixed' !== control.setting() );
  7862. control.element.bind( function( to ) {
  7863. control.setting.set( to ? 'scroll' : 'fixed' );
  7864. } );
  7865. control.setting.bind( function( to ) {
  7866. control.element.set( 'fixed' !== to );
  7867. } );
  7868. } );
  7869. // Juggle the two controls that use header_textcolor
  7870. api.control( 'display_header_text', function( control ) {
  7871. var last = '';
  7872. control.elements[0].unsync( api( 'header_textcolor' ) );
  7873. control.element = new api.Element( control.container.find('input') );
  7874. control.element.set( 'blank' !== control.setting() );
  7875. control.element.bind( function( to ) {
  7876. if ( ! to ) {
  7877. last = api( 'header_textcolor' ).get();
  7878. }
  7879. control.setting.set( to ? last : 'blank' );
  7880. });
  7881. control.setting.bind( function( to ) {
  7882. control.element.set( 'blank' !== to );
  7883. });
  7884. });
  7885. // Add behaviors to the static front page controls.
  7886. api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) {
  7887. var handleChange = function() {
  7888. var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision';
  7889. pageOnFrontId = parseInt( pageOnFront(), 10 );
  7890. pageForPostsId = parseInt( pageForPosts(), 10 );
  7891. if ( 'page' === showOnFront() ) {
  7892. // Change previewed URL to the homepage when changing the page_on_front.
  7893. if ( setting === pageOnFront && pageOnFrontId > 0 ) {
  7894. api.previewer.previewUrl.set( api.settings.url.home );
  7895. }
  7896. // Change the previewed URL to the selected page when changing the page_for_posts.
  7897. if ( setting === pageForPosts && pageForPostsId > 0 ) {
  7898. api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId );
  7899. }
  7900. }
  7901. // Toggle notification when the homepage and posts page are both set and the same.
  7902. if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) {
  7903. showOnFront.notifications.add( new api.Notification( errorCode, {
  7904. type: 'error',
  7905. message: api.l10n.pageOnFrontError
  7906. } ) );
  7907. } else {
  7908. showOnFront.notifications.remove( errorCode );
  7909. }
  7910. };
  7911. showOnFront.bind( handleChange );
  7912. pageOnFront.bind( handleChange );
  7913. pageForPosts.bind( handleChange );
  7914. handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset.
  7915. // Move notifications container to the bottom.
  7916. api.control( 'show_on_front', function( showOnFrontControl ) {
  7917. showOnFrontControl.deferred.embedded.done( function() {
  7918. showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() );
  7919. });
  7920. });
  7921. });
  7922. // Add code editor for Custom CSS.
  7923. (function() {
  7924. var sectionReady = $.Deferred();
  7925. api.section( 'custom_css', function( section ) {
  7926. section.deferred.embedded.done( function() {
  7927. if ( section.expanded() ) {
  7928. sectionReady.resolve( section );
  7929. } else {
  7930. section.expanded.bind( function( isExpanded ) {
  7931. if ( isExpanded ) {
  7932. sectionReady.resolve( section );
  7933. }
  7934. } );
  7935. }
  7936. });
  7937. });
  7938. // Set up the section description behaviors.
  7939. sectionReady.done( function setupSectionDescription( section ) {
  7940. var control = api.control( 'custom_css' );
  7941. // Hide redundant label for visual users.
  7942. control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' );
  7943. // Close the section description when clicking the close button.
  7944. section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
  7945. section.container.find( '.section-meta .customize-section-description:first' )
  7946. .removeClass( 'open' )
  7947. .slideUp();
  7948. section.container.find( '.customize-help-toggle' )
  7949. .attr( 'aria-expanded', 'false' )
  7950. .focus(); // Avoid focus loss.
  7951. });
  7952. // Reveal help text if setting is empty.
  7953. if ( control && ! control.setting.get() ) {
  7954. section.container.find( '.section-meta .customize-section-description:first' )
  7955. .addClass( 'open' )
  7956. .show()
  7957. .trigger( 'toggled' );
  7958. section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' );
  7959. }
  7960. });
  7961. })();
  7962. // Toggle visibility of Header Video notice when active state change.
  7963. api.control( 'header_video', function( headerVideoControl ) {
  7964. headerVideoControl.deferred.embedded.done( function() {
  7965. var toggleNotice = function() {
  7966. var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
  7967. if ( ! section ) {
  7968. return;
  7969. }
  7970. if ( headerVideoControl.active.get() ) {
  7971. section.notifications.remove( noticeCode );
  7972. } else {
  7973. section.notifications.add( new api.Notification( noticeCode, {
  7974. type: 'info',
  7975. message: api.l10n.videoHeaderNotice
  7976. } ) );
  7977. }
  7978. };
  7979. toggleNotice();
  7980. headerVideoControl.active.bind( toggleNotice );
  7981. } );
  7982. } );
  7983. // Update the setting validities.
  7984. api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
  7985. api._handleSettingValidities( {
  7986. settingValidities: settingValidities,
  7987. focusInvalidControl: false
  7988. } );
  7989. } );
  7990. // Focus on the control that is associated with the given setting.
  7991. api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
  7992. var matchedControls = [];
  7993. api.control.each( function( control ) {
  7994. var settingIds = _.pluck( control.settings, 'id' );
  7995. if ( -1 !== _.indexOf( settingIds, settingId ) ) {
  7996. matchedControls.push( control );
  7997. }
  7998. } );
  7999. // Focus on the matched control with the lowest priority (appearing higher).
  8000. if ( matchedControls.length ) {
  8001. matchedControls.sort( function( a, b ) {
  8002. return a.priority() - b.priority();
  8003. } );
  8004. matchedControls[0].focus();
  8005. }
  8006. } );
  8007. // Refresh the preview when it requests.
  8008. api.previewer.bind( 'refresh', function() {
  8009. api.previewer.refresh();
  8010. });
  8011. // Update the edit shortcut visibility state.
  8012. api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
  8013. var isMobileScreen;
  8014. if ( window.matchMedia ) {
  8015. isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
  8016. } else {
  8017. isMobileScreen = $( window ).width() <= 640;
  8018. }
  8019. api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
  8020. } );
  8021. if ( window.matchMedia ) {
  8022. window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
  8023. var state = api.state( 'paneVisible' );
  8024. state.callbacks.fireWith( state, [ state.get(), state.get() ] );
  8025. } );
  8026. }
  8027. api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
  8028. api.state( 'editShortcutVisibility' ).set( visibility );
  8029. } );
  8030. api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
  8031. api.previewer.send( 'edit-shortcut-visibility', visibility );
  8032. } );
  8033. // Autosave changeset.
  8034. function startAutosaving() {
  8035. var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
  8036. api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
  8037. function onChangeSaved( isSaved ) {
  8038. if ( ! isSaved && ! api.settings.changeset.autosaved ) {
  8039. api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in.
  8040. api.previewer.send( 'autosaving' );
  8041. }
  8042. }
  8043. api.state( 'saved' ).bind( onChangeSaved );
  8044. onChangeSaved( api.state( 'saved' ).get() );
  8045. /**
  8046. * Request changeset update and then re-schedule the next changeset update time.
  8047. *
  8048. * @since 4.7.0
  8049. * @private
  8050. */
  8051. updateChangesetWithReschedule = function() {
  8052. if ( ! updatePending ) {
  8053. updatePending = true;
  8054. api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
  8055. updatePending = false;
  8056. } );
  8057. }
  8058. scheduleChangesetUpdate();
  8059. };
  8060. /**
  8061. * Schedule changeset update.
  8062. *
  8063. * @since 4.7.0
  8064. * @private
  8065. */
  8066. scheduleChangesetUpdate = function() {
  8067. clearTimeout( timeoutId );
  8068. timeoutId = setTimeout( function() {
  8069. updateChangesetWithReschedule();
  8070. }, api.settings.timeouts.changesetAutoSave );
  8071. };
  8072. // Start auto-save interval for updating changeset.
  8073. scheduleChangesetUpdate();
  8074. // Save changeset when focus removed from window.
  8075. $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
  8076. if ( document.hidden ) {
  8077. updateChangesetWithReschedule();
  8078. }
  8079. } );
  8080. // Save changeset before unloading window.
  8081. $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
  8082. updateChangesetWithReschedule();
  8083. } );
  8084. }
  8085. api.bind( 'change', startAutosaving );
  8086. // Make sure TinyMCE dialogs appear above Customizer UI.
  8087. $( document ).one( 'tinymce-editor-setup', function() {
  8088. if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) {
  8089. window.tinymce.ui.FloatPanel.zIndex = 500001;
  8090. }
  8091. } );
  8092. body.addClass( 'ready' );
  8093. api.trigger( 'ready' );
  8094. });
  8095. })( wp, jQuery );