customize-controls.js 282 KB

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