Няма описание

customize-controls.js 285KB

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