customize-controls.js 162 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521
  1. /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
  2. (function( exports, $ ){
  3. var Container, focus, normalizedTransitionendEventName, api = wp.customize;
  4. /**
  5. * A Customizer Setting.
  6. *
  7. * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
  8. * draft changes to in the Customizer.
  9. *
  10. * @see PHP class WP_Customize_Setting.
  11. *
  12. * @class
  13. * @augments wp.customize.Value
  14. * @augments wp.customize.Class
  15. *
  16. * @param {object} id The Setting ID.
  17. * @param {object} value The initial value of the setting.
  18. * @param {object} options.previewer The Previewer instance to sync with.
  19. * @param {object} options.transport The transport to use for previewing. Supports 'refresh' and 'postMessage'.
  20. * @param {object} options.dirty
  21. */
  22. api.Setting = api.Value.extend({
  23. initialize: function( id, value, options ) {
  24. var setting = this;
  25. api.Value.prototype.initialize.call( setting, value, options );
  26. setting.id = id;
  27. setting.transport = setting.transport || 'refresh';
  28. setting._dirty = options.dirty || false;
  29. setting.notifications = new api.Values({ defaultConstructor: api.Notification });
  30. // Whenever the setting's value changes, refresh the preview.
  31. setting.bind( setting.preview );
  32. },
  33. /**
  34. * Refresh the preview, respective of the setting's refresh policy.
  35. *
  36. * If the preview hasn't sent a keep-alive message and is likely
  37. * disconnected by having navigated to a non-allowed URL, then the
  38. * refresh transport will be forced when postMessage is the transport.
  39. * Note that postMessage does not throw an error when the recipient window
  40. * fails to match the origin window, so using try/catch around the
  41. * previewer.send() call to then fallback to refresh will not work.
  42. *
  43. * @since 3.4.0
  44. * @access public
  45. *
  46. * @returns {void}
  47. */
  48. preview: function() {
  49. var setting = this, transport;
  50. transport = setting.transport;
  51. if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
  52. transport = 'refresh';
  53. }
  54. if ( 'postMessage' === transport ) {
  55. setting.previewer.send( 'setting', [ setting.id, setting() ] );
  56. } else if ( 'refresh' === transport ) {
  57. setting.previewer.refresh();
  58. }
  59. },
  60. /**
  61. * Find controls associated with this setting.
  62. *
  63. * @since 4.6.0
  64. * @returns {wp.customize.Control[]} Controls associated with setting.
  65. */
  66. findControls: function() {
  67. var setting = this, controls = [];
  68. api.control.each( function( control ) {
  69. _.each( control.settings, function( controlSetting ) {
  70. if ( controlSetting.id === setting.id ) {
  71. controls.push( control );
  72. }
  73. } );
  74. } );
  75. return controls;
  76. }
  77. });
  78. /**
  79. * Current change count.
  80. *
  81. * @since 4.7.0
  82. * @type {number}
  83. * @protected
  84. */
  85. api._latestRevision = 0;
  86. /**
  87. * Last revision that was saved.
  88. *
  89. * @since 4.7.0
  90. * @type {number}
  91. * @protected
  92. */
  93. api._lastSavedRevision = 0;
  94. /**
  95. * Latest revisions associated with the updated setting.
  96. *
  97. * @since 4.7.0
  98. * @type {object}
  99. * @protected
  100. */
  101. api._latestSettingRevisions = {};
  102. /*
  103. * Keep track of the revision associated with each updated setting so that
  104. * requestChangesetUpdate knows which dirty settings to include. Also, once
  105. * ready is triggered and all initial settings have been added, increment
  106. * revision for each newly-created initially-dirty setting so that it will
  107. * also be included in changeset update requests.
  108. */
  109. api.bind( 'change', function incrementChangedSettingRevision( setting ) {
  110. api._latestRevision += 1;
  111. api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  112. } );
  113. api.bind( 'ready', function() {
  114. api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
  115. if ( setting._dirty ) {
  116. api._latestRevision += 1;
  117. api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  118. }
  119. } );
  120. } );
  121. /**
  122. * Get the dirty setting values.
  123. *
  124. * @since 4.7.0
  125. * @access public
  126. *
  127. * @param {object} [options] Options.
  128. * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
  129. * @returns {object} Dirty setting values.
  130. */
  131. api.dirtyValues = function dirtyValues( options ) {
  132. var values = {};
  133. api.each( function( setting ) {
  134. var settingRevision;
  135. if ( ! setting._dirty ) {
  136. return;
  137. }
  138. settingRevision = api._latestSettingRevisions[ setting.id ];
  139. // Skip including settings that have already been included in the changeset, if only requesting unsaved.
  140. if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
  141. return;
  142. }
  143. values[ setting.id ] = setting.get();
  144. } );
  145. return values;
  146. };
  147. /**
  148. * Request updates to the changeset.
  149. *
  150. * @since 4.7.0
  151. * @access public
  152. *
  153. * @param {object} [changes] Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
  154. * If not provided, then the changes will still be obtained from unsaved dirty settings.
  155. * @returns {jQuery.Promise} Promise resolving with the response data.
  156. */
  157. api.requestChangesetUpdate = function requestChangesetUpdate( changes ) {
  158. var deferred, request, submittedChanges = {}, data;
  159. deferred = new $.Deferred();
  160. if ( changes ) {
  161. _.extend( submittedChanges, changes );
  162. }
  163. // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
  164. _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
  165. if ( ! changes || null !== changes[ settingId ] ) {
  166. submittedChanges[ settingId ] = _.extend(
  167. {},
  168. submittedChanges[ settingId ] || {},
  169. { value: dirtyValue }
  170. );
  171. }
  172. } );
  173. // Short-circuit when there are no pending changes.
  174. if ( _.isEmpty( submittedChanges ) ) {
  175. deferred.resolve( {} );
  176. return deferred.promise();
  177. }
  178. // Make sure that publishing a changeset waits for all changeset update requests to complete.
  179. api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  180. deferred.always( function() {
  181. api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  182. } );
  183. // Allow plugins to attach additional params to the settings.
  184. api.trigger( 'changeset-save', submittedChanges );
  185. // Ensure that if any plugins add data to save requests by extending query() that they get included here.
  186. data = api.previewer.query( { excludeCustomizedSaved: true } );
  187. delete data.customized; // Being sent in customize_changeset_data instead.
  188. _.extend( data, {
  189. nonce: api.settings.nonce.save,
  190. customize_theme: api.settings.theme.stylesheet,
  191. customize_changeset_data: JSON.stringify( submittedChanges )
  192. } );
  193. request = wp.ajax.post( 'customize_save', data );
  194. request.done( function requestChangesetUpdateDone( data ) {
  195. var savedChangesetValues = {};
  196. // Ensure that all settings updated subsequently will be included in the next changeset update request.
  197. api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
  198. api.state( 'changesetStatus' ).set( data.changeset_status );
  199. deferred.resolve( data );
  200. api.trigger( 'changeset-saved', data );
  201. if ( data.setting_validities ) {
  202. _.each( data.setting_validities, function( validity, settingId ) {
  203. if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
  204. savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
  205. }
  206. } );
  207. }
  208. api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
  209. } );
  210. request.fail( function requestChangesetUpdateFail( data ) {
  211. deferred.reject( data );
  212. api.trigger( 'changeset-error', data );
  213. } );
  214. request.always( function( data ) {
  215. if ( data.setting_validities ) {
  216. api._handleSettingValidities( {
  217. settingValidities: data.setting_validities
  218. } );
  219. }
  220. } );
  221. return deferred.promise();
  222. };
  223. /**
  224. * Watch all changes to Value properties, and bubble changes to parent Values instance
  225. *
  226. * @since 4.1.0
  227. *
  228. * @param {wp.customize.Class} instance
  229. * @param {Array} properties The names of the Value instances to watch.
  230. */
  231. api.utils.bubbleChildValueChanges = function ( instance, properties ) {
  232. $.each( properties, function ( i, key ) {
  233. instance[ key ].bind( function ( to, from ) {
  234. if ( instance.parent && to !== from ) {
  235. instance.parent.trigger( 'change', instance );
  236. }
  237. } );
  238. } );
  239. };
  240. /**
  241. * Expand a panel, section, or control and focus on the first focusable element.
  242. *
  243. * @since 4.1.0
  244. *
  245. * @param {Object} [params]
  246. * @param {Function} [params.completeCallback]
  247. */
  248. focus = function ( params ) {
  249. var construct, completeCallback, focus, focusElement;
  250. construct = this;
  251. params = params || {};
  252. focus = function () {
  253. var focusContainer;
  254. if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
  255. focusContainer = construct.contentContainer;
  256. } else {
  257. focusContainer = construct.container;
  258. }
  259. focusElement = focusContainer.find( '.control-focus:first' );
  260. if ( 0 === focusElement.length ) {
  261. // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
  262. focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
  263. }
  264. focusElement.focus();
  265. };
  266. if ( params.completeCallback ) {
  267. completeCallback = params.completeCallback;
  268. params.completeCallback = function () {
  269. focus();
  270. completeCallback();
  271. };
  272. } else {
  273. params.completeCallback = focus;
  274. }
  275. api.state( 'paneVisible' ).set( true );
  276. if ( construct.expand ) {
  277. construct.expand( params );
  278. } else {
  279. params.completeCallback();
  280. }
  281. };
  282. /**
  283. * Stable sort for Panels, Sections, and Controls.
  284. *
  285. * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
  286. *
  287. * @since 4.1.0
  288. *
  289. * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
  290. * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
  291. * @returns {Number}
  292. */
  293. api.utils.prioritySort = function ( a, b ) {
  294. if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
  295. return a.params.instanceNumber - b.params.instanceNumber;
  296. } else {
  297. return a.priority() - b.priority();
  298. }
  299. };
  300. /**
  301. * Return whether the supplied Event object is for a keydown event but not the Enter key.
  302. *
  303. * @since 4.1.0
  304. *
  305. * @param {jQuery.Event} event
  306. * @returns {boolean}
  307. */
  308. api.utils.isKeydownButNotEnterEvent = function ( event ) {
  309. return ( 'keydown' === event.type && 13 !== event.which );
  310. };
  311. /**
  312. * Return whether the two lists of elements are the same and are in the same order.
  313. *
  314. * @since 4.1.0
  315. *
  316. * @param {Array|jQuery} listA
  317. * @param {Array|jQuery} listB
  318. * @returns {boolean}
  319. */
  320. api.utils.areElementListsEqual = function ( listA, listB ) {
  321. var equal = (
  322. listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
  323. -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
  324. _.zip( listA, listB ), // pair up each element between the two lists
  325. function ( pair ) {
  326. return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
  327. }
  328. ), false ) // check for presence of false in map's return value
  329. );
  330. return equal;
  331. };
  332. /**
  333. * Return browser supported `transitionend` event name.
  334. *
  335. * @since 4.7.0
  336. *
  337. * @returns {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
  338. */
  339. normalizedTransitionendEventName = (function () {
  340. var el, transitions, prop;
  341. el = document.createElement( 'div' );
  342. transitions = {
  343. 'transition' : 'transitionend',
  344. 'OTransition' : 'oTransitionEnd',
  345. 'MozTransition' : 'transitionend',
  346. 'WebkitTransition': 'webkitTransitionEnd'
  347. };
  348. prop = _.find( _.keys( transitions ), function( prop ) {
  349. return ! _.isUndefined( el.style[ prop ] );
  350. } );
  351. if ( prop ) {
  352. return transitions[ prop ];
  353. } else {
  354. return null;
  355. }
  356. })();
  357. /**
  358. * Base class for Panel and Section.
  359. *
  360. * @since 4.1.0
  361. *
  362. * @class
  363. * @augments wp.customize.Class
  364. */
  365. Container = api.Class.extend({
  366. defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  367. defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
  368. containerType: 'container',
  369. defaults: {
  370. title: '',
  371. description: '',
  372. priority: 100,
  373. type: 'default',
  374. content: null,
  375. active: true,
  376. instanceNumber: null
  377. },
  378. /**
  379. * @since 4.1.0
  380. *
  381. * @param {string} id - The ID for the container.
  382. * @param {object} options - Object containing one property: params.
  383. * @param {object} options.params - Object containing the following properties.
  384. * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
  385. * @param {string=} [options.params.description] - Description shown at the top of the panel.
  386. * @param {number=100} [options.params.priority] - The sort priority for the panel.
  387. * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
  388. * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
  389. * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
  390. */
  391. initialize: function ( id, options ) {
  392. var container = this;
  393. container.id = id;
  394. options = options || {};
  395. options.params = _.defaults(
  396. options.params || {},
  397. container.defaults
  398. );
  399. $.extend( container, options );
  400. container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
  401. container.container = $( container.params.content );
  402. if ( 0 === container.container.length ) {
  403. container.container = $( container.getContainer() );
  404. }
  405. container.headContainer = container.container;
  406. container.contentContainer = container.getContent();
  407. container.container = container.container.add( container.contentContainer );
  408. container.deferred = {
  409. embedded: new $.Deferred()
  410. };
  411. container.priority = new api.Value();
  412. container.active = new api.Value();
  413. container.activeArgumentsQueue = [];
  414. container.expanded = new api.Value();
  415. container.expandedArgumentsQueue = [];
  416. container.active.bind( function ( active ) {
  417. var args = container.activeArgumentsQueue.shift();
  418. args = $.extend( {}, container.defaultActiveArguments, args );
  419. active = ( active && container.isContextuallyActive() );
  420. container.onChangeActive( active, args );
  421. });
  422. container.expanded.bind( function ( expanded ) {
  423. var args = container.expandedArgumentsQueue.shift();
  424. args = $.extend( {}, container.defaultExpandedArguments, args );
  425. container.onChangeExpanded( expanded, args );
  426. });
  427. container.deferred.embedded.done( function () {
  428. container.attachEvents();
  429. });
  430. api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
  431. container.priority.set( container.params.priority );
  432. container.active.set( container.params.active );
  433. container.expanded.set( false );
  434. },
  435. /**
  436. * @since 4.1.0
  437. *
  438. * @abstract
  439. */
  440. ready: function() {},
  441. /**
  442. * Get the child models associated with this parent, sorting them by their priority Value.
  443. *
  444. * @since 4.1.0
  445. *
  446. * @param {String} parentType
  447. * @param {String} childType
  448. * @returns {Array}
  449. */
  450. _children: function ( parentType, childType ) {
  451. var parent = this,
  452. children = [];
  453. api[ childType ].each( function ( child ) {
  454. if ( child[ parentType ].get() === parent.id ) {
  455. children.push( child );
  456. }
  457. } );
  458. children.sort( api.utils.prioritySort );
  459. return children;
  460. },
  461. /**
  462. * To override by subclass, to return whether the container has active children.
  463. *
  464. * @since 4.1.0
  465. *
  466. * @abstract
  467. */
  468. isContextuallyActive: function () {
  469. throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
  470. },
  471. /**
  472. * Active state change handler.
  473. *
  474. * Shows the container if it is active, hides it if not.
  475. *
  476. * To override by subclass, update the container's UI to reflect the provided active state.
  477. *
  478. * @since 4.1.0
  479. *
  480. * @param {boolean} active - The active state to transiution to.
  481. * @param {Object} [args] - Args.
  482. * @param {Object} [args.duration] - The duration for the slideUp/slideDown animation.
  483. * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
  484. * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
  485. */
  486. onChangeActive: function( active, args ) {
  487. var construct = this,
  488. headContainer = construct.headContainer,
  489. duration, expandedOtherPanel;
  490. if ( args.unchanged ) {
  491. if ( args.completeCallback ) {
  492. args.completeCallback();
  493. }
  494. return;
  495. }
  496. duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
  497. if ( construct.extended( api.Panel ) ) {
  498. // If this is a panel is not currently expanded but another panel is expanded, do not animate.
  499. api.panel.each(function ( panel ) {
  500. if ( panel !== construct && panel.expanded() ) {
  501. expandedOtherPanel = panel;
  502. duration = 0;
  503. }
  504. });
  505. // Collapse any expanded sections inside of this panel first before deactivating.
  506. if ( ! active ) {
  507. _.each( construct.sections(), function( section ) {
  508. section.collapse( { duration: 0 } );
  509. } );
  510. }
  511. }
  512. if ( ! $.contains( document, headContainer.get( 0 ) ) ) {
  513. // If the element is not in the DOM, then jQuery.fn.slideUp() does nothing. In this case, a hard toggle is required instead.
  514. headContainer.toggle( active );
  515. if ( args.completeCallback ) {
  516. args.completeCallback();
  517. }
  518. } else if ( active ) {
  519. headContainer.slideDown( duration, args.completeCallback );
  520. } else {
  521. if ( construct.expanded() ) {
  522. construct.collapse({
  523. duration: duration,
  524. completeCallback: function() {
  525. headContainer.slideUp( duration, args.completeCallback );
  526. }
  527. });
  528. } else {
  529. headContainer.slideUp( duration, args.completeCallback );
  530. }
  531. }
  532. },
  533. /**
  534. * @since 4.1.0
  535. *
  536. * @params {Boolean} active
  537. * @param {Object} [params]
  538. * @returns {Boolean} false if state already applied
  539. */
  540. _toggleActive: function ( active, params ) {
  541. var self = this;
  542. params = params || {};
  543. if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
  544. params.unchanged = true;
  545. self.onChangeActive( self.active.get(), params );
  546. return false;
  547. } else {
  548. params.unchanged = false;
  549. this.activeArgumentsQueue.push( params );
  550. this.active.set( active );
  551. return true;
  552. }
  553. },
  554. /**
  555. * @param {Object} [params]
  556. * @returns {Boolean} false if already active
  557. */
  558. activate: function ( params ) {
  559. return this._toggleActive( true, params );
  560. },
  561. /**
  562. * @param {Object} [params]
  563. * @returns {Boolean} false if already inactive
  564. */
  565. deactivate: function ( params ) {
  566. return this._toggleActive( false, params );
  567. },
  568. /**
  569. * To override by subclass, update the container's UI to reflect the provided active state.
  570. * @abstract
  571. */
  572. onChangeExpanded: function () {
  573. throw new Error( 'Must override with subclass.' );
  574. },
  575. /**
  576. * Handle the toggle logic for expand/collapse.
  577. *
  578. * @param {Boolean} expanded - The new state to apply.
  579. * @param {Object} [params] - Object containing options for expand/collapse.
  580. * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
  581. * @returns {Boolean} false if state already applied or active state is false
  582. */
  583. _toggleExpanded: function( expanded, params ) {
  584. var instance = this, previousCompleteCallback;
  585. params = params || {};
  586. previousCompleteCallback = params.completeCallback;
  587. // Short-circuit expand() if the instance is not active.
  588. if ( expanded && ! instance.active() ) {
  589. return false;
  590. }
  591. api.state( 'paneVisible' ).set( true );
  592. params.completeCallback = function() {
  593. if ( previousCompleteCallback ) {
  594. previousCompleteCallback.apply( instance, arguments );
  595. }
  596. if ( expanded ) {
  597. instance.container.trigger( 'expanded' );
  598. } else {
  599. instance.container.trigger( 'collapsed' );
  600. }
  601. };
  602. if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
  603. params.unchanged = true;
  604. instance.onChangeExpanded( instance.expanded.get(), params );
  605. return false;
  606. } else {
  607. params.unchanged = false;
  608. instance.expandedArgumentsQueue.push( params );
  609. instance.expanded.set( expanded );
  610. return true;
  611. }
  612. },
  613. /**
  614. * @param {Object} [params]
  615. * @returns {Boolean} false if already expanded or if inactive.
  616. */
  617. expand: function ( params ) {
  618. return this._toggleExpanded( true, params );
  619. },
  620. /**
  621. * @param {Object} [params]
  622. * @returns {Boolean} false if already collapsed.
  623. */
  624. collapse: function ( params ) {
  625. return this._toggleExpanded( false, params );
  626. },
  627. /**
  628. * Animate container state change if transitions are supported by the browser.
  629. *
  630. * @since 4.7.0
  631. * @private
  632. *
  633. * @param {function} completeCallback Function to be called after transition is completed.
  634. * @returns {void}
  635. */
  636. _animateChangeExpanded: function( completeCallback ) {
  637. // Return if CSS transitions are not supported.
  638. if ( ! normalizedTransitionendEventName ) {
  639. if ( completeCallback ) {
  640. completeCallback();
  641. }
  642. return;
  643. }
  644. var construct = this,
  645. content = construct.contentContainer,
  646. overlay = content.closest( '.wp-full-overlay' ),
  647. elements, transitionEndCallback, transitionParentPane;
  648. // Determine set of elements that are affected by the animation.
  649. elements = overlay.add( content );
  650. if ( ! construct.panel || '' === construct.panel() ) {
  651. transitionParentPane = true;
  652. } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) {
  653. transitionParentPane = true;
  654. } else {
  655. transitionParentPane = false;
  656. }
  657. if ( transitionParentPane ) {
  658. elements = elements.add( '#customize-info, .customize-pane-parent' );
  659. }
  660. // Handle `transitionEnd` event.
  661. transitionEndCallback = function( e ) {
  662. if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
  663. return;
  664. }
  665. content.off( normalizedTransitionendEventName, transitionEndCallback );
  666. elements.removeClass( 'busy' );
  667. if ( completeCallback ) {
  668. completeCallback();
  669. }
  670. };
  671. content.on( normalizedTransitionendEventName, transitionEndCallback );
  672. elements.addClass( 'busy' );
  673. // Prevent screen flicker when pane has been scrolled before expanding.
  674. _.defer( function() {
  675. var container = content.closest( '.wp-full-overlay-sidebar-content' ),
  676. currentScrollTop = container.scrollTop(),
  677. previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
  678. expanded = construct.expanded();
  679. if ( expanded && 0 < currentScrollTop ) {
  680. content.css( 'top', currentScrollTop + 'px' );
  681. content.data( 'previous-scrollTop', currentScrollTop );
  682. } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
  683. content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
  684. container.scrollTop( previousScrollTop );
  685. }
  686. } );
  687. },
  688. /**
  689. * Bring the container into view and then expand this and bring it into view
  690. * @param {Object} [params]
  691. */
  692. focus: focus,
  693. /**
  694. * Return the container html, generated from its JS template, if it exists.
  695. *
  696. * @since 4.3.0
  697. */
  698. getContainer: function () {
  699. var template,
  700. container = this;
  701. if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
  702. template = wp.template( container.templateSelector );
  703. } else {
  704. template = wp.template( 'customize-' + container.containerType + '-default' );
  705. }
  706. if ( template && container.container ) {
  707. return $.trim( template( container.params ) );
  708. }
  709. return '<li></li>';
  710. },
  711. /**
  712. * Find content element which is displayed when the section is expanded.
  713. *
  714. * After a construct is initialized, the return value will be available via the `contentContainer` property.
  715. * By default the element will be related it to the parent container with `aria-owns` and detached.
  716. * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
  717. * just return the content element without needing to add the `aria-owns` element or detach it from
  718. * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
  719. * method to handle animating the panel/section into and out of view.
  720. *
  721. * @since 4.7.0
  722. * @access public
  723. *
  724. * @returns {jQuery} Detached content element.
  725. */
  726. getContent: function() {
  727. var construct = this,
  728. container = construct.container,
  729. content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
  730. contentId = 'sub-' + container.attr( 'id' ),
  731. ownedElements = contentId,
  732. alreadyOwnedElements = container.attr( 'aria-owns' );
  733. if ( alreadyOwnedElements ) {
  734. ownedElements = ownedElements + ' ' + alreadyOwnedElements;
  735. }
  736. container.attr( 'aria-owns', ownedElements );
  737. return content.detach().attr( {
  738. 'id': contentId,
  739. 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
  740. } );
  741. }
  742. });
  743. /**
  744. * @since 4.1.0
  745. *
  746. * @class
  747. * @augments wp.customize.Class
  748. */
  749. api.Section = Container.extend({
  750. containerType: 'section',
  751. defaults: {
  752. title: '',
  753. description: '',
  754. priority: 100,
  755. type: 'default',
  756. content: null,
  757. active: true,
  758. instanceNumber: null,
  759. panel: null,
  760. customizeAction: ''
  761. },
  762. /**
  763. * @since 4.1.0
  764. *
  765. * @param {string} id - The ID for the section.
  766. * @param {object} options - Object containing one property: params.
  767. * @param {object} options.params - Object containing the following properties.
  768. * @param {string} options.params.title - Title shown when section is collapsed and expanded.
  769. * @param {string=} [options.params.description] - Description shown at the top of the section.
  770. * @param {number=100} [options.params.priority] - The sort priority for the section.
  771. * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
  772. * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
  773. * @param {boolean=true} [options.params.active] - Whether the section is active or not.
  774. * @param {string} options.params.panel - The ID for the panel this section is associated with.
  775. * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
  776. */
  777. initialize: function ( id, options ) {
  778. var section = this;
  779. Container.prototype.initialize.call( section, id, options );
  780. section.id = id;
  781. section.panel = new api.Value();
  782. section.panel.bind( function ( id ) {
  783. $( section.headContainer ).toggleClass( 'control-subsection', !! id );
  784. });
  785. section.panel.set( section.params.panel || '' );
  786. api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
  787. section.embed();
  788. section.deferred.embedded.done( function () {
  789. section.ready();
  790. });
  791. },
  792. /**
  793. * Embed the container in the DOM when any parent panel is ready.
  794. *
  795. * @since 4.1.0
  796. */
  797. embed: function () {
  798. var inject,
  799. section = this,
  800. container = $( '#customize-theme-controls' );
  801. // Watch for changes to the panel state
  802. inject = function ( panelId ) {
  803. var parentContainer;
  804. if ( panelId ) {
  805. // The panel has been supplied, so wait until the panel object is registered
  806. api.panel( panelId, function ( panel ) {
  807. // The panel has been registered, wait for it to become ready/initialized
  808. panel.deferred.embedded.done( function () {
  809. parentContainer = panel.contentContainer;
  810. if ( ! section.headContainer.parent().is( parentContainer ) ) {
  811. parentContainer.append( section.headContainer );
  812. }
  813. if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  814. container.append( section.contentContainer );
  815. }
  816. section.deferred.embedded.resolve();
  817. });
  818. } );
  819. } else {
  820. // There is no panel, so embed the section in the root of the customizer
  821. parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  822. if ( ! section.headContainer.parent().is( parentContainer ) ) {
  823. parentContainer.append( section.headContainer );
  824. }
  825. if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  826. container.append( section.contentContainer );
  827. }
  828. section.deferred.embedded.resolve();
  829. }
  830. };
  831. section.panel.bind( inject );
  832. inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
  833. },
  834. /**
  835. * Add behaviors for the accordion section.
  836. *
  837. * @since 4.1.0
  838. */
  839. attachEvents: function () {
  840. var meta, content, section = this;
  841. if ( section.container.hasClass( 'cannot-expand' ) ) {
  842. return;
  843. }
  844. // Expand/Collapse accordion sections on click.
  845. section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
  846. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  847. return;
  848. }
  849. event.preventDefault(); // Keep this AFTER the key filter above
  850. if ( section.expanded() ) {
  851. section.collapse();
  852. } else {
  853. section.expand();
  854. }
  855. });
  856. // This is very similar to what is found for api.Panel.attachEvents().
  857. section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
  858. meta = section.container.find( '.section-meta' );
  859. if ( meta.hasClass( 'cannot-expand' ) ) {
  860. return;
  861. }
  862. content = meta.find( '.customize-section-description:first' );
  863. content.toggleClass( 'open' );
  864. content.slideToggle();
  865. content.attr( 'aria-expanded', function ( i, attr ) {
  866. return 'true' === attr ? 'false' : 'true';
  867. });
  868. });
  869. },
  870. /**
  871. * Return whether this section has any active controls.
  872. *
  873. * @since 4.1.0
  874. *
  875. * @returns {Boolean}
  876. */
  877. isContextuallyActive: function () {
  878. var section = this,
  879. controls = section.controls(),
  880. activeCount = 0;
  881. _( controls ).each( function ( control ) {
  882. if ( control.active() ) {
  883. activeCount += 1;
  884. }
  885. } );
  886. return ( activeCount !== 0 );
  887. },
  888. /**
  889. * Get the controls that are associated with this section, sorted by their priority Value.
  890. *
  891. * @since 4.1.0
  892. *
  893. * @returns {Array}
  894. */
  895. controls: function () {
  896. return this._children( 'section', 'control' );
  897. },
  898. /**
  899. * Update UI to reflect expanded state.
  900. *
  901. * @since 4.1.0
  902. *
  903. * @param {Boolean} expanded
  904. * @param {Object} args
  905. */
  906. onChangeExpanded: function ( expanded, args ) {
  907. var section = this,
  908. container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
  909. content = section.contentContainer,
  910. overlay = section.headContainer.closest( '.wp-full-overlay' ),
  911. backBtn = content.find( '.customize-section-back' ),
  912. sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
  913. expand, panel;
  914. if ( expanded && ! content.hasClass( 'open' ) ) {
  915. if ( args.unchanged ) {
  916. expand = args.completeCallback;
  917. } else {
  918. expand = $.proxy( function() {
  919. section._animateChangeExpanded( function() {
  920. sectionTitle.attr( 'tabindex', '-1' );
  921. backBtn.attr( 'tabindex', '0' );
  922. backBtn.focus();
  923. content.css( 'top', '' );
  924. container.scrollTop( 0 );
  925. if ( args.completeCallback ) {
  926. args.completeCallback();
  927. }
  928. } );
  929. content.addClass( 'open' );
  930. overlay.addClass( 'section-open' );
  931. api.state( 'expandedSection' ).set( section );
  932. }, this );
  933. }
  934. if ( ! args.allowMultiple ) {
  935. api.section.each( function ( otherSection ) {
  936. if ( otherSection !== section ) {
  937. otherSection.collapse( { duration: args.duration } );
  938. }
  939. });
  940. }
  941. if ( section.panel() ) {
  942. api.panel( section.panel() ).expand({
  943. duration: args.duration,
  944. completeCallback: expand
  945. });
  946. } else {
  947. api.panel.each( function( panel ) {
  948. panel.collapse();
  949. });
  950. expand();
  951. }
  952. } else if ( ! expanded && content.hasClass( 'open' ) ) {
  953. if ( section.panel() ) {
  954. panel = api.panel( section.panel() );
  955. if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
  956. panel.collapse();
  957. }
  958. }
  959. section._animateChangeExpanded( function() {
  960. backBtn.attr( 'tabindex', '-1' );
  961. sectionTitle.attr( 'tabindex', '0' );
  962. sectionTitle.focus();
  963. content.css( 'top', '' );
  964. if ( args.completeCallback ) {
  965. args.completeCallback();
  966. }
  967. } );
  968. content.removeClass( 'open' );
  969. overlay.removeClass( 'section-open' );
  970. if ( section === api.state( 'expandedSection' ).get() ) {
  971. api.state( 'expandedSection' ).set( false );
  972. }
  973. } else {
  974. if ( args.completeCallback ) {
  975. args.completeCallback();
  976. }
  977. }
  978. }
  979. });
  980. /**
  981. * wp.customize.ThemesSection
  982. *
  983. * Custom section for themes that functions similarly to a backwards panel,
  984. * and also handles the theme-details view rendering and navigation.
  985. *
  986. * @constructor
  987. * @augments wp.customize.Section
  988. * @augments wp.customize.Container
  989. */
  990. api.ThemesSection = api.Section.extend({
  991. currentTheme: '',
  992. overlay: '',
  993. template: '',
  994. screenshotQueue: null,
  995. $window: $( window ),
  996. /**
  997. * @since 4.2.0
  998. */
  999. initialize: function () {
  1000. this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
  1001. return api.Section.prototype.initialize.apply( this, arguments );
  1002. },
  1003. /**
  1004. * @since 4.2.0
  1005. */
  1006. ready: function () {
  1007. var section = this;
  1008. section.overlay = section.container.find( '.theme-overlay' );
  1009. section.template = wp.template( 'customize-themes-details-view' );
  1010. // Bind global keyboard events.
  1011. section.container.on( 'keydown', function( event ) {
  1012. if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
  1013. return;
  1014. }
  1015. // Pressing the right arrow key fires a theme:next event
  1016. if ( 39 === event.keyCode ) {
  1017. section.nextTheme();
  1018. }
  1019. // Pressing the left arrow key fires a theme:previous event
  1020. if ( 37 === event.keyCode ) {
  1021. section.previousTheme();
  1022. }
  1023. // Pressing the escape key fires a theme:collapse event
  1024. if ( 27 === event.keyCode ) {
  1025. section.closeDetails();
  1026. event.stopPropagation(); // Prevent section from being collapsed.
  1027. }
  1028. });
  1029. _.bindAll( this, 'renderScreenshots' );
  1030. },
  1031. /**
  1032. * Override Section.isContextuallyActive method.
  1033. *
  1034. * Ignore the active states' of the contained theme controls, and just
  1035. * use the section's own active state instead. This ensures empty search
  1036. * results for themes to cause the section to become inactive.
  1037. *
  1038. * @since 4.2.0
  1039. *
  1040. * @returns {Boolean}
  1041. */
  1042. isContextuallyActive: function () {
  1043. return this.active();
  1044. },
  1045. /**
  1046. * @since 4.2.0
  1047. */
  1048. attachEvents: function () {
  1049. var section = this;
  1050. // Expand/Collapse section/panel.
  1051. section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
  1052. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1053. return;
  1054. }
  1055. event.preventDefault(); // Keep this AFTER the key filter above
  1056. if ( section.expanded() ) {
  1057. section.collapse();
  1058. } else {
  1059. section.expand();
  1060. }
  1061. });
  1062. // Theme navigation in details view.
  1063. section.container.on( 'click keydown', '.left', function( event ) {
  1064. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1065. return;
  1066. }
  1067. event.preventDefault(); // Keep this AFTER the key filter above
  1068. section.previousTheme();
  1069. });
  1070. section.container.on( 'click keydown', '.right', function( event ) {
  1071. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1072. return;
  1073. }
  1074. event.preventDefault(); // Keep this AFTER the key filter above
  1075. section.nextTheme();
  1076. });
  1077. section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
  1078. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1079. return;
  1080. }
  1081. event.preventDefault(); // Keep this AFTER the key filter above
  1082. section.closeDetails();
  1083. });
  1084. var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
  1085. section.container.on( 'input', '#themes-filter', function( event ) {
  1086. var count,
  1087. term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
  1088. controls = section.controls();
  1089. _.each( controls, function( control ) {
  1090. control.filter( term );
  1091. });
  1092. renderScreenshots();
  1093. // Update theme count.
  1094. count = section.container.find( 'li.customize-control:visible' ).length;
  1095. section.container.find( '.theme-count' ).text( count );
  1096. });
  1097. // Pre-load the first 3 theme screenshots.
  1098. api.bind( 'ready', function () {
  1099. _.each( section.controls().slice( 0, 3 ), function ( control ) {
  1100. var img, src = control.params.theme.screenshot[0];
  1101. if ( src ) {
  1102. img = new Image();
  1103. img.src = src;
  1104. }
  1105. });
  1106. });
  1107. },
  1108. /**
  1109. * Update UI to reflect expanded state
  1110. *
  1111. * @since 4.2.0
  1112. *
  1113. * @param {Boolean} expanded
  1114. * @param {Object} args
  1115. * @param {Boolean} args.unchanged
  1116. * @param {Callback} args.completeCallback
  1117. */
  1118. onChangeExpanded: function ( expanded, args ) {
  1119. // Immediately call the complete callback if there were no changes
  1120. if ( args.unchanged ) {
  1121. if ( args.completeCallback ) {
  1122. args.completeCallback();
  1123. }
  1124. return;
  1125. }
  1126. // Note: there is a second argument 'args' passed
  1127. var panel = this,
  1128. section = panel.contentContainer,
  1129. overlay = section.closest( '.wp-full-overlay' ),
  1130. container = section.closest( '.wp-full-overlay-sidebar-content' ),
  1131. customizeBtn = section.find( '.customize-theme' ),
  1132. changeBtn = panel.headContainer.find( '.change-theme' );
  1133. if ( expanded && ! section.hasClass( 'current-panel' ) ) {
  1134. // Collapse any sibling sections/panels
  1135. api.section.each( function ( otherSection ) {
  1136. if ( otherSection !== panel ) {
  1137. otherSection.collapse( { duration: args.duration } );
  1138. }
  1139. });
  1140. api.panel.each( function ( otherPanel ) {
  1141. otherPanel.collapse( { duration: 0 } );
  1142. });
  1143. panel._animateChangeExpanded( function() {
  1144. changeBtn.attr( 'tabindex', '-1' );
  1145. customizeBtn.attr( 'tabindex', '0' );
  1146. customizeBtn.focus();
  1147. section.css( 'top', '' );
  1148. container.scrollTop( 0 );
  1149. if ( args.completeCallback ) {
  1150. args.completeCallback();
  1151. }
  1152. } );
  1153. overlay.addClass( 'in-themes-panel' );
  1154. section.addClass( 'current-panel' );
  1155. _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
  1156. panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
  1157. } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
  1158. panel._animateChangeExpanded( function() {
  1159. changeBtn.attr( 'tabindex', '0' );
  1160. customizeBtn.attr( 'tabindex', '-1' );
  1161. changeBtn.focus();
  1162. section.css( 'top', '' );
  1163. if ( args.completeCallback ) {
  1164. args.completeCallback();
  1165. }
  1166. } );
  1167. overlay.removeClass( 'in-themes-panel' );
  1168. section.removeClass( 'current-panel' );
  1169. panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
  1170. }
  1171. },
  1172. /**
  1173. * Render control's screenshot if the control comes into view.
  1174. *
  1175. * @since 4.2.0
  1176. */
  1177. renderScreenshots: function( ) {
  1178. var section = this;
  1179. // Fill queue initially.
  1180. if ( section.screenshotQueue === null ) {
  1181. section.screenshotQueue = section.controls();
  1182. }
  1183. // Are all screenshots rendered?
  1184. if ( ! section.screenshotQueue.length ) {
  1185. return;
  1186. }
  1187. section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
  1188. var $imageWrapper = control.container.find( '.theme-screenshot' ),
  1189. $image = $imageWrapper.find( 'img' );
  1190. if ( ! $image.length ) {
  1191. return false;
  1192. }
  1193. if ( $image.is( ':hidden' ) ) {
  1194. return true;
  1195. }
  1196. // Based on unveil.js.
  1197. var wt = section.$window.scrollTop(),
  1198. wb = wt + section.$window.height(),
  1199. et = $image.offset().top,
  1200. ih = $imageWrapper.height(),
  1201. eb = et + ih,
  1202. threshold = ih * 3,
  1203. inView = eb >= wt - threshold && et <= wb + threshold;
  1204. if ( inView ) {
  1205. control.container.trigger( 'render-screenshot' );
  1206. }
  1207. // If the image is in view return false so it's cleared from the queue.
  1208. return ! inView;
  1209. } );
  1210. },
  1211. /**
  1212. * Advance the modal to the next theme.
  1213. *
  1214. * @since 4.2.0
  1215. */
  1216. nextTheme: function () {
  1217. var section = this;
  1218. if ( section.getNextTheme() ) {
  1219. section.showDetails( section.getNextTheme(), function() {
  1220. section.overlay.find( '.right' ).focus();
  1221. } );
  1222. }
  1223. },
  1224. /**
  1225. * Get the next theme model.
  1226. *
  1227. * @since 4.2.0
  1228. */
  1229. getNextTheme: function () {
  1230. var control, next;
  1231. control = api.control( 'theme_' + this.currentTheme );
  1232. next = control.container.next( 'li.customize-control-theme' );
  1233. if ( ! next.length ) {
  1234. return false;
  1235. }
  1236. next = next[0].id.replace( 'customize-control-', '' );
  1237. control = api.control( next );
  1238. return control.params.theme;
  1239. },
  1240. /**
  1241. * Advance the modal to the previous theme.
  1242. *
  1243. * @since 4.2.0
  1244. */
  1245. previousTheme: function () {
  1246. var section = this;
  1247. if ( section.getPreviousTheme() ) {
  1248. section.showDetails( section.getPreviousTheme(), function() {
  1249. section.overlay.find( '.left' ).focus();
  1250. } );
  1251. }
  1252. },
  1253. /**
  1254. * Get the previous theme model.
  1255. *
  1256. * @since 4.2.0
  1257. */
  1258. getPreviousTheme: function () {
  1259. var control, previous;
  1260. control = api.control( 'theme_' + this.currentTheme );
  1261. previous = control.container.prev( 'li.customize-control-theme' );
  1262. if ( ! previous.length ) {
  1263. return false;
  1264. }
  1265. previous = previous[0].id.replace( 'customize-control-', '' );
  1266. control = api.control( previous );
  1267. return control.params.theme;
  1268. },
  1269. /**
  1270. * Disable buttons when we're viewing the first or last theme.
  1271. *
  1272. * @since 4.2.0
  1273. */
  1274. updateLimits: function () {
  1275. if ( ! this.getNextTheme() ) {
  1276. this.overlay.find( '.right' ).addClass( 'disabled' );
  1277. }
  1278. if ( ! this.getPreviousTheme() ) {
  1279. this.overlay.find( '.left' ).addClass( 'disabled' );
  1280. }
  1281. },
  1282. /**
  1283. * Load theme preview.
  1284. *
  1285. * @since 4.7.0
  1286. * @access public
  1287. *
  1288. * @param {string} themeId Theme ID.
  1289. * @returns {jQuery.promise} Promise.
  1290. */
  1291. loadThemePreview: function( themeId ) {
  1292. var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
  1293. urlParser = document.createElement( 'a' );
  1294. urlParser.href = location.href;
  1295. urlParser.search = $.param( _.extend(
  1296. api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  1297. {
  1298. theme: themeId,
  1299. changeset_uuid: api.settings.changeset.uuid
  1300. }
  1301. ) );
  1302. overlay = $( '.wp-full-overlay' );
  1303. overlay.addClass( 'customize-loading' );
  1304. onceProcessingComplete = function() {
  1305. var request;
  1306. if ( api.state( 'processing' ).get() > 0 ) {
  1307. return;
  1308. }
  1309. api.state( 'processing' ).unbind( onceProcessingComplete );
  1310. request = api.requestChangesetUpdate();
  1311. request.done( function() {
  1312. $( window ).off( 'beforeunload.customize-confirm' );
  1313. top.location.href = urlParser.href;
  1314. deferred.resolve();
  1315. } );
  1316. request.fail( function() {
  1317. overlay.removeClass( 'customize-loading' );
  1318. deferred.reject();
  1319. } );
  1320. };
  1321. if ( 0 === api.state( 'processing' ).get() ) {
  1322. onceProcessingComplete();
  1323. } else {
  1324. api.state( 'processing' ).bind( onceProcessingComplete );
  1325. }
  1326. return deferred.promise();
  1327. },
  1328. /**
  1329. * Render & show the theme details for a given theme model.
  1330. *
  1331. * @since 4.2.0
  1332. *
  1333. * @param {Object} theme
  1334. */
  1335. showDetails: function ( theme, callback ) {
  1336. var section = this, link;
  1337. callback = callback || function(){};
  1338. section.currentTheme = theme.id;
  1339. section.overlay.html( section.template( theme ) )
  1340. .fadeIn( 'fast' )
  1341. .focus();
  1342. $( 'body' ).addClass( 'modal-open' );
  1343. section.containFocus( section.overlay );
  1344. section.updateLimits();
  1345. link = section.overlay.find( '.inactive-theme > a' );
  1346. link.on( 'click', function( event ) {
  1347. event.preventDefault();
  1348. // Short-circuit if request is currently being made.
  1349. if ( link.hasClass( 'disabled' ) ) {
  1350. return;
  1351. }
  1352. link.addClass( 'disabled' );
  1353. section.loadThemePreview( theme.id ).fail( function() {
  1354. link.removeClass( 'disabled' );
  1355. } );
  1356. } );
  1357. callback();
  1358. },
  1359. /**
  1360. * Close the theme details modal.
  1361. *
  1362. * @since 4.2.0
  1363. */
  1364. closeDetails: function () {
  1365. $( 'body' ).removeClass( 'modal-open' );
  1366. this.overlay.fadeOut( 'fast' );
  1367. api.control( 'theme_' + this.currentTheme ).focus();
  1368. },
  1369. /**
  1370. * Keep tab focus within the theme details modal.
  1371. *
  1372. * @since 4.2.0
  1373. */
  1374. containFocus: function( el ) {
  1375. var tabbables;
  1376. el.on( 'keydown', function( event ) {
  1377. // Return if it's not the tab key
  1378. // When navigating with prev/next focus is already handled
  1379. if ( 9 !== event.keyCode ) {
  1380. return;
  1381. }
  1382. // uses jQuery UI to get the tabbable elements
  1383. tabbables = $( ':tabbable', el );
  1384. // Keep focus within the overlay
  1385. if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
  1386. tabbables.first().focus();
  1387. return false;
  1388. } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
  1389. tabbables.last().focus();
  1390. return false;
  1391. }
  1392. });
  1393. }
  1394. });
  1395. /**
  1396. * @since 4.1.0
  1397. *
  1398. * @class
  1399. * @augments wp.customize.Class
  1400. */
  1401. api.Panel = Container.extend({
  1402. containerType: 'panel',
  1403. /**
  1404. * @since 4.1.0
  1405. *
  1406. * @param {string} id - The ID for the panel.
  1407. * @param {object} options - Object containing one property: params.
  1408. * @param {object} options.params - Object containing the following properties.
  1409. * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
  1410. * @param {string=} [options.params.description] - Description shown at the top of the panel.
  1411. * @param {number=100} [options.params.priority] - The sort priority for the panel.
  1412. * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
  1413. * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
  1414. * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
  1415. */
  1416. initialize: function ( id, options ) {
  1417. var panel = this;
  1418. Container.prototype.initialize.call( panel, id, options );
  1419. panel.embed();
  1420. panel.deferred.embedded.done( function () {
  1421. panel.ready();
  1422. });
  1423. },
  1424. /**
  1425. * Embed the container in the DOM when any parent panel is ready.
  1426. *
  1427. * @since 4.1.0
  1428. */
  1429. embed: function () {
  1430. var panel = this,
  1431. container = $( '#customize-theme-controls' ),
  1432. parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  1433. if ( ! panel.headContainer.parent().is( parentContainer ) ) {
  1434. parentContainer.append( panel.headContainer );
  1435. }
  1436. if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
  1437. container.append( panel.contentContainer );
  1438. panel.renderContent();
  1439. }
  1440. panel.deferred.embedded.resolve();
  1441. },
  1442. /**
  1443. * @since 4.1.0
  1444. */
  1445. attachEvents: function () {
  1446. var meta, panel = this;
  1447. // Expand/Collapse accordion sections on click.
  1448. panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
  1449. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1450. return;
  1451. }
  1452. event.preventDefault(); // Keep this AFTER the key filter above
  1453. if ( ! panel.expanded() ) {
  1454. panel.expand();
  1455. }
  1456. });
  1457. // Close panel.
  1458. panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
  1459. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1460. return;
  1461. }
  1462. event.preventDefault(); // Keep this AFTER the key filter above
  1463. if ( panel.expanded() ) {
  1464. panel.collapse();
  1465. }
  1466. });
  1467. meta = panel.container.find( '.panel-meta:first' );
  1468. meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
  1469. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1470. return;
  1471. }
  1472. event.preventDefault(); // Keep this AFTER the key filter above
  1473. if ( meta.hasClass( 'cannot-expand' ) ) {
  1474. return;
  1475. }
  1476. var content = meta.find( '.customize-panel-description:first' );
  1477. if ( meta.hasClass( 'open' ) ) {
  1478. meta.toggleClass( 'open' );
  1479. content.slideUp( panel.defaultExpandedArguments.duration );
  1480. $( this ).attr( 'aria-expanded', false );
  1481. } else {
  1482. content.slideDown( panel.defaultExpandedArguments.duration );
  1483. meta.toggleClass( 'open' );
  1484. $( this ).attr( 'aria-expanded', true );
  1485. }
  1486. });
  1487. },
  1488. /**
  1489. * Get the sections that are associated with this panel, sorted by their priority Value.
  1490. *
  1491. * @since 4.1.0
  1492. *
  1493. * @returns {Array}
  1494. */
  1495. sections: function () {
  1496. return this._children( 'panel', 'section' );
  1497. },
  1498. /**
  1499. * Return whether this panel has any active sections.
  1500. *
  1501. * @since 4.1.0
  1502. *
  1503. * @returns {boolean}
  1504. */
  1505. isContextuallyActive: function () {
  1506. var panel = this,
  1507. sections = panel.sections(),
  1508. activeCount = 0;
  1509. _( sections ).each( function ( section ) {
  1510. if ( section.active() && section.isContextuallyActive() ) {
  1511. activeCount += 1;
  1512. }
  1513. } );
  1514. return ( activeCount !== 0 );
  1515. },
  1516. /**
  1517. * Update UI to reflect expanded state
  1518. *
  1519. * @since 4.1.0
  1520. *
  1521. * @param {Boolean} expanded
  1522. * @param {Object} args
  1523. * @param {Boolean} args.unchanged
  1524. * @param {Function} args.completeCallback
  1525. */
  1526. onChangeExpanded: function ( expanded, args ) {
  1527. // Immediately call the complete callback if there were no changes
  1528. if ( args.unchanged ) {
  1529. if ( args.completeCallback ) {
  1530. args.completeCallback();
  1531. }
  1532. return;
  1533. }
  1534. // Note: there is a second argument 'args' passed
  1535. var panel = this,
  1536. accordionSection = panel.contentContainer,
  1537. overlay = accordionSection.closest( '.wp-full-overlay' ),
  1538. container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
  1539. topPanel = panel.headContainer.find( '.accordion-section-title' ),
  1540. backBtn = accordionSection.find( '.customize-panel-back' ),
  1541. childSections = panel.sections(),
  1542. skipTransition;
  1543. if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
  1544. // Collapse any sibling sections/panels
  1545. api.section.each( function ( section ) {
  1546. if ( panel.id !== section.panel() ) {
  1547. section.collapse( { duration: 0 } );
  1548. }
  1549. });
  1550. api.panel.each( function ( otherPanel ) {
  1551. if ( panel !== otherPanel ) {
  1552. otherPanel.collapse( { duration: 0 } );
  1553. }
  1554. });
  1555. if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
  1556. accordionSection.addClass( 'current-panel skip-transition' );
  1557. overlay.addClass( 'in-sub-panel' );
  1558. childSections[0].expand( {
  1559. completeCallback: args.completeCallback
  1560. } );
  1561. } else {
  1562. panel._animateChangeExpanded( function() {
  1563. topPanel.attr( 'tabindex', '-1' );
  1564. backBtn.attr( 'tabindex', '0' );
  1565. backBtn.focus();
  1566. accordionSection.css( 'top', '' );
  1567. container.scrollTop( 0 );
  1568. if ( args.completeCallback ) {
  1569. args.completeCallback();
  1570. }
  1571. } );
  1572. accordionSection.addClass( 'current-panel' );
  1573. overlay.addClass( 'in-sub-panel' );
  1574. }
  1575. api.state( 'expandedPanel' ).set( panel );
  1576. } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
  1577. skipTransition = accordionSection.hasClass( 'skip-transition' );
  1578. if ( ! skipTransition ) {
  1579. panel._animateChangeExpanded( function() {
  1580. topPanel.attr( 'tabindex', '0' );
  1581. backBtn.attr( 'tabindex', '-1' );
  1582. topPanel.focus();
  1583. accordionSection.css( 'top', '' );
  1584. if ( args.completeCallback ) {
  1585. args.completeCallback();
  1586. }
  1587. } );
  1588. } else {
  1589. accordionSection.removeClass( 'skip-transition' );
  1590. }
  1591. overlay.removeClass( 'in-sub-panel' );
  1592. accordionSection.removeClass( 'current-panel' );
  1593. if ( panel === api.state( 'expandedPanel' ).get() ) {
  1594. api.state( 'expandedPanel' ).set( false );
  1595. }
  1596. }
  1597. },
  1598. /**
  1599. * Render the panel from its JS template, if it exists.
  1600. *
  1601. * The panel's container must already exist in the DOM.
  1602. *
  1603. * @since 4.3.0
  1604. */
  1605. renderContent: function () {
  1606. var template,
  1607. panel = this;
  1608. // Add the content to the container.
  1609. if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
  1610. template = wp.template( panel.templateSelector + '-content' );
  1611. } else {
  1612. template = wp.template( 'customize-panel-default-content' );
  1613. }
  1614. if ( template && panel.headContainer ) {
  1615. panel.contentContainer.html( template( panel.params ) );
  1616. }
  1617. }
  1618. });
  1619. /**
  1620. * A Customizer Control.
  1621. *
  1622. * A control provides a UI element that allows a user to modify a Customizer Setting.
  1623. *
  1624. * @see PHP class WP_Customize_Control.
  1625. *
  1626. * @class
  1627. * @augments wp.customize.Class
  1628. *
  1629. * @param {string} id Unique identifier for the control instance.
  1630. * @param {object} options Options hash for the control instance.
  1631. * @param {object} options.params
  1632. * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
  1633. * @param {string} options.params.content The HTML content for the control.
  1634. * @param {string} options.params.priority Order of priority to show the control within the section.
  1635. * @param {string} options.params.active
  1636. * @param {string} options.params.section The ID of the section the control belongs to.
  1637. * @param {string} options.params.settings.default The ID of the setting the control relates to.
  1638. * @param {string} options.params.settings.data
  1639. * @param {string} options.params.label
  1640. * @param {string} options.params.description
  1641. * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
  1642. */
  1643. api.Control = api.Class.extend({
  1644. defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  1645. initialize: function( id, options ) {
  1646. var control = this,
  1647. nodes, radios, settings;
  1648. control.params = {};
  1649. $.extend( control, options || {} );
  1650. control.id = id;
  1651. control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
  1652. control.templateSelector = 'customize-control-' + control.params.type + '-content';
  1653. control.container = control.params.content ? $( control.params.content ) : $( control.selector );
  1654. control.deferred = {
  1655. embedded: new $.Deferred()
  1656. };
  1657. control.section = new api.Value();
  1658. control.priority = new api.Value();
  1659. control.active = new api.Value();
  1660. control.activeArgumentsQueue = [];
  1661. control.notifications = new api.Values({ defaultConstructor: api.Notification });
  1662. control.elements = [];
  1663. nodes = control.container.find('[data-customize-setting-link]');
  1664. radios = {};
  1665. nodes.each( function() {
  1666. var node = $( this ),
  1667. name;
  1668. if ( node.is( ':radio' ) ) {
  1669. name = node.prop( 'name' );
  1670. if ( radios[ name ] ) {
  1671. return;
  1672. }
  1673. radios[ name ] = true;
  1674. node = nodes.filter( '[name="' + name + '"]' );
  1675. }
  1676. api( node.data( 'customizeSettingLink' ), function( setting ) {
  1677. var element = new api.Element( node );
  1678. control.elements.push( element );
  1679. element.sync( setting );
  1680. element.set( setting() );
  1681. });
  1682. });
  1683. control.active.bind( function ( active ) {
  1684. var args = control.activeArgumentsQueue.shift();
  1685. args = $.extend( {}, control.defaultActiveArguments, args );
  1686. control.onChangeActive( active, args );
  1687. } );
  1688. control.section.set( control.params.section );
  1689. control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
  1690. control.active.set( control.params.active );
  1691. api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
  1692. /*
  1693. * After all settings related to the control are available,
  1694. * make them available on the control and embed the control into the page.
  1695. */
  1696. settings = $.map( control.params.settings, function( value ) {
  1697. return value;
  1698. });
  1699. if ( 0 === settings.length ) {
  1700. control.setting = null;
  1701. control.settings = {};
  1702. control.embed();
  1703. } else {
  1704. api.apply( api, settings.concat( function() {
  1705. var key;
  1706. control.settings = {};
  1707. for ( key in control.params.settings ) {
  1708. control.settings[ key ] = api( control.params.settings[ key ] );
  1709. }
  1710. control.setting = control.settings['default'] || null;
  1711. // Add setting notifications to the control notification.
  1712. _.each( control.settings, function( setting ) {
  1713. setting.notifications.bind( 'add', function( settingNotification ) {
  1714. var controlNotification, code, params;
  1715. code = setting.id + ':' + settingNotification.code;
  1716. params = _.extend(
  1717. {},
  1718. settingNotification,
  1719. {
  1720. setting: setting.id
  1721. }
  1722. );
  1723. controlNotification = new api.Notification( code, params );
  1724. control.notifications.add( controlNotification.code, controlNotification );
  1725. } );
  1726. setting.notifications.bind( 'remove', function( settingNotification ) {
  1727. control.notifications.remove( setting.id + ':' + settingNotification.code );
  1728. } );
  1729. } );
  1730. control.embed();
  1731. }) );
  1732. }
  1733. // After the control is embedded on the page, invoke the "ready" method.
  1734. control.deferred.embedded.done( function () {
  1735. /*
  1736. * Note that this debounced/deferred rendering is needed for two reasons:
  1737. * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
  1738. * 2) Improve performance when adding/removing multiple notifications at a time.
  1739. */
  1740. var debouncedRenderNotifications = _.debounce( function renderNotifications() {
  1741. control.renderNotifications();
  1742. } );
  1743. control.notifications.bind( 'add', function( notification ) {
  1744. wp.a11y.speak( notification.message, 'assertive' );
  1745. debouncedRenderNotifications();
  1746. } );
  1747. control.notifications.bind( 'remove', debouncedRenderNotifications );
  1748. control.renderNotifications();
  1749. control.ready();
  1750. });
  1751. },
  1752. /**
  1753. * Embed the control into the page.
  1754. */
  1755. embed: function () {
  1756. var control = this,
  1757. inject;
  1758. // Watch for changes to the section state
  1759. inject = function ( sectionId ) {
  1760. var parentContainer;
  1761. if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
  1762. return;
  1763. }
  1764. // Wait for the section to be registered
  1765. api.section( sectionId, function ( section ) {
  1766. // Wait for the section to be ready/initialized
  1767. section.deferred.embedded.done( function () {
  1768. parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  1769. if ( ! control.container.parent().is( parentContainer ) ) {
  1770. parentContainer.append( control.container );
  1771. control.renderContent();
  1772. }
  1773. control.deferred.embedded.resolve();
  1774. });
  1775. });
  1776. };
  1777. control.section.bind( inject );
  1778. inject( control.section.get() );
  1779. },
  1780. /**
  1781. * Triggered when the control's markup has been injected into the DOM.
  1782. *
  1783. * @returns {void}
  1784. */
  1785. ready: function() {
  1786. var control = this, newItem;
  1787. if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
  1788. newItem = control.container.find( '.new-content-item' );
  1789. newItem.hide(); // Hide in JS to preserve flex display when showing.
  1790. control.container.on( 'click', '.add-new-toggle', function( e ) {
  1791. $( e.currentTarget ).slideUp( 180 );
  1792. newItem.slideDown( 180 );
  1793. newItem.find( '.create-item-input' ).focus();
  1794. });
  1795. control.container.on( 'click', '.add-content', function() {
  1796. control.addNewPage();
  1797. });
  1798. control.container.on( 'keyup', '.create-item-input', function( e ) {
  1799. if ( 13 === e.which ) { // Enter
  1800. control.addNewPage();
  1801. }
  1802. });
  1803. }
  1804. },
  1805. /**
  1806. * Get the element inside of a control's container that contains the validation error message.
  1807. *
  1808. * Control subclasses may override this to return the proper container to render notifications into.
  1809. * Injects the notification container for existing controls that lack the necessary container,
  1810. * including special handling for nav menu items and widgets.
  1811. *
  1812. * @since 4.6.0
  1813. * @returns {jQuery} Setting validation message element.
  1814. * @this {wp.customize.Control}
  1815. */
  1816. getNotificationsContainerElement: function() {
  1817. var control = this, controlTitle, notificationsContainer;
  1818. notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
  1819. if ( notificationsContainer.length ) {
  1820. return notificationsContainer;
  1821. }
  1822. notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
  1823. if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
  1824. control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
  1825. } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
  1826. control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
  1827. } else {
  1828. controlTitle = control.container.find( '.customize-control-title' );
  1829. if ( controlTitle.length ) {
  1830. controlTitle.after( notificationsContainer );
  1831. } else {
  1832. control.container.prepend( notificationsContainer );
  1833. }
  1834. }
  1835. return notificationsContainer;
  1836. },
  1837. /**
  1838. * Render notifications.
  1839. *
  1840. * Renders the `control.notifications` into the control's container.
  1841. * Control subclasses may override this method to do their own handling
  1842. * of rendering notifications.
  1843. *
  1844. * @since 4.6.0
  1845. * @this {wp.customize.Control}
  1846. */
  1847. renderNotifications: function() {
  1848. var control = this, container, notifications, hasError = false;
  1849. container = control.getNotificationsContainerElement();
  1850. if ( ! container || ! container.length ) {
  1851. return;
  1852. }
  1853. notifications = [];
  1854. control.notifications.each( function( notification ) {
  1855. notifications.push( notification );
  1856. if ( 'error' === notification.type ) {
  1857. hasError = true;
  1858. }
  1859. } );
  1860. if ( 0 === notifications.length ) {
  1861. container.stop().slideUp( 'fast' );
  1862. } else {
  1863. container.stop().slideDown( 'fast', null, function() {
  1864. $( this ).css( 'height', 'auto' );
  1865. } );
  1866. }
  1867. if ( ! control.notificationsTemplate ) {
  1868. control.notificationsTemplate = wp.template( 'customize-control-notifications' );
  1869. }
  1870. control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
  1871. control.container.toggleClass( 'has-error', hasError );
  1872. container.empty().append( $.trim(
  1873. control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
  1874. ) );
  1875. },
  1876. /**
  1877. * Normal controls do not expand, so just expand its parent
  1878. *
  1879. * @param {Object} [params]
  1880. */
  1881. expand: function ( params ) {
  1882. api.section( this.section() ).expand( params );
  1883. },
  1884. /**
  1885. * Bring the containing section and panel into view and then
  1886. * this control into view, focusing on the first input.
  1887. */
  1888. focus: focus,
  1889. /**
  1890. * Update UI in response to a change in the control's active state.
  1891. * This does not change the active state, it merely handles the behavior
  1892. * for when it does change.
  1893. *
  1894. * @since 4.1.0
  1895. *
  1896. * @param {Boolean} active
  1897. * @param {Object} args
  1898. * @param {Number} args.duration
  1899. * @param {Callback} args.completeCallback
  1900. */
  1901. onChangeActive: function ( active, args ) {
  1902. if ( args.unchanged ) {
  1903. if ( args.completeCallback ) {
  1904. args.completeCallback();
  1905. }
  1906. return;
  1907. }
  1908. if ( ! $.contains( document, this.container[0] ) ) {
  1909. // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
  1910. this.container.toggle( active );
  1911. if ( args.completeCallback ) {
  1912. args.completeCallback();
  1913. }
  1914. } else if ( active ) {
  1915. this.container.slideDown( args.duration, args.completeCallback );
  1916. } else {
  1917. this.container.slideUp( args.duration, args.completeCallback );
  1918. }
  1919. },
  1920. /**
  1921. * @deprecated 4.1.0 Use this.onChangeActive() instead.
  1922. */
  1923. toggle: function ( active ) {
  1924. return this.onChangeActive( active, this.defaultActiveArguments );
  1925. },
  1926. /**
  1927. * Shorthand way to enable the active state.
  1928. *
  1929. * @since 4.1.0
  1930. *
  1931. * @param {Object} [params]
  1932. * @returns {Boolean} false if already active
  1933. */
  1934. activate: Container.prototype.activate,
  1935. /**
  1936. * Shorthand way to disable the active state.
  1937. *
  1938. * @since 4.1.0
  1939. *
  1940. * @param {Object} [params]
  1941. * @returns {Boolean} false if already inactive
  1942. */
  1943. deactivate: Container.prototype.deactivate,
  1944. /**
  1945. * Re-use _toggleActive from Container class.
  1946. *
  1947. * @access private
  1948. */
  1949. _toggleActive: Container.prototype._toggleActive,
  1950. dropdownInit: function() {
  1951. var control = this,
  1952. statuses = this.container.find('.dropdown-status'),
  1953. params = this.params,
  1954. toggleFreeze = false,
  1955. update = function( to ) {
  1956. if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
  1957. statuses.html( params.statuses[ to ] ).show();
  1958. else
  1959. statuses.hide();
  1960. };
  1961. // Support the .dropdown class to open/close complex elements
  1962. this.container.on( 'click keydown', '.dropdown', function( event ) {
  1963. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1964. return;
  1965. }
  1966. event.preventDefault();
  1967. if (!toggleFreeze)
  1968. control.container.toggleClass('open');
  1969. if ( control.container.hasClass('open') )
  1970. control.container.parent().parent().find('li.library-selected').focus();
  1971. // Don't want to fire focus and click at same time
  1972. toggleFreeze = true;
  1973. setTimeout(function () {
  1974. toggleFreeze = false;
  1975. }, 400);
  1976. });
  1977. this.setting.bind( update );
  1978. update( this.setting() );
  1979. },
  1980. /**
  1981. * Render the control from its JS template, if it exists.
  1982. *
  1983. * The control's container must already exist in the DOM.
  1984. *
  1985. * @since 4.1.0
  1986. */
  1987. renderContent: function () {
  1988. var template,
  1989. control = this;
  1990. // Replace the container element's content with the control.
  1991. if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
  1992. template = wp.template( control.templateSelector );
  1993. if ( template && control.container ) {
  1994. control.container.html( template( control.params ) );
  1995. }
  1996. }
  1997. },
  1998. /**
  1999. * Add a new page to a dropdown-pages control reusing menus code for this.
  2000. *
  2001. * @since 4.7.0
  2002. * @access private
  2003. * @returns {void}
  2004. */
  2005. addNewPage: function () {
  2006. var control = this, promise, toggle, container, input, title, select;
  2007. if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
  2008. return;
  2009. }
  2010. toggle = control.container.find( '.add-new-toggle' );
  2011. container = control.container.find( '.new-content-item' );
  2012. input = control.container.find( '.create-item-input' );
  2013. title = input.val();
  2014. select = control.container.find( 'select' );
  2015. if ( ! title ) {
  2016. input.addClass( 'invalid' );
  2017. return;
  2018. }
  2019. input.removeClass( 'invalid' );
  2020. input.attr( 'disabled', 'disabled' );
  2021. // The menus functions add the page, publish when appropriate, and also add the new page to the dropdown-pages controls.
  2022. promise = api.Menus.insertAutoDraftPost( {
  2023. post_title: title,
  2024. post_type: 'page'
  2025. } );
  2026. promise.done( function( data ) {
  2027. var availableItem, $content, itemTemplate;
  2028. // Prepare the new page as an available menu item.
  2029. // See api.Menus.submitNew().
  2030. availableItem = new api.Menus.AvailableItemModel( {
  2031. 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
  2032. 'title': title,
  2033. 'type': 'post_type',
  2034. 'type_label': api.Menus.data.l10n.page_label,
  2035. 'object': 'page',
  2036. 'object_id': data.post_id,
  2037. 'url': data.url
  2038. } );
  2039. // Add the new item to the list of available menu items.
  2040. api.Menus.availableMenuItemsPanel.collection.add( availableItem );
  2041. $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
  2042. itemTemplate = wp.template( 'available-menu-item' );
  2043. $content.prepend( itemTemplate( availableItem.attributes ) );
  2044. // Focus the select control.
  2045. select.focus();
  2046. control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
  2047. // Reset the create page form.
  2048. container.slideUp( 180 );
  2049. toggle.slideDown( 180 );
  2050. } );
  2051. promise.always( function() {
  2052. input.val( '' ).removeAttr( 'disabled' );
  2053. } );
  2054. }
  2055. });
  2056. /**
  2057. * A colorpicker control.
  2058. *
  2059. * @class
  2060. * @augments wp.customize.Control
  2061. * @augments wp.customize.Class
  2062. */
  2063. api.ColorControl = api.Control.extend({
  2064. ready: function() {
  2065. var control = this,
  2066. isHueSlider = this.params.mode === 'hue',
  2067. updating = false,
  2068. picker;
  2069. if ( isHueSlider ) {
  2070. picker = this.container.find( '.color-picker-hue' );
  2071. picker.val( control.setting() ).wpColorPicker({
  2072. change: function( event, ui ) {
  2073. updating = true;
  2074. control.setting( ui.color.h() );
  2075. updating = false;
  2076. }
  2077. });
  2078. } else {
  2079. picker = this.container.find( '.color-picker-hex' );
  2080. picker.val( control.setting() ).wpColorPicker({
  2081. change: function() {
  2082. updating = true;
  2083. control.setting.set( picker.wpColorPicker( 'color' ) );
  2084. updating = false;
  2085. },
  2086. clear: function() {
  2087. updating = true;
  2088. control.setting.set( '' );
  2089. updating = false;
  2090. }
  2091. });
  2092. }
  2093. control.setting.bind( function ( value ) {
  2094. // Bail if the update came from the control itself.
  2095. if ( updating ) {
  2096. return;
  2097. }
  2098. picker.val( value );
  2099. picker.wpColorPicker( 'color', value );
  2100. } );
  2101. // Collapse color picker when hitting Esc instead of collapsing the current section.
  2102. control.container.on( 'keydown', function( event ) {
  2103. var pickerContainer;
  2104. if ( 27 !== event.which ) { // Esc.
  2105. return;
  2106. }
  2107. pickerContainer = control.container.find( '.wp-picker-container' );
  2108. if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
  2109. picker.wpColorPicker( 'close' );
  2110. control.container.find( '.wp-color-result' ).focus();
  2111. event.stopPropagation(); // Prevent section from being collapsed.
  2112. }
  2113. } );
  2114. }
  2115. });
  2116. /**
  2117. * A control that implements the media modal.
  2118. *
  2119. * @class
  2120. * @augments wp.customize.Control
  2121. * @augments wp.customize.Class
  2122. */
  2123. api.MediaControl = api.Control.extend({
  2124. /**
  2125. * When the control's DOM structure is ready,
  2126. * set up internal event bindings.
  2127. */
  2128. ready: function() {
  2129. var control = this;
  2130. // Shortcut so that we don't have to use _.bind every time we add a callback.
  2131. _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
  2132. // Bind events, with delegation to facilitate re-rendering.
  2133. control.container.on( 'click keydown', '.upload-button', control.openFrame );
  2134. control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
  2135. control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
  2136. control.container.on( 'click keydown', '.default-button', control.restoreDefault );
  2137. control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
  2138. control.container.on( 'click keydown', '.remove-button', control.removeFile );
  2139. control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
  2140. // Resize the player controls when it becomes visible (ie when section is expanded)
  2141. api.section( control.section() ).container
  2142. .on( 'expanded', function() {
  2143. if ( control.player ) {
  2144. control.player.setControlsSize();
  2145. }
  2146. })
  2147. .on( 'collapsed', function() {
  2148. control.pausePlayer();
  2149. });
  2150. /**
  2151. * Set attachment data and render content.
  2152. *
  2153. * Note that BackgroundImage.prototype.ready applies this ready method
  2154. * to itself. Since BackgroundImage is an UploadControl, the value
  2155. * is the attachment URL instead of the attachment ID. In this case
  2156. * we skip fetching the attachment data because we have no ID available,
  2157. * and it is the responsibility of the UploadControl to set the control's
  2158. * attachmentData before calling the renderContent method.
  2159. *
  2160. * @param {number|string} value Attachment
  2161. */
  2162. function setAttachmentDataAndRenderContent( value ) {
  2163. var hasAttachmentData = $.Deferred();
  2164. if ( control.extended( api.UploadControl ) ) {
  2165. hasAttachmentData.resolve();
  2166. } else {
  2167. value = parseInt( value, 10 );
  2168. if ( _.isNaN( value ) || value <= 0 ) {
  2169. delete control.params.attachment;
  2170. hasAttachmentData.resolve();
  2171. } else if ( control.params.attachment && control.params.attachment.id === value ) {
  2172. hasAttachmentData.resolve();
  2173. }
  2174. }
  2175. // Fetch the attachment data.
  2176. if ( 'pending' === hasAttachmentData.state() ) {
  2177. wp.media.attachment( value ).fetch().done( function() {
  2178. control.params.attachment = this.attributes;
  2179. hasAttachmentData.resolve();
  2180. // Send attachment information to the preview for possible use in `postMessage` transport.
  2181. wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
  2182. } );
  2183. }
  2184. hasAttachmentData.done( function() {
  2185. control.renderContent();
  2186. } );
  2187. }
  2188. // Ensure attachment data is initially set (for dynamically-instantiated controls).
  2189. setAttachmentDataAndRenderContent( control.setting() );
  2190. // Update the attachment data and re-render the control when the setting changes.
  2191. control.setting.bind( setAttachmentDataAndRenderContent );
  2192. },
  2193. pausePlayer: function () {
  2194. this.player && this.player.pause();
  2195. },
  2196. cleanupPlayer: function () {
  2197. this.player && wp.media.mixin.removePlayer( this.player );
  2198. },
  2199. /**
  2200. * Open the media modal.
  2201. */
  2202. openFrame: function( event ) {
  2203. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2204. return;
  2205. }
  2206. event.preventDefault();
  2207. if ( ! this.frame ) {
  2208. this.initFrame();
  2209. }
  2210. this.frame.open();
  2211. },
  2212. /**
  2213. * Create a media modal select frame, and store it so the instance can be reused when needed.
  2214. */
  2215. initFrame: function() {
  2216. this.frame = wp.media({
  2217. button: {
  2218. text: this.params.button_labels.frame_button
  2219. },
  2220. states: [
  2221. new wp.media.controller.Library({
  2222. title: this.params.button_labels.frame_title,
  2223. library: wp.media.query({ type: this.params.mime_type }),
  2224. multiple: false,
  2225. date: false
  2226. })
  2227. ]
  2228. });
  2229. // When a file is selected, run a callback.
  2230. this.frame.on( 'select', this.select );
  2231. },
  2232. /**
  2233. * Callback handler for when an attachment is selected in the media modal.
  2234. * Gets the selected image information, and sets it within the control.
  2235. */
  2236. select: function() {
  2237. // Get the attachment from the modal frame.
  2238. var node,
  2239. attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  2240. mejsSettings = window._wpmejsSettings || {};
  2241. this.params.attachment = attachment;
  2242. // Set the Customizer setting; the callback takes care of rendering.
  2243. this.setting( attachment.id );
  2244. node = this.container.find( 'audio, video' ).get(0);
  2245. // Initialize audio/video previews.
  2246. if ( node ) {
  2247. this.player = new MediaElementPlayer( node, mejsSettings );
  2248. } else {
  2249. this.cleanupPlayer();
  2250. }
  2251. },
  2252. /**
  2253. * Reset the setting to the default value.
  2254. */
  2255. restoreDefault: function( event ) {
  2256. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2257. return;
  2258. }
  2259. event.preventDefault();
  2260. this.params.attachment = this.params.defaultAttachment;
  2261. this.setting( this.params.defaultAttachment.url );
  2262. },
  2263. /**
  2264. * Called when the "Remove" link is clicked. Empties the setting.
  2265. *
  2266. * @param {object} event jQuery Event object
  2267. */
  2268. removeFile: function( event ) {
  2269. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2270. return;
  2271. }
  2272. event.preventDefault();
  2273. this.params.attachment = {};
  2274. this.setting( '' );
  2275. this.renderContent(); // Not bound to setting change when emptying.
  2276. }
  2277. });
  2278. /**
  2279. * An upload control, which utilizes the media modal.
  2280. *
  2281. * @class
  2282. * @augments wp.customize.MediaControl
  2283. * @augments wp.customize.Control
  2284. * @augments wp.customize.Class
  2285. */
  2286. api.UploadControl = api.MediaControl.extend({
  2287. /**
  2288. * Callback handler for when an attachment is selected in the media modal.
  2289. * Gets the selected image information, and sets it within the control.
  2290. */
  2291. select: function() {
  2292. // Get the attachment from the modal frame.
  2293. var node,
  2294. attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  2295. mejsSettings = window._wpmejsSettings || {};
  2296. this.params.attachment = attachment;
  2297. // Set the Customizer setting; the callback takes care of rendering.
  2298. this.setting( attachment.url );
  2299. node = this.container.find( 'audio, video' ).get(0);
  2300. // Initialize audio/video previews.
  2301. if ( node ) {
  2302. this.player = new MediaElementPlayer( node, mejsSettings );
  2303. } else {
  2304. this.cleanupPlayer();
  2305. }
  2306. },
  2307. // @deprecated
  2308. success: function() {},
  2309. // @deprecated
  2310. removerVisibility: function() {}
  2311. });
  2312. /**
  2313. * A control for uploading images.
  2314. *
  2315. * This control no longer needs to do anything more
  2316. * than what the upload control does in JS.
  2317. *
  2318. * @class
  2319. * @augments wp.customize.UploadControl
  2320. * @augments wp.customize.MediaControl
  2321. * @augments wp.customize.Control
  2322. * @augments wp.customize.Class
  2323. */
  2324. api.ImageControl = api.UploadControl.extend({
  2325. // @deprecated
  2326. thumbnailSrc: function() {}
  2327. });
  2328. /**
  2329. * A control for uploading background images.
  2330. *
  2331. * @class
  2332. * @augments wp.customize.UploadControl
  2333. * @augments wp.customize.MediaControl
  2334. * @augments wp.customize.Control
  2335. * @augments wp.customize.Class
  2336. */
  2337. api.BackgroundControl = api.UploadControl.extend({
  2338. /**
  2339. * When the control's DOM structure is ready,
  2340. * set up internal event bindings.
  2341. */
  2342. ready: function() {
  2343. api.UploadControl.prototype.ready.apply( this, arguments );
  2344. },
  2345. /**
  2346. * Callback handler for when an attachment is selected in the media modal.
  2347. * Does an additional AJAX request for setting the background context.
  2348. */
  2349. select: function() {
  2350. api.UploadControl.prototype.select.apply( this, arguments );
  2351. wp.ajax.post( 'custom-background-add', {
  2352. nonce: _wpCustomizeBackground.nonces.add,
  2353. wp_customize: 'on',
  2354. customize_theme: api.settings.theme.stylesheet,
  2355. attachment_id: this.params.attachment.id
  2356. } );
  2357. }
  2358. });
  2359. /**
  2360. * A control for positioning a background image.
  2361. *
  2362. * @since 4.7.0
  2363. *
  2364. * @class
  2365. * @augments wp.customize.Control
  2366. * @augments wp.customize.Class
  2367. */
  2368. api.BackgroundPositionControl = api.Control.extend( {
  2369. /**
  2370. * Set up control UI once embedded in DOM and settings are created.
  2371. *
  2372. * @since 4.7.0
  2373. * @access public
  2374. */
  2375. ready: function() {
  2376. var control = this, updateRadios;
  2377. control.container.on( 'change', 'input[name="background-position"]', function() {
  2378. var position = $( this ).val().split( ' ' );
  2379. control.settings.x( position[0] );
  2380. control.settings.y( position[1] );
  2381. } );
  2382. updateRadios = _.debounce( function() {
  2383. var x, y, radioInput, inputValue;
  2384. x = control.settings.x.get();
  2385. y = control.settings.y.get();
  2386. inputValue = String( x ) + ' ' + String( y );
  2387. radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
  2388. radioInput.click();
  2389. } );
  2390. control.settings.x.bind( updateRadios );
  2391. control.settings.y.bind( updateRadios );
  2392. updateRadios(); // Set initial UI.
  2393. }
  2394. } );
  2395. /**
  2396. * A control for selecting and cropping an image.
  2397. *
  2398. * @class
  2399. * @augments wp.customize.MediaControl
  2400. * @augments wp.customize.Control
  2401. * @augments wp.customize.Class
  2402. */
  2403. api.CroppedImageControl = api.MediaControl.extend({
  2404. /**
  2405. * Open the media modal to the library state.
  2406. */
  2407. openFrame: function( event ) {
  2408. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2409. return;
  2410. }
  2411. this.initFrame();
  2412. this.frame.setState( 'library' ).open();
  2413. },
  2414. /**
  2415. * Create a media modal select frame, and store it so the instance can be reused when needed.
  2416. */
  2417. initFrame: function() {
  2418. var l10n = _wpMediaViewsL10n;
  2419. this.frame = wp.media({
  2420. button: {
  2421. text: l10n.select,
  2422. close: false
  2423. },
  2424. states: [
  2425. new wp.media.controller.Library({
  2426. title: this.params.button_labels.frame_title,
  2427. library: wp.media.query({ type: 'image' }),
  2428. multiple: false,
  2429. date: false,
  2430. priority: 20,
  2431. suggestedWidth: this.params.width,
  2432. suggestedHeight: this.params.height
  2433. }),
  2434. new wp.media.controller.CustomizeImageCropper({
  2435. imgSelectOptions: this.calculateImageSelectOptions,
  2436. control: this
  2437. })
  2438. ]
  2439. });
  2440. this.frame.on( 'select', this.onSelect, this );
  2441. this.frame.on( 'cropped', this.onCropped, this );
  2442. this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  2443. },
  2444. /**
  2445. * After an image is selected in the media modal, switch to the cropper
  2446. * state if the image isn't the right size.
  2447. */
  2448. onSelect: function() {
  2449. var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  2450. if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  2451. this.setImageFromAttachment( attachment );
  2452. this.frame.close();
  2453. } else {
  2454. this.frame.setState( 'cropper' );
  2455. }
  2456. },
  2457. /**
  2458. * After the image has been cropped, apply the cropped image data to the setting.
  2459. *
  2460. * @param {object} croppedImage Cropped attachment data.
  2461. */
  2462. onCropped: function( croppedImage ) {
  2463. this.setImageFromAttachment( croppedImage );
  2464. },
  2465. /**
  2466. * Returns a set of options, computed from the attached image data and
  2467. * control-specific data, to be fed to the imgAreaSelect plugin in
  2468. * wp.media.view.Cropper.
  2469. *
  2470. * @param {wp.media.model.Attachment} attachment
  2471. * @param {wp.media.controller.Cropper} controller
  2472. * @returns {Object} Options
  2473. */
  2474. calculateImageSelectOptions: function( attachment, controller ) {
  2475. var control = controller.get( 'control' ),
  2476. flexWidth = !! parseInt( control.params.flex_width, 10 ),
  2477. flexHeight = !! parseInt( control.params.flex_height, 10 ),
  2478. realWidth = attachment.get( 'width' ),
  2479. realHeight = attachment.get( 'height' ),
  2480. xInit = parseInt( control.params.width, 10 ),
  2481. yInit = parseInt( control.params.height, 10 ),
  2482. ratio = xInit / yInit,
  2483. xImg = xInit,
  2484. yImg = yInit,
  2485. x1, y1, imgSelectOptions;
  2486. controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
  2487. if ( realWidth / realHeight > ratio ) {
  2488. yInit = realHeight;
  2489. xInit = yInit * ratio;
  2490. } else {
  2491. xInit = realWidth;
  2492. yInit = xInit / ratio;
  2493. }
  2494. x1 = ( realWidth - xInit ) / 2;
  2495. y1 = ( realHeight - yInit ) / 2;
  2496. imgSelectOptions = {
  2497. handles: true,
  2498. keys: true,
  2499. instance: true,
  2500. persistent: true,
  2501. imageWidth: realWidth,
  2502. imageHeight: realHeight,
  2503. minWidth: xImg > xInit ? xInit : xImg,
  2504. minHeight: yImg > yInit ? yInit : yImg,
  2505. x1: x1,
  2506. y1: y1,
  2507. x2: xInit + x1,
  2508. y2: yInit + y1
  2509. };
  2510. if ( flexHeight === false && flexWidth === false ) {
  2511. imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  2512. }
  2513. if ( true === flexHeight ) {
  2514. delete imgSelectOptions.minHeight;
  2515. imgSelectOptions.maxWidth = realWidth;
  2516. }
  2517. if ( true === flexWidth ) {
  2518. delete imgSelectOptions.minWidth;
  2519. imgSelectOptions.maxHeight = realHeight;
  2520. }
  2521. return imgSelectOptions;
  2522. },
  2523. /**
  2524. * Return whether the image must be cropped, based on required dimensions.
  2525. *
  2526. * @param {bool} flexW
  2527. * @param {bool} flexH
  2528. * @param {int} dstW
  2529. * @param {int} dstH
  2530. * @param {int} imgW
  2531. * @param {int} imgH
  2532. * @return {bool}
  2533. */
  2534. mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
  2535. if ( true === flexW && true === flexH ) {
  2536. return false;
  2537. }
  2538. if ( true === flexW && dstH === imgH ) {
  2539. return false;
  2540. }
  2541. if ( true === flexH && dstW === imgW ) {
  2542. return false;
  2543. }
  2544. if ( dstW === imgW && dstH === imgH ) {
  2545. return false;
  2546. }
  2547. if ( imgW <= dstW ) {
  2548. return false;
  2549. }
  2550. return true;
  2551. },
  2552. /**
  2553. * If cropping was skipped, apply the image data directly to the setting.
  2554. */
  2555. onSkippedCrop: function() {
  2556. var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  2557. this.setImageFromAttachment( attachment );
  2558. },
  2559. /**
  2560. * Updates the setting and re-renders the control UI.
  2561. *
  2562. * @param {object} attachment
  2563. */
  2564. setImageFromAttachment: function( attachment ) {
  2565. this.params.attachment = attachment;
  2566. // Set the Customizer setting; the callback takes care of rendering.
  2567. this.setting( attachment.id );
  2568. }
  2569. });
  2570. /**
  2571. * A control for selecting and cropping Site Icons.
  2572. *
  2573. * @class
  2574. * @augments wp.customize.CroppedImageControl
  2575. * @augments wp.customize.MediaControl
  2576. * @augments wp.customize.Control
  2577. * @augments wp.customize.Class
  2578. */
  2579. api.SiteIconControl = api.CroppedImageControl.extend({
  2580. /**
  2581. * Create a media modal select frame, and store it so the instance can be reused when needed.
  2582. */
  2583. initFrame: function() {
  2584. var l10n = _wpMediaViewsL10n;
  2585. this.frame = wp.media({
  2586. button: {
  2587. text: l10n.select,
  2588. close: false
  2589. },
  2590. states: [
  2591. new wp.media.controller.Library({
  2592. title: this.params.button_labels.frame_title,
  2593. library: wp.media.query({ type: 'image' }),
  2594. multiple: false,
  2595. date: false,
  2596. priority: 20,
  2597. suggestedWidth: this.params.width,
  2598. suggestedHeight: this.params.height
  2599. }),
  2600. new wp.media.controller.SiteIconCropper({
  2601. imgSelectOptions: this.calculateImageSelectOptions,
  2602. control: this
  2603. })
  2604. ]
  2605. });
  2606. this.frame.on( 'select', this.onSelect, this );
  2607. this.frame.on( 'cropped', this.onCropped, this );
  2608. this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  2609. },
  2610. /**
  2611. * After an image is selected in the media modal, switch to the cropper
  2612. * state if the image isn't the right size.
  2613. */
  2614. onSelect: function() {
  2615. var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  2616. controller = this;
  2617. if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  2618. wp.ajax.post( 'crop-image', {
  2619. nonce: attachment.nonces.edit,
  2620. id: attachment.id,
  2621. context: 'site-icon',
  2622. cropDetails: {
  2623. x1: 0,
  2624. y1: 0,
  2625. width: this.params.width,
  2626. height: this.params.height,
  2627. dst_width: this.params.width,
  2628. dst_height: this.params.height
  2629. }
  2630. } ).done( function( croppedImage ) {
  2631. controller.setImageFromAttachment( croppedImage );
  2632. controller.frame.close();
  2633. } ).fail( function() {
  2634. controller.frame.trigger('content:error:crop');
  2635. } );
  2636. } else {
  2637. this.frame.setState( 'cropper' );
  2638. }
  2639. },
  2640. /**
  2641. * Updates the setting and re-renders the control UI.
  2642. *
  2643. * @param {object} attachment
  2644. */
  2645. setImageFromAttachment: function( attachment ) {
  2646. var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
  2647. icon;
  2648. _.each( sizes, function( size ) {
  2649. if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
  2650. icon = attachment.sizes[ size ];
  2651. }
  2652. } );
  2653. this.params.attachment = attachment;
  2654. // Set the Customizer setting; the callback takes care of rendering.
  2655. this.setting( attachment.id );
  2656. if ( ! icon ) {
  2657. return;
  2658. }
  2659. // Update the icon in-browser.
  2660. link = $( 'link[rel="icon"][sizes="32x32"]' );
  2661. link.attr( 'href', icon.url );
  2662. },
  2663. /**
  2664. * Called when the "Remove" link is clicked. Empties the setting.
  2665. *
  2666. * @param {object} event jQuery Event object
  2667. */
  2668. removeFile: function( event ) {
  2669. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2670. return;
  2671. }
  2672. event.preventDefault();
  2673. this.params.attachment = {};
  2674. this.setting( '' );
  2675. this.renderContent(); // Not bound to setting change when emptying.
  2676. $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
  2677. }
  2678. });
  2679. /**
  2680. * @class
  2681. * @augments wp.customize.Control
  2682. * @augments wp.customize.Class
  2683. */
  2684. api.HeaderControl = api.Control.extend({
  2685. ready: function() {
  2686. this.btnRemove = $('#customize-control-header_image .actions .remove');
  2687. this.btnNew = $('#customize-control-header_image .actions .new');
  2688. _.bindAll(this, 'openMedia', 'removeImage');
  2689. this.btnNew.on( 'click', this.openMedia );
  2690. this.btnRemove.on( 'click', this.removeImage );
  2691. api.HeaderTool.currentHeader = this.getInitialHeaderImage();
  2692. new api.HeaderTool.CurrentView({
  2693. model: api.HeaderTool.currentHeader,
  2694. el: '#customize-control-header_image .current .container'
  2695. });
  2696. new api.HeaderTool.ChoiceListView({
  2697. collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
  2698. el: '#customize-control-header_image .choices .uploaded .list'
  2699. });
  2700. new api.HeaderTool.ChoiceListView({
  2701. collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
  2702. el: '#customize-control-header_image .choices .default .list'
  2703. });
  2704. api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
  2705. api.HeaderTool.UploadsList,
  2706. api.HeaderTool.DefaultsList
  2707. ]);
  2708. // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
  2709. wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
  2710. wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
  2711. },
  2712. /**
  2713. * Returns a new instance of api.HeaderTool.ImageModel based on the currently
  2714. * saved header image (if any).
  2715. *
  2716. * @since 4.2.0
  2717. *
  2718. * @returns {Object} Options
  2719. */
  2720. getInitialHeaderImage: function() {
  2721. if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
  2722. return new api.HeaderTool.ImageModel();
  2723. }
  2724. // Get the matching uploaded image object.
  2725. var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
  2726. return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
  2727. } );
  2728. // Fall back to raw current header image.
  2729. if ( ! currentHeaderObject ) {
  2730. currentHeaderObject = {
  2731. url: api.get().header_image,
  2732. thumbnail_url: api.get().header_image,
  2733. attachment_id: api.get().header_image_data.attachment_id
  2734. };
  2735. }
  2736. return new api.HeaderTool.ImageModel({
  2737. header: currentHeaderObject,
  2738. choice: currentHeaderObject.url.split( '/' ).pop()
  2739. });
  2740. },
  2741. /**
  2742. * Returns a set of options, computed from the attached image data and
  2743. * theme-specific data, to be fed to the imgAreaSelect plugin in
  2744. * wp.media.view.Cropper.
  2745. *
  2746. * @param {wp.media.model.Attachment} attachment
  2747. * @param {wp.media.controller.Cropper} controller
  2748. * @returns {Object} Options
  2749. */
  2750. calculateImageSelectOptions: function(attachment, controller) {
  2751. var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
  2752. yInit = parseInt(_wpCustomizeHeader.data.height, 10),
  2753. flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
  2754. flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
  2755. ratio, xImg, yImg, realHeight, realWidth,
  2756. imgSelectOptions;
  2757. realWidth = attachment.get('width');
  2758. realHeight = attachment.get('height');
  2759. this.headerImage = new api.HeaderTool.ImageModel();
  2760. this.headerImage.set({
  2761. themeWidth: xInit,
  2762. themeHeight: yInit,
  2763. themeFlexWidth: flexWidth,
  2764. themeFlexHeight: flexHeight,
  2765. imageWidth: realWidth,
  2766. imageHeight: realHeight
  2767. });
  2768. controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
  2769. ratio = xInit / yInit;
  2770. xImg = realWidth;
  2771. yImg = realHeight;
  2772. if ( xImg / yImg > ratio ) {
  2773. yInit = yImg;
  2774. xInit = yInit * ratio;
  2775. } else {
  2776. xInit = xImg;
  2777. yInit = xInit / ratio;
  2778. }
  2779. imgSelectOptions = {
  2780. handles: true,
  2781. keys: true,
  2782. instance: true,
  2783. persistent: true,
  2784. imageWidth: realWidth,
  2785. imageHeight: realHeight,
  2786. x1: 0,
  2787. y1: 0,
  2788. x2: xInit,
  2789. y2: yInit
  2790. };
  2791. if (flexHeight === false && flexWidth === false) {
  2792. imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  2793. }
  2794. if (flexHeight === false ) {
  2795. imgSelectOptions.maxHeight = yInit;
  2796. }
  2797. if (flexWidth === false ) {
  2798. imgSelectOptions.maxWidth = xInit;
  2799. }
  2800. return imgSelectOptions;
  2801. },
  2802. /**
  2803. * Sets up and opens the Media Manager in order to select an image.
  2804. * Depending on both the size of the image and the properties of the
  2805. * current theme, a cropping step after selection may be required or
  2806. * skippable.
  2807. *
  2808. * @param {event} event
  2809. */
  2810. openMedia: function(event) {
  2811. var l10n = _wpMediaViewsL10n;
  2812. event.preventDefault();
  2813. this.frame = wp.media({
  2814. button: {
  2815. text: l10n.selectAndCrop,
  2816. close: false
  2817. },
  2818. states: [
  2819. new wp.media.controller.Library({
  2820. title: l10n.chooseImage,
  2821. library: wp.media.query({ type: 'image' }),
  2822. multiple: false,
  2823. date: false,
  2824. priority: 20,
  2825. suggestedWidth: _wpCustomizeHeader.data.width,
  2826. suggestedHeight: _wpCustomizeHeader.data.height
  2827. }),
  2828. new wp.media.controller.Cropper({
  2829. imgSelectOptions: this.calculateImageSelectOptions
  2830. })
  2831. ]
  2832. });
  2833. this.frame.on('select', this.onSelect, this);
  2834. this.frame.on('cropped', this.onCropped, this);
  2835. this.frame.on('skippedcrop', this.onSkippedCrop, this);
  2836. this.frame.open();
  2837. },
  2838. /**
  2839. * After an image is selected in the media modal,
  2840. * switch to the cropper state.
  2841. */
  2842. onSelect: function() {
  2843. this.frame.setState('cropper');
  2844. },
  2845. /**
  2846. * After the image has been cropped, apply the cropped image data to the setting.
  2847. *
  2848. * @param {object} croppedImage Cropped attachment data.
  2849. */
  2850. onCropped: function(croppedImage) {
  2851. var url = croppedImage.url,
  2852. attachmentId = croppedImage.attachment_id,
  2853. w = croppedImage.width,
  2854. h = croppedImage.height;
  2855. this.setImageFromURL(url, attachmentId, w, h);
  2856. },
  2857. /**
  2858. * If cropping was skipped, apply the image data directly to the setting.
  2859. *
  2860. * @param {object} selection
  2861. */
  2862. onSkippedCrop: function(selection) {
  2863. var url = selection.get('url'),
  2864. w = selection.get('width'),
  2865. h = selection.get('height');
  2866. this.setImageFromURL(url, selection.id, w, h);
  2867. },
  2868. /**
  2869. * Creates a new wp.customize.HeaderTool.ImageModel from provided
  2870. * header image data and inserts it into the user-uploaded headers
  2871. * collection.
  2872. *
  2873. * @param {String} url
  2874. * @param {Number} attachmentId
  2875. * @param {Number} width
  2876. * @param {Number} height
  2877. */
  2878. setImageFromURL: function(url, attachmentId, width, height) {
  2879. var choice, data = {};
  2880. data.url = url;
  2881. data.thumbnail_url = url;
  2882. data.timestamp = _.now();
  2883. if (attachmentId) {
  2884. data.attachment_id = attachmentId;
  2885. }
  2886. if (width) {
  2887. data.width = width;
  2888. }
  2889. if (height) {
  2890. data.height = height;
  2891. }
  2892. choice = new api.HeaderTool.ImageModel({
  2893. header: data,
  2894. choice: url.split('/').pop()
  2895. });
  2896. api.HeaderTool.UploadsList.add(choice);
  2897. api.HeaderTool.currentHeader.set(choice.toJSON());
  2898. choice.save();
  2899. choice.importImage();
  2900. },
  2901. /**
  2902. * Triggers the necessary events to deselect an image which was set as
  2903. * the currently selected one.
  2904. */
  2905. removeImage: function() {
  2906. api.HeaderTool.currentHeader.trigger('hide');
  2907. api.HeaderTool.CombinedList.trigger('control:removeImage');
  2908. }
  2909. });
  2910. /**
  2911. * wp.customize.ThemeControl
  2912. *
  2913. * @constructor
  2914. * @augments wp.customize.Control
  2915. * @augments wp.customize.Class
  2916. */
  2917. api.ThemeControl = api.Control.extend({
  2918. touchDrag: false,
  2919. isRendered: false,
  2920. /**
  2921. * Defer rendering the theme control until the section is displayed.
  2922. *
  2923. * @since 4.2.0
  2924. */
  2925. renderContent: function () {
  2926. var control = this,
  2927. renderContentArgs = arguments;
  2928. api.section( control.section(), function( section ) {
  2929. if ( section.expanded() ) {
  2930. api.Control.prototype.renderContent.apply( control, renderContentArgs );
  2931. control.isRendered = true;
  2932. } else {
  2933. section.expanded.bind( function( expanded ) {
  2934. if ( expanded && ! control.isRendered ) {
  2935. api.Control.prototype.renderContent.apply( control, renderContentArgs );
  2936. control.isRendered = true;
  2937. }
  2938. } );
  2939. }
  2940. } );
  2941. },
  2942. /**
  2943. * @since 4.2.0
  2944. */
  2945. ready: function() {
  2946. var control = this;
  2947. control.container.on( 'touchmove', '.theme', function() {
  2948. control.touchDrag = true;
  2949. });
  2950. // Bind details view trigger.
  2951. control.container.on( 'click keydown touchend', '.theme', function( event ) {
  2952. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2953. return;
  2954. }
  2955. // Bail if the user scrolled on a touch device.
  2956. if ( control.touchDrag === true ) {
  2957. return control.touchDrag = false;
  2958. }
  2959. // Prevent the modal from showing when the user clicks the action button.
  2960. if ( $( event.target ).is( '.theme-actions .button' ) ) {
  2961. return;
  2962. }
  2963. api.section( control.section() ).loadThemePreview( control.params.theme.id );
  2964. });
  2965. control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
  2966. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2967. return;
  2968. }
  2969. event.preventDefault(); // Keep this AFTER the key filter above
  2970. api.section( control.section() ).showDetails( control.params.theme );
  2971. });
  2972. control.container.on( 'render-screenshot', function() {
  2973. var $screenshot = $( this ).find( 'img' ),
  2974. source = $screenshot.data( 'src' );
  2975. if ( source ) {
  2976. $screenshot.attr( 'src', source );
  2977. }
  2978. });
  2979. },
  2980. /**
  2981. * Show or hide the theme based on the presence of the term in the title, description, and author.
  2982. *
  2983. * @since 4.2.0
  2984. */
  2985. filter: function( term ) {
  2986. var control = this,
  2987. haystack = control.params.theme.name + ' ' +
  2988. control.params.theme.description + ' ' +
  2989. control.params.theme.tags + ' ' +
  2990. control.params.theme.author;
  2991. haystack = haystack.toLowerCase().replace( '-', ' ' );
  2992. if ( -1 !== haystack.search( term ) ) {
  2993. control.activate();
  2994. } else {
  2995. control.deactivate();
  2996. }
  2997. }
  2998. });
  2999. // Change objects contained within the main customize object to Settings.
  3000. api.defaultConstructor = api.Setting;
  3001. // Create the collections for Controls, Sections and Panels.
  3002. api.control = new api.Values({ defaultConstructor: api.Control });
  3003. api.section = new api.Values({ defaultConstructor: api.Section });
  3004. api.panel = new api.Values({ defaultConstructor: api.Panel });
  3005. /**
  3006. * An object that fetches a preview in the background of the document, which
  3007. * allows for seamless replacement of an existing preview.
  3008. *
  3009. * @class
  3010. * @augments wp.customize.Messenger
  3011. * @augments wp.customize.Class
  3012. * @mixes wp.customize.Events
  3013. */
  3014. api.PreviewFrame = api.Messenger.extend({
  3015. sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
  3016. /**
  3017. * Initialize the PreviewFrame.
  3018. *
  3019. * @param {object} params.container
  3020. * @param {object} params.previewUrl
  3021. * @param {object} params.query
  3022. * @param {object} options
  3023. */
  3024. initialize: function( params, options ) {
  3025. var deferred = $.Deferred();
  3026. /*
  3027. * Make the instance of the PreviewFrame the promise object
  3028. * so other objects can easily interact with it.
  3029. */
  3030. deferred.promise( this );
  3031. this.container = params.container;
  3032. $.extend( params, { channel: api.PreviewFrame.uuid() });
  3033. api.Messenger.prototype.initialize.call( this, params, options );
  3034. this.add( 'previewUrl', params.previewUrl );
  3035. this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
  3036. this.run( deferred );
  3037. },
  3038. /**
  3039. * Run the preview request.
  3040. *
  3041. * @param {object} deferred jQuery Deferred object to be resolved with
  3042. * the request.
  3043. */
  3044. run: function( deferred ) {
  3045. var previewFrame = this,
  3046. loaded = false,
  3047. ready = false,
  3048. readyData = null,
  3049. hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
  3050. urlParser,
  3051. params,
  3052. form;
  3053. if ( previewFrame._ready ) {
  3054. previewFrame.unbind( 'ready', previewFrame._ready );
  3055. }
  3056. previewFrame._ready = function( data ) {
  3057. ready = true;
  3058. readyData = data;
  3059. previewFrame.container.addClass( 'iframe-ready' );
  3060. if ( ! data ) {
  3061. return;
  3062. }
  3063. if ( loaded ) {
  3064. deferred.resolveWith( previewFrame, [ data ] );
  3065. }
  3066. };
  3067. previewFrame.bind( 'ready', previewFrame._ready );
  3068. urlParser = document.createElement( 'a' );
  3069. urlParser.href = previewFrame.previewUrl();
  3070. params = _.extend(
  3071. api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  3072. {
  3073. customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
  3074. customize_theme: previewFrame.query.customize_theme,
  3075. customize_messenger_channel: previewFrame.query.customize_messenger_channel
  3076. }
  3077. );
  3078. urlParser.search = $.param( params );
  3079. previewFrame.iframe = $( '<iframe />', {
  3080. title: api.l10n.previewIframeTitle,
  3081. name: 'customize-' + previewFrame.channel()
  3082. } );
  3083. previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
  3084. if ( ! hasPendingChangesetUpdate ) {
  3085. previewFrame.iframe.attr( 'src', urlParser.href );
  3086. } else {
  3087. previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
  3088. }
  3089. previewFrame.iframe.appendTo( previewFrame.container );
  3090. previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
  3091. /*
  3092. * Submit customized data in POST request to preview frame window since
  3093. * there are setting value changes not yet written to changeset.
  3094. */
  3095. if ( hasPendingChangesetUpdate ) {
  3096. form = $( '<form>', {
  3097. action: urlParser.href,
  3098. target: previewFrame.iframe.attr( 'name' ),
  3099. method: 'post',
  3100. hidden: 'hidden'
  3101. } );
  3102. form.append( $( '<input>', {
  3103. type: 'hidden',
  3104. name: '_method',
  3105. value: 'GET'
  3106. } ) );
  3107. _.each( previewFrame.query, function( value, key ) {
  3108. form.append( $( '<input>', {
  3109. type: 'hidden',
  3110. name: key,
  3111. value: value
  3112. } ) );
  3113. } );
  3114. previewFrame.container.append( form );
  3115. form.submit();
  3116. form.remove(); // No need to keep the form around after submitted.
  3117. }
  3118. previewFrame.bind( 'iframe-loading-error', function( error ) {
  3119. previewFrame.iframe.remove();
  3120. // Check if the user is not logged in.
  3121. if ( 0 === error ) {
  3122. previewFrame.login( deferred );
  3123. return;
  3124. }
  3125. // Check for cheaters.
  3126. if ( -1 === error ) {
  3127. deferred.rejectWith( previewFrame, [ 'cheatin' ] );
  3128. return;
  3129. }
  3130. deferred.rejectWith( previewFrame, [ 'request failure' ] );
  3131. } );
  3132. previewFrame.iframe.one( 'load', function() {
  3133. loaded = true;
  3134. if ( ready ) {
  3135. deferred.resolveWith( previewFrame, [ readyData ] );
  3136. } else {
  3137. setTimeout( function() {
  3138. deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
  3139. }, previewFrame.sensitivity );
  3140. }
  3141. });
  3142. },
  3143. login: function( deferred ) {
  3144. var self = this,
  3145. reject;
  3146. reject = function() {
  3147. deferred.rejectWith( self, [ 'logged out' ] );
  3148. };
  3149. if ( this.triedLogin ) {
  3150. return reject();
  3151. }
  3152. // Check if we have an admin cookie.
  3153. $.get( api.settings.url.ajax, {
  3154. action: 'logged-in'
  3155. }).fail( reject ).done( function( response ) {
  3156. var iframe;
  3157. if ( '1' !== response ) {
  3158. reject();
  3159. }
  3160. iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
  3161. iframe.appendTo( self.container );
  3162. iframe.on( 'load', function() {
  3163. self.triedLogin = true;
  3164. iframe.remove();
  3165. self.run( deferred );
  3166. });
  3167. });
  3168. },
  3169. destroy: function() {
  3170. api.Messenger.prototype.destroy.call( this );
  3171. if ( this.iframe ) {
  3172. this.iframe.remove();
  3173. }
  3174. delete this.iframe;
  3175. delete this.targetWindow;
  3176. }
  3177. });
  3178. (function(){
  3179. var id = 0;
  3180. /**
  3181. * Return an incremented ID for a preview messenger channel.
  3182. *
  3183. * This function is named "uuid" for historical reasons, but it is a
  3184. * misnomer as it is not an actual UUID, and it is not universally unique.
  3185. * This is not to be confused with `api.settings.changeset.uuid`.
  3186. *
  3187. * @return {string}
  3188. */
  3189. api.PreviewFrame.uuid = function() {
  3190. return 'preview-' + String( id++ );
  3191. };
  3192. }());
  3193. /**
  3194. * Set the document title of the customizer.
  3195. *
  3196. * @since 4.1.0
  3197. *
  3198. * @param {string} documentTitle
  3199. */
  3200. api.setDocumentTitle = function ( documentTitle ) {
  3201. var tmpl, title;
  3202. tmpl = api.settings.documentTitleTmpl;
  3203. title = tmpl.replace( '%s', documentTitle );
  3204. document.title = title;
  3205. api.trigger( 'title', title );
  3206. };
  3207. /**
  3208. * @class
  3209. * @augments wp.customize.Messenger
  3210. * @augments wp.customize.Class
  3211. * @mixes wp.customize.Events
  3212. */
  3213. api.Previewer = api.Messenger.extend({
  3214. refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
  3215. /**
  3216. * @param {array} params.allowedUrls
  3217. * @param {string} params.container A selector or jQuery element for the preview
  3218. * frame to be placed.
  3219. * @param {string} params.form
  3220. * @param {string} params.previewUrl The URL to preview.
  3221. * @param {object} options
  3222. */
  3223. initialize: function( params, options ) {
  3224. var previewer = this,
  3225. urlParser = document.createElement( 'a' );
  3226. $.extend( previewer, options || {} );
  3227. previewer.deferred = {
  3228. active: $.Deferred()
  3229. };
  3230. // Debounce to prevent hammering server and then wait for any pending update requests.
  3231. previewer.refresh = _.debounce(
  3232. ( function( originalRefresh ) {
  3233. return function() {
  3234. var isProcessingComplete, refreshOnceProcessingComplete;
  3235. isProcessingComplete = function() {
  3236. return 0 === api.state( 'processing' ).get();
  3237. };
  3238. if ( isProcessingComplete() ) {
  3239. originalRefresh.call( previewer );
  3240. } else {
  3241. refreshOnceProcessingComplete = function() {
  3242. if ( isProcessingComplete() ) {
  3243. originalRefresh.call( previewer );
  3244. api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
  3245. }
  3246. };
  3247. api.state( 'processing' ).bind( refreshOnceProcessingComplete );
  3248. }
  3249. };
  3250. }( previewer.refresh ) ),
  3251. previewer.refreshBuffer
  3252. );
  3253. previewer.container = api.ensure( params.container );
  3254. previewer.allowedUrls = params.allowedUrls;
  3255. params.url = window.location.href;
  3256. api.Messenger.prototype.initialize.call( previewer, params );
  3257. urlParser.href = previewer.origin();
  3258. previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
  3259. // Limit the URL to internal, front-end links.
  3260. //
  3261. // If the front end and the admin are served from the same domain, load the
  3262. // preview over ssl if the Customizer is being loaded over ssl. This avoids
  3263. // insecure content warnings. This is not attempted if the admin and front end
  3264. // are on different domains to avoid the case where the front end doesn't have
  3265. // ssl certs.
  3266. previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
  3267. var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
  3268. urlParser = document.createElement( 'a' );
  3269. urlParser.href = to;
  3270. // Abort if URL is for admin or (static) files in wp-includes or wp-content.
  3271. if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
  3272. return null;
  3273. }
  3274. // Remove state query params.
  3275. if ( urlParser.search.length > 1 ) {
  3276. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  3277. delete queryParams.customize_changeset_uuid;
  3278. delete queryParams.customize_theme;
  3279. delete queryParams.customize_messenger_channel;
  3280. if ( _.isEmpty( queryParams ) ) {
  3281. urlParser.search = '';
  3282. } else {
  3283. urlParser.search = $.param( queryParams );
  3284. }
  3285. }
  3286. parsedCandidateUrls.push( urlParser );
  3287. // Prepend list with URL that matches the scheme/protocol of the iframe.
  3288. if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
  3289. urlParser = document.createElement( 'a' );
  3290. urlParser.href = parsedCandidateUrls[0].href;
  3291. urlParser.protocol = previewer.scheme.get() + ':';
  3292. parsedCandidateUrls.unshift( urlParser );
  3293. }
  3294. // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
  3295. parsedAllowedUrl = document.createElement( 'a' );
  3296. _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
  3297. return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
  3298. parsedAllowedUrl.href = allowedUrl;
  3299. if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
  3300. result = parsedCandidateUrl.href;
  3301. return true;
  3302. }
  3303. } ) );
  3304. } );
  3305. return result;
  3306. });
  3307. previewer.bind( 'ready', previewer.ready );
  3308. // Start listening for keep-alive messages when iframe first loads.
  3309. previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
  3310. previewer.bind( 'synced', function() {
  3311. previewer.send( 'active' );
  3312. } );
  3313. // Refresh the preview when the URL is changed (but not yet).
  3314. previewer.previewUrl.bind( previewer.refresh );
  3315. previewer.scroll = 0;
  3316. previewer.bind( 'scroll', function( distance ) {
  3317. previewer.scroll = distance;
  3318. });
  3319. // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
  3320. previewer.bind( 'url', function( url ) {
  3321. var onUrlChange, urlChanged = false;
  3322. previewer.scroll = 0;
  3323. onUrlChange = function() {
  3324. urlChanged = true;
  3325. };
  3326. previewer.previewUrl.bind( onUrlChange );
  3327. previewer.previewUrl.set( url );
  3328. previewer.previewUrl.unbind( onUrlChange );
  3329. if ( ! urlChanged ) {
  3330. previewer.refresh();
  3331. }
  3332. } );
  3333. // Update the document title when the preview changes.
  3334. previewer.bind( 'documentTitle', function ( title ) {
  3335. api.setDocumentTitle( title );
  3336. } );
  3337. },
  3338. /**
  3339. * Handle the preview receiving the ready message.
  3340. *
  3341. * @since 4.7.0
  3342. * @access public
  3343. *
  3344. * @param {object} data - Data from preview.
  3345. * @param {string} data.currentUrl - Current URL.
  3346. * @param {object} data.activePanels - Active panels.
  3347. * @param {object} data.activeSections Active sections.
  3348. * @param {object} data.activeControls Active controls.
  3349. * @returns {void}
  3350. */
  3351. ready: function( data ) {
  3352. var previewer = this, synced = {}, constructs;
  3353. synced.settings = api.get();
  3354. synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
  3355. if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
  3356. synced.scroll = previewer.scroll;
  3357. }
  3358. synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
  3359. previewer.send( 'sync', synced );
  3360. // Set the previewUrl without causing the url to set the iframe.
  3361. if ( data.currentUrl ) {
  3362. previewer.previewUrl.unbind( previewer.refresh );
  3363. previewer.previewUrl.set( data.currentUrl );
  3364. previewer.previewUrl.bind( previewer.refresh );
  3365. }
  3366. /*
  3367. * Walk over all panels, sections, and controls and set their
  3368. * respective active states to true if the preview explicitly
  3369. * indicates as such.
  3370. */
  3371. constructs = {
  3372. panel: data.activePanels,
  3373. section: data.activeSections,
  3374. control: data.activeControls
  3375. };
  3376. _( constructs ).each( function ( activeConstructs, type ) {
  3377. api[ type ].each( function ( construct, id ) {
  3378. var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
  3379. /*
  3380. * If the construct was created statically in PHP (not dynamically in JS)
  3381. * then consider a missing (undefined) value in the activeConstructs to
  3382. * mean it should be deactivated (since it is gone). But if it is
  3383. * dynamically created then only toggle activation if the value is defined,
  3384. * as this means that the construct was also then correspondingly
  3385. * created statically in PHP and the active callback is available.
  3386. * Otherwise, dynamically-created constructs should normally have
  3387. * their active states toggled in JS rather than from PHP.
  3388. */
  3389. if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
  3390. if ( activeConstructs[ id ] ) {
  3391. construct.activate();
  3392. } else {
  3393. construct.deactivate();
  3394. }
  3395. }
  3396. } );
  3397. } );
  3398. if ( data.settingValidities ) {
  3399. api._handleSettingValidities( {
  3400. settingValidities: data.settingValidities,
  3401. focusInvalidControl: false
  3402. } );
  3403. }
  3404. },
  3405. /**
  3406. * Keep the preview alive by listening for ready and keep-alive messages.
  3407. *
  3408. * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
  3409. *
  3410. * @since 4.7.0
  3411. * @access public
  3412. *
  3413. * @returns {void}
  3414. */
  3415. keepPreviewAlive: function keepPreviewAlive() {
  3416. var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
  3417. /**
  3418. * Schedule a preview keep-alive check.
  3419. *
  3420. * Note that if a page load takes longer than keepAliveCheck milliseconds,
  3421. * the keep-alive messages will still be getting sent from the previous
  3422. * URL.
  3423. */
  3424. scheduleKeepAliveCheck = function() {
  3425. timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
  3426. };
  3427. /**
  3428. * Set the previewerAlive state to true when receiving a message from the preview.
  3429. */
  3430. keepAliveTick = function() {
  3431. api.state( 'previewerAlive' ).set( true );
  3432. clearTimeout( timeoutId );
  3433. scheduleKeepAliveCheck();
  3434. };
  3435. /**
  3436. * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
  3437. *
  3438. * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
  3439. * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
  3440. * transport to use refresh instead, causing the preview frame also to be replaced with the current
  3441. * allowed preview URL.
  3442. */
  3443. handleMissingKeepAlive = function() {
  3444. api.state( 'previewerAlive' ).set( false );
  3445. };
  3446. scheduleKeepAliveCheck();
  3447. previewer.bind( 'ready', keepAliveTick );
  3448. previewer.bind( 'keep-alive', keepAliveTick );
  3449. },
  3450. /**
  3451. * Query string data sent with each preview request.
  3452. *
  3453. * @abstract
  3454. */
  3455. query: function() {},
  3456. abort: function() {
  3457. if ( this.loading ) {
  3458. this.loading.destroy();
  3459. delete this.loading;
  3460. }
  3461. },
  3462. /**
  3463. * Refresh the preview seamlessly.
  3464. *
  3465. * @since 3.4.0
  3466. * @access public
  3467. * @returns {void}
  3468. */
  3469. refresh: function() {
  3470. var previewer = this, onSettingChange;
  3471. // Display loading indicator
  3472. previewer.send( 'loading-initiated' );
  3473. previewer.abort();
  3474. previewer.loading = new api.PreviewFrame({
  3475. url: previewer.url(),
  3476. previewUrl: previewer.previewUrl(),
  3477. query: previewer.query( { excludeCustomizedSaved: true } ) || {},
  3478. container: previewer.container
  3479. });
  3480. previewer.settingsModifiedWhileLoading = {};
  3481. onSettingChange = function( setting ) {
  3482. previewer.settingsModifiedWhileLoading[ setting.id ] = true;
  3483. };
  3484. api.bind( 'change', onSettingChange );
  3485. previewer.loading.always( function() {
  3486. api.unbind( 'change', onSettingChange );
  3487. } );
  3488. previewer.loading.done( function( readyData ) {
  3489. var loadingFrame = this, onceSynced;
  3490. previewer.preview = loadingFrame;
  3491. previewer.targetWindow( loadingFrame.targetWindow() );
  3492. previewer.channel( loadingFrame.channel() );
  3493. onceSynced = function() {
  3494. loadingFrame.unbind( 'synced', onceSynced );
  3495. if ( previewer._previousPreview ) {
  3496. previewer._previousPreview.destroy();
  3497. }
  3498. previewer._previousPreview = previewer.preview;
  3499. previewer.deferred.active.resolve();
  3500. delete previewer.loading;
  3501. };
  3502. loadingFrame.bind( 'synced', onceSynced );
  3503. // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
  3504. previewer.trigger( 'ready', readyData );
  3505. });
  3506. previewer.loading.fail( function( reason ) {
  3507. previewer.send( 'loading-failed' );
  3508. if ( 'logged out' === reason ) {
  3509. if ( previewer.preview ) {
  3510. previewer.preview.destroy();
  3511. delete previewer.preview;
  3512. }
  3513. previewer.login().done( previewer.refresh );
  3514. }
  3515. if ( 'cheatin' === reason ) {
  3516. previewer.cheatin();
  3517. }
  3518. });
  3519. },
  3520. login: function() {
  3521. var previewer = this,
  3522. deferred, messenger, iframe;
  3523. if ( this._login )
  3524. return this._login;
  3525. deferred = $.Deferred();
  3526. this._login = deferred.promise();
  3527. messenger = new api.Messenger({
  3528. channel: 'login',
  3529. url: api.settings.url.login
  3530. });
  3531. iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
  3532. messenger.targetWindow( iframe[0].contentWindow );
  3533. messenger.bind( 'login', function () {
  3534. var refreshNonces = previewer.refreshNonces();
  3535. refreshNonces.always( function() {
  3536. iframe.remove();
  3537. messenger.destroy();
  3538. delete previewer._login;
  3539. });
  3540. refreshNonces.done( function() {
  3541. deferred.resolve();
  3542. });
  3543. refreshNonces.fail( function() {
  3544. previewer.cheatin();
  3545. deferred.reject();
  3546. });
  3547. });
  3548. return this._login;
  3549. },
  3550. cheatin: function() {
  3551. $( document.body ).empty().addClass( 'cheatin' ).append(
  3552. '<h1>' + api.l10n.cheatin + '</h1>' +
  3553. '<p>' + api.l10n.notAllowed + '</p>'
  3554. );
  3555. },
  3556. refreshNonces: function() {
  3557. var request, deferred = $.Deferred();
  3558. deferred.promise();
  3559. request = wp.ajax.post( 'customize_refresh_nonces', {
  3560. wp_customize: 'on',
  3561. customize_theme: api.settings.theme.stylesheet
  3562. });
  3563. request.done( function( response ) {
  3564. api.trigger( 'nonce-refresh', response );
  3565. deferred.resolve();
  3566. });
  3567. request.fail( function() {
  3568. deferred.reject();
  3569. });
  3570. return deferred;
  3571. }
  3572. });
  3573. api.settingConstructor = {};
  3574. api.controlConstructor = {
  3575. color: api.ColorControl,
  3576. media: api.MediaControl,
  3577. upload: api.UploadControl,
  3578. image: api.ImageControl,
  3579. cropped_image: api.CroppedImageControl,
  3580. site_icon: api.SiteIconControl,
  3581. header: api.HeaderControl,
  3582. background: api.BackgroundControl,
  3583. background_position: api.BackgroundPositionControl,
  3584. theme: api.ThemeControl
  3585. };
  3586. api.panelConstructor = {};
  3587. api.sectionConstructor = {
  3588. themes: api.ThemesSection
  3589. };
  3590. /**
  3591. * Handle setting_validities in an error response for the customize-save request.
  3592. *
  3593. * Add notifications to the settings and focus on the first control that has an invalid setting.
  3594. *
  3595. * @since 4.6.0
  3596. * @private
  3597. *
  3598. * @param {object} args
  3599. * @param {object} args.settingValidities
  3600. * @param {boolean} [args.focusInvalidControl=false]
  3601. * @returns {void}
  3602. */
  3603. api._handleSettingValidities = function handleSettingValidities( args ) {
  3604. var invalidSettingControls, invalidSettings = [], wasFocused = false;
  3605. // Find the controls that correspond to each invalid setting.
  3606. _.each( args.settingValidities, function( validity, settingId ) {
  3607. var setting = api( settingId );
  3608. if ( setting ) {
  3609. // Add notifications for invalidities.
  3610. if ( _.isObject( validity ) ) {
  3611. _.each( validity, function( params, code ) {
  3612. var notification, existingNotification, needsReplacement = false;
  3613. notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
  3614. // Remove existing notification if already exists for code but differs in parameters.
  3615. existingNotification = setting.notifications( notification.code );
  3616. if ( existingNotification ) {
  3617. needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
  3618. }
  3619. if ( needsReplacement ) {
  3620. setting.notifications.remove( code );
  3621. }
  3622. if ( ! setting.notifications.has( notification.code ) ) {
  3623. setting.notifications.add( code, notification );
  3624. }
  3625. invalidSettings.push( setting.id );
  3626. } );
  3627. }
  3628. // Remove notification errors that are no longer valid.
  3629. setting.notifications.each( function( notification ) {
  3630. if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
  3631. setting.notifications.remove( notification.code );
  3632. }
  3633. } );
  3634. }
  3635. } );
  3636. if ( args.focusInvalidControl ) {
  3637. invalidSettingControls = api.findControlsForSettings( invalidSettings );
  3638. // Focus on the first control that is inside of an expanded section (one that is visible).
  3639. _( _.values( invalidSettingControls ) ).find( function( controls ) {
  3640. return _( controls ).find( function( control ) {
  3641. var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
  3642. if ( isExpanded && control.expanded ) {
  3643. isExpanded = control.expanded();
  3644. }
  3645. if ( isExpanded ) {
  3646. control.focus();
  3647. wasFocused = true;
  3648. }
  3649. return wasFocused;
  3650. } );
  3651. } );
  3652. // Focus on the first invalid control.
  3653. if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
  3654. _.values( invalidSettingControls )[0][0].focus();
  3655. }
  3656. }
  3657. };
  3658. /**
  3659. * Find all controls associated with the given settings.
  3660. *
  3661. * @since 4.6.0
  3662. * @param {string[]} settingIds Setting IDs.
  3663. * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
  3664. */
  3665. api.findControlsForSettings = function findControlsForSettings( settingIds ) {
  3666. var controls = {}, settingControls;
  3667. _.each( _.unique( settingIds ), function( settingId ) {
  3668. var setting = api( settingId );
  3669. if ( setting ) {
  3670. settingControls = setting.findControls();
  3671. if ( settingControls && settingControls.length > 0 ) {
  3672. controls[ settingId ] = settingControls;
  3673. }
  3674. }
  3675. } );
  3676. return controls;
  3677. };
  3678. /**
  3679. * Sort panels, sections, controls by priorities. Hide empty sections and panels.
  3680. *
  3681. * @since 4.1.0
  3682. */
  3683. api.reflowPaneContents = _.bind( function () {
  3684. var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
  3685. if ( document.activeElement ) {
  3686. activeElement = $( document.activeElement );
  3687. }
  3688. // Sort the sections within each panel
  3689. api.panel.each( function ( panel ) {
  3690. var sections = panel.sections(),
  3691. sectionHeadContainers = _.pluck( sections, 'headContainer' );
  3692. rootNodes.push( panel );
  3693. appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
  3694. if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
  3695. _( sections ).each( function ( section ) {
  3696. appendContainer.append( section.headContainer );
  3697. } );
  3698. wasReflowed = true;
  3699. }
  3700. } );
  3701. // Sort the controls within each section
  3702. api.section.each( function ( section ) {
  3703. var controls = section.controls(),
  3704. controlContainers = _.pluck( controls, 'container' );
  3705. if ( ! section.panel() ) {
  3706. rootNodes.push( section );
  3707. }
  3708. appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  3709. if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
  3710. _( controls ).each( function ( control ) {
  3711. appendContainer.append( control.container );
  3712. } );
  3713. wasReflowed = true;
  3714. }
  3715. } );
  3716. // Sort the root panels and sections
  3717. rootNodes.sort( api.utils.prioritySort );
  3718. rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
  3719. appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  3720. if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
  3721. _( rootNodes ).each( function ( rootNode ) {
  3722. appendContainer.append( rootNode.headContainer );
  3723. } );
  3724. wasReflowed = true;
  3725. }
  3726. // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
  3727. api.panel.each( function ( panel ) {
  3728. var value = panel.active();
  3729. panel.active.callbacks.fireWith( panel.active, [ value, value ] );
  3730. } );
  3731. api.section.each( function ( section ) {
  3732. var value = section.active();
  3733. section.active.callbacks.fireWith( section.active, [ value, value ] );
  3734. } );
  3735. // Restore focus if there was a reflow and there was an active (focused) element
  3736. if ( wasReflowed && activeElement ) {
  3737. activeElement.focus();
  3738. }
  3739. api.trigger( 'pane-contents-reflowed' );
  3740. }, api );
  3741. $( function() {
  3742. api.settings = window._wpCustomizeSettings;
  3743. api.l10n = window._wpCustomizeControlsL10n;
  3744. // Check if we can run the Customizer.
  3745. if ( ! api.settings ) {
  3746. return;
  3747. }
  3748. // Bail if any incompatibilities are found.
  3749. if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
  3750. return;
  3751. }
  3752. if ( null === api.PreviewFrame.prototype.sensitivity ) {
  3753. api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
  3754. }
  3755. if ( null === api.Previewer.prototype.refreshBuffer ) {
  3756. api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
  3757. }
  3758. var parent,
  3759. body = $( document.body ),
  3760. overlay = body.children( '.wp-full-overlay' ),
  3761. title = $( '#customize-info .panel-title.site-title' ),
  3762. closeBtn = $( '.customize-controls-close' ),
  3763. saveBtn = $( '#save' ),
  3764. footerActions = $( '#customize-footer-actions' );
  3765. // Prevent the form from saving when enter is pressed on an input or select element.
  3766. $('#customize-controls').on( 'keydown', function( e ) {
  3767. var isEnter = ( 13 === e.which ),
  3768. $el = $( e.target );
  3769. if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
  3770. e.preventDefault();
  3771. }
  3772. });
  3773. // Expand/Collapse the main customizer customize info.
  3774. $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
  3775. var section = $( this ).closest( '.accordion-section' ),
  3776. content = section.find( '.customize-panel-description:first' );
  3777. if ( section.hasClass( 'cannot-expand' ) ) {
  3778. return;
  3779. }
  3780. if ( section.hasClass( 'open' ) ) {
  3781. section.toggleClass( 'open' );
  3782. content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
  3783. $( this ).attr( 'aria-expanded', false );
  3784. } else {
  3785. content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
  3786. section.toggleClass( 'open' );
  3787. $( this ).attr( 'aria-expanded', true );
  3788. }
  3789. });
  3790. // Initialize Previewer
  3791. api.previewer = new api.Previewer({
  3792. container: '#customize-preview',
  3793. form: '#customize-controls',
  3794. previewUrl: api.settings.url.preview,
  3795. allowedUrls: api.settings.url.allowed
  3796. }, {
  3797. nonce: api.settings.nonce,
  3798. /**
  3799. * Build the query to send along with the Preview request.
  3800. *
  3801. * @since 3.4.0
  3802. * @since 4.7.0 Added options param.
  3803. * @access public
  3804. *
  3805. * @param {object} [options] Options.
  3806. * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
  3807. * @return {object} Query vars.
  3808. */
  3809. query: function( options ) {
  3810. var queryVars = {
  3811. wp_customize: 'on',
  3812. customize_theme: api.settings.theme.stylesheet,
  3813. nonce: this.nonce.preview,
  3814. customize_changeset_uuid: api.settings.changeset.uuid
  3815. };
  3816. /*
  3817. * Exclude customized data if requested especially for calls to requestChangesetUpdate.
  3818. * Changeset updates are differential and so it is a performance waste to send all of
  3819. * the dirty settings with each update.
  3820. */
  3821. queryVars.customized = JSON.stringify( api.dirtyValues( {
  3822. unsaved: options && options.excludeCustomizedSaved
  3823. } ) );
  3824. return queryVars;
  3825. },
  3826. /**
  3827. * Save (and publish) the customizer changeset.
  3828. *
  3829. * Updates to the changeset are transactional. If any of the settings
  3830. * are invalid then none of them will be written into the changeset.
  3831. * A revision will be made for the changeset post if revisions support
  3832. * has been added to the post type.
  3833. *
  3834. * @since 3.4.0
  3835. * @since 4.7.0 Added args param and return value.
  3836. *
  3837. * @param {object} [args] Args.
  3838. * @param {string} [args.status=publish] Status.
  3839. * @param {string} [args.date] Date, in local time in MySQL format.
  3840. * @param {string} [args.title] Title
  3841. * @returns {jQuery.promise} Promise.
  3842. */
  3843. save: function( args ) {
  3844. var previewer = this,
  3845. deferred = $.Deferred(),
  3846. changesetStatus = 'publish',
  3847. processing = api.state( 'processing' ),
  3848. submitWhenDoneProcessing,
  3849. submit,
  3850. modifiedWhileSaving = {},
  3851. invalidSettings = [],
  3852. invalidControls;
  3853. if ( args && args.status ) {
  3854. changesetStatus = args.status;
  3855. }
  3856. if ( api.state( 'saving' ).get() ) {
  3857. deferred.reject( 'already_saving' );
  3858. deferred.promise();
  3859. }
  3860. api.state( 'saving' ).set( true );
  3861. function captureSettingModifiedDuringSave( setting ) {
  3862. modifiedWhileSaving[ setting.id ] = true;
  3863. }
  3864. submit = function () {
  3865. var request, query, settingInvalidities = {}, latestRevision = api._latestRevision;
  3866. api.bind( 'change', captureSettingModifiedDuringSave );
  3867. /*
  3868. * Block saving if there are any settings that are marked as
  3869. * invalid from the client (not from the server). Focus on
  3870. * the control.
  3871. */
  3872. api.each( function( setting ) {
  3873. setting.notifications.each( function( notification ) {
  3874. if ( 'error' === notification.type && ! notification.fromServer ) {
  3875. invalidSettings.push( setting.id );
  3876. if ( ! settingInvalidities[ setting.id ] ) {
  3877. settingInvalidities[ setting.id ] = {};
  3878. }
  3879. settingInvalidities[ setting.id ][ notification.code ] = notification;
  3880. }
  3881. } );
  3882. } );
  3883. invalidControls = api.findControlsForSettings( invalidSettings );
  3884. if ( ! _.isEmpty( invalidControls ) ) {
  3885. _.values( invalidControls )[0][0].focus();
  3886. api.unbind( 'change', captureSettingModifiedDuringSave );
  3887. deferred.rejectWith( previewer, [
  3888. { setting_invalidities: settingInvalidities }
  3889. ] );
  3890. api.state( 'saving' ).set( false );
  3891. return deferred.promise();
  3892. }
  3893. /*
  3894. * Note that excludeCustomizedSaved is intentionally false so that the entire
  3895. * set of customized data will be included if bypassed changeset update.
  3896. */
  3897. query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
  3898. nonce: previewer.nonce.save,
  3899. customize_changeset_status: changesetStatus
  3900. } );
  3901. if ( args && args.date ) {
  3902. query.customize_changeset_date = args.date;
  3903. }
  3904. if ( args && args.title ) {
  3905. query.customize_changeset_title = args.title;
  3906. }
  3907. /*
  3908. * Note that the dirty customized values will have already been set in the
  3909. * changeset and so technically query.customized could be deleted. However,
  3910. * it is remaining here to make sure that any settings that got updated
  3911. * quietly which may have not triggered an update request will also get
  3912. * included in the values that get saved to the changeset. This will ensure
  3913. * that values that get injected via the saved event will be included in
  3914. * the changeset. This also ensures that setting values that were invalid
  3915. * will get re-validated, perhaps in the case of settings that are invalid
  3916. * due to dependencies on other settings.
  3917. */
  3918. request = wp.ajax.post( 'customize_save', query );
  3919. // Disable save button during the save request.
  3920. saveBtn.prop( 'disabled', true );
  3921. api.trigger( 'save', request );
  3922. request.always( function () {
  3923. api.state( 'saving' ).set( false );
  3924. saveBtn.prop( 'disabled', false );
  3925. api.unbind( 'change', captureSettingModifiedDuringSave );
  3926. } );
  3927. request.fail( function ( response ) {
  3928. if ( '0' === response ) {
  3929. response = 'not_logged_in';
  3930. } else if ( '-1' === response ) {
  3931. // Back-compat in case any other check_ajax_referer() call is dying
  3932. response = 'invalid_nonce';
  3933. }
  3934. if ( 'invalid_nonce' === response ) {
  3935. previewer.cheatin();
  3936. } else if ( 'not_logged_in' === response ) {
  3937. previewer.preview.iframe.hide();
  3938. previewer.login().done( function() {
  3939. previewer.save();
  3940. previewer.preview.iframe.show();
  3941. } );
  3942. }
  3943. if ( response.setting_validities ) {
  3944. api._handleSettingValidities( {
  3945. settingValidities: response.setting_validities,
  3946. focusInvalidControl: true
  3947. } );
  3948. }
  3949. deferred.rejectWith( previewer, [ response ] );
  3950. api.trigger( 'error', response );
  3951. } );
  3952. request.done( function( response ) {
  3953. previewer.send( 'saved', response );
  3954. api.state( 'changesetStatus' ).set( response.changeset_status );
  3955. if ( 'publish' === response.changeset_status ) {
  3956. // Mark all published as clean if they haven't been modified during the request.
  3957. api.each( function( setting ) {
  3958. /*
  3959. * Note that the setting revision will be undefined in the case of setting
  3960. * values that are marked as dirty when the customizer is loaded, such as
  3961. * when applying starter content. All other dirty settings will have an
  3962. * associated revision due to their modification triggering a change event.
  3963. */
  3964. if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
  3965. setting._dirty = false;
  3966. }
  3967. } );
  3968. api.state( 'changesetStatus' ).set( '' );
  3969. api.settings.changeset.uuid = response.next_changeset_uuid;
  3970. parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  3971. }
  3972. if ( response.setting_validities ) {
  3973. api._handleSettingValidities( {
  3974. settingValidities: response.setting_validities,
  3975. focusInvalidControl: true
  3976. } );
  3977. }
  3978. deferred.resolveWith( previewer, [ response ] );
  3979. api.trigger( 'saved', response );
  3980. // Restore the global dirty state if any settings were modified during save.
  3981. if ( ! _.isEmpty( modifiedWhileSaving ) ) {
  3982. api.state( 'saved' ).set( false );
  3983. }
  3984. } );
  3985. };
  3986. if ( 0 === processing() ) {
  3987. submit();
  3988. } else {
  3989. submitWhenDoneProcessing = function () {
  3990. if ( 0 === processing() ) {
  3991. api.state.unbind( 'change', submitWhenDoneProcessing );
  3992. submit();
  3993. }
  3994. };
  3995. api.state.bind( 'change', submitWhenDoneProcessing );
  3996. }
  3997. return deferred.promise();
  3998. }
  3999. });
  4000. // Ensure preview nonce is included with every customized request, to allow post data to be read.
  4001. $.ajaxPrefilter( function injectPreviewNonce( options ) {
  4002. if ( ! /wp_customize=on/.test( options.data ) ) {
  4003. return;
  4004. }
  4005. options.data += '&' + $.param({
  4006. customize_preview_nonce: api.settings.nonce.preview
  4007. });
  4008. });
  4009. // Refresh the nonces if the preview sends updated nonces over.
  4010. api.previewer.bind( 'nonce', function( nonce ) {
  4011. $.extend( this.nonce, nonce );
  4012. });
  4013. // Refresh the nonces if login sends updated nonces over.
  4014. api.bind( 'nonce-refresh', function( nonce ) {
  4015. $.extend( api.settings.nonce, nonce );
  4016. $.extend( api.previewer.nonce, nonce );
  4017. api.previewer.send( 'nonce-refresh', nonce );
  4018. });
  4019. // Create Settings
  4020. $.each( api.settings.settings, function( id, data ) {
  4021. var constructor = api.settingConstructor[ data.type ] || api.Setting,
  4022. setting;
  4023. setting = new constructor( id, data.value, {
  4024. transport: data.transport,
  4025. previewer: api.previewer,
  4026. dirty: !! data.dirty
  4027. } );
  4028. api.add( id, setting );
  4029. });
  4030. // Create Panels
  4031. $.each( api.settings.panels, function ( id, data ) {
  4032. var constructor = api.panelConstructor[ data.type ] || api.Panel,
  4033. panel;
  4034. panel = new constructor( id, {
  4035. params: data
  4036. } );
  4037. api.panel.add( id, panel );
  4038. });
  4039. // Create Sections
  4040. $.each( api.settings.sections, function ( id, data ) {
  4041. var constructor = api.sectionConstructor[ data.type ] || api.Section,
  4042. section;
  4043. section = new constructor( id, {
  4044. params: data
  4045. } );
  4046. api.section.add( id, section );
  4047. });
  4048. // Create Controls
  4049. $.each( api.settings.controls, function( id, data ) {
  4050. var constructor = api.controlConstructor[ data.type ] || api.Control,
  4051. control;
  4052. control = new constructor( id, {
  4053. params: data,
  4054. previewer: api.previewer
  4055. } );
  4056. api.control.add( id, control );
  4057. });
  4058. // Focus the autofocused element
  4059. _.each( [ 'panel', 'section', 'control' ], function( type ) {
  4060. var id = api.settings.autofocus[ type ];
  4061. if ( ! id ) {
  4062. return;
  4063. }
  4064. /*
  4065. * Defer focus until:
  4066. * 1. The panel, section, or control exists (especially for dynamically-created ones).
  4067. * 2. The instance is embedded in the document (and so is focusable).
  4068. * 3. The preview has finished loading so that the active states have been set.
  4069. */
  4070. api[ type ]( id, function( instance ) {
  4071. instance.deferred.embedded.done( function() {
  4072. api.previewer.deferred.active.done( function() {
  4073. instance.focus();
  4074. });
  4075. });
  4076. });
  4077. });
  4078. api.bind( 'ready', api.reflowPaneContents );
  4079. $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
  4080. var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
  4081. values.bind( 'add', debouncedReflowPaneContents );
  4082. values.bind( 'change', debouncedReflowPaneContents );
  4083. values.bind( 'remove', debouncedReflowPaneContents );
  4084. } );
  4085. // Save and activated states
  4086. (function() {
  4087. var state = new api.Values(),
  4088. saved = state.create( 'saved' ),
  4089. saving = state.create( 'saving' ),
  4090. activated = state.create( 'activated' ),
  4091. processing = state.create( 'processing' ),
  4092. paneVisible = state.create( 'paneVisible' ),
  4093. expandedPanel = state.create( 'expandedPanel' ),
  4094. expandedSection = state.create( 'expandedSection' ),
  4095. changesetStatus = state.create( 'changesetStatus' ),
  4096. previewerAlive = state.create( 'previewerAlive' ),
  4097. editShortcutVisibility = state.create( 'editShortcutVisibility' ),
  4098. populateChangesetUuidParam;
  4099. state.bind( 'change', function() {
  4100. var canSave;
  4101. if ( ! activated() ) {
  4102. saveBtn.val( api.l10n.activate );
  4103. closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  4104. } else if ( '' === changesetStatus.get() && saved() ) {
  4105. saveBtn.val( api.l10n.saved );
  4106. closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
  4107. } else {
  4108. saveBtn.val( api.l10n.save );
  4109. closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  4110. }
  4111. /*
  4112. * Save (publish) button should be enabled if saving is not currently happening,
  4113. * and if the theme is not active or the changeset exists but is not published.
  4114. */
  4115. canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) );
  4116. saveBtn.prop( 'disabled', ! canSave );
  4117. });
  4118. // Set default states.
  4119. changesetStatus( api.settings.changeset.status );
  4120. saved( true );
  4121. if ( '' === changesetStatus() ) { // Handle case for loading starter content.
  4122. api.each( function( setting ) {
  4123. if ( setting._dirty ) {
  4124. saved( false );
  4125. }
  4126. } );
  4127. }
  4128. saving( false );
  4129. activated( api.settings.theme.active );
  4130. processing( 0 );
  4131. paneVisible( true );
  4132. expandedPanel( false );
  4133. expandedSection( false );
  4134. previewerAlive( true );
  4135. editShortcutVisibility( 'visible' );
  4136. api.bind( 'change', function() {
  4137. if ( state( 'saved' ).get() ) {
  4138. state( 'saved' ).set( false );
  4139. populateChangesetUuidParam( true );
  4140. }
  4141. });
  4142. saving.bind( function( isSaving ) {
  4143. body.toggleClass( 'saving', isSaving );
  4144. } );
  4145. api.bind( 'saved', function( response ) {
  4146. state('saved').set( true );
  4147. if ( 'publish' === response.changeset_status ) {
  4148. state( 'activated' ).set( true );
  4149. }
  4150. });
  4151. activated.bind( function( to ) {
  4152. if ( to ) {
  4153. api.trigger( 'activated' );
  4154. }
  4155. });
  4156. /**
  4157. * Populate URL with UUID via `history.replaceState()`.
  4158. *
  4159. * @since 4.7.0
  4160. * @access private
  4161. *
  4162. * @param {boolean} isIncluded Is UUID included.
  4163. * @returns {void}
  4164. */
  4165. populateChangesetUuidParam = function( isIncluded ) {
  4166. var urlParser, queryParams;
  4167. // Abort on IE9 which doesn't support history management.
  4168. if ( ! history.replaceState ) {
  4169. return;
  4170. }
  4171. urlParser = document.createElement( 'a' );
  4172. urlParser.href = location.href;
  4173. queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  4174. if ( isIncluded ) {
  4175. if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
  4176. return;
  4177. }
  4178. queryParams.changeset_uuid = api.settings.changeset.uuid;
  4179. } else {
  4180. if ( ! queryParams.changeset_uuid ) {
  4181. return;
  4182. }
  4183. delete queryParams.changeset_uuid;
  4184. }
  4185. urlParser.search = $.param( queryParams );
  4186. history.replaceState( {}, document.title, urlParser.href );
  4187. };
  4188. changesetStatus.bind( function( newStatus ) {
  4189. populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus );
  4190. } );
  4191. // Expose states to the API.
  4192. api.state = state;
  4193. }());
  4194. // Check if preview url is valid and load the preview frame.
  4195. if ( api.previewer.previewUrl() ) {
  4196. api.previewer.refresh();
  4197. } else {
  4198. api.previewer.previewUrl( api.settings.url.home );
  4199. }
  4200. // Button bindings.
  4201. saveBtn.click( function( event ) {
  4202. api.previewer.save();
  4203. event.preventDefault();
  4204. }).keydown( function( event ) {
  4205. if ( 9 === event.which ) // tab
  4206. return;
  4207. if ( 13 === event.which ) // enter
  4208. api.previewer.save();
  4209. event.preventDefault();
  4210. });
  4211. closeBtn.keydown( function( event ) {
  4212. if ( 9 === event.which ) // tab
  4213. return;
  4214. if ( 13 === event.which ) // enter
  4215. this.click();
  4216. event.preventDefault();
  4217. });
  4218. $( '.collapse-sidebar' ).on( 'click', function() {
  4219. api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  4220. });
  4221. api.state( 'paneVisible' ).bind( function( paneVisible ) {
  4222. overlay.toggleClass( 'preview-only', ! paneVisible );
  4223. overlay.toggleClass( 'expanded', paneVisible );
  4224. overlay.toggleClass( 'collapsed', ! paneVisible );
  4225. if ( ! paneVisible ) {
  4226. $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
  4227. } else {
  4228. $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
  4229. }
  4230. });
  4231. // Keyboard shortcuts - esc to exit section/panel.
  4232. $( 'body' ).on( 'keydown', function( event ) {
  4233. var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
  4234. if ( 27 !== event.which ) { // Esc.
  4235. return;
  4236. }
  4237. /*
  4238. * Abort if the event target is not the body (the default) and not inside of #customize-controls.
  4239. * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
  4240. */
  4241. if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
  4242. return;
  4243. }
  4244. // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
  4245. api.control.each( function( control ) {
  4246. if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
  4247. expandedControls.push( control );
  4248. }
  4249. });
  4250. api.section.each( function( section ) {
  4251. if ( section.expanded() ) {
  4252. expandedSections.push( section );
  4253. }
  4254. });
  4255. api.panel.each( function( panel ) {
  4256. if ( panel.expanded() ) {
  4257. expandedPanels.push( panel );
  4258. }
  4259. });
  4260. // Skip collapsing expanded controls if there are no expanded sections.
  4261. if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
  4262. expandedControls.length = 0;
  4263. }
  4264. // Collapse the most granular expanded object.
  4265. collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
  4266. if ( collapsedObject ) {
  4267. collapsedObject.collapse();
  4268. event.preventDefault();
  4269. }
  4270. });
  4271. $( '.customize-controls-preview-toggle' ).on( 'click', function() {
  4272. api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  4273. });
  4274. /*
  4275. * Sticky header feature.
  4276. */
  4277. (function initStickyHeaders() {
  4278. var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
  4279. changeContainer, getHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
  4280. activeHeader, lastScrollTop;
  4281. /**
  4282. * Determine which panel or section is currently expanded.
  4283. *
  4284. * @since 4.7.0
  4285. * @access private
  4286. *
  4287. * @param {wp.customize.Panel|wp.customize.Section} container Construct.
  4288. * @returns {void}
  4289. */
  4290. changeContainer = function( container ) {
  4291. var newInstance = container,
  4292. expandedSection = api.state( 'expandedSection' ).get(),
  4293. expandedPanel = api.state( 'expandedPanel' ).get(),
  4294. headerElement;
  4295. // Release previously active header element.
  4296. if ( activeHeader && activeHeader.element ) {
  4297. releaseStickyHeader( activeHeader.element );
  4298. }
  4299. if ( ! newInstance ) {
  4300. if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
  4301. newInstance = expandedPanel;
  4302. } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
  4303. newInstance = expandedSection;
  4304. } else {
  4305. activeHeader = false;
  4306. return;
  4307. }
  4308. }
  4309. headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
  4310. if ( headerElement.length ) {
  4311. activeHeader = {
  4312. instance: newInstance,
  4313. element: headerElement,
  4314. parent: headerElement.closest( '.customize-pane-child' ),
  4315. height: getHeaderHeight( headerElement )
  4316. };
  4317. if ( expandedSection ) {
  4318. resetStickyHeader( activeHeader.element, activeHeader.parent );
  4319. }
  4320. } else {
  4321. activeHeader = false;
  4322. }
  4323. };
  4324. api.state( 'expandedSection' ).bind( changeContainer );
  4325. api.state( 'expandedPanel' ).bind( changeContainer );
  4326. // Throttled scroll event handler.
  4327. parentContainer.on( 'scroll', _.throttle( function() {
  4328. if ( ! activeHeader ) {
  4329. return;
  4330. }
  4331. var scrollTop = parentContainer.scrollTop(),
  4332. isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true;
  4333. lastScrollTop = scrollTop;
  4334. positionStickyHeader( activeHeader, scrollTop, isScrollingUp );
  4335. }, 8 ) );
  4336. // Release header element if it is sticky.
  4337. releaseStickyHeader = function( headerElement ) {
  4338. if ( ! headerElement.hasClass( 'is-sticky' ) ) {
  4339. return;
  4340. }
  4341. headerElement
  4342. .removeClass( 'is-sticky' )
  4343. .addClass( 'maybe-sticky is-in-view' )
  4344. .css( 'top', parentContainer.scrollTop() + 'px' );
  4345. };
  4346. // Reset position of the sticky header.
  4347. resetStickyHeader = function( headerElement, headerParent ) {
  4348. headerElement
  4349. .removeClass( 'maybe-sticky is-in-view' )
  4350. .css( {
  4351. width: '',
  4352. top: ''
  4353. } );
  4354. headerParent.css( 'padding-top', '' );
  4355. };
  4356. /**
  4357. * Get header height.
  4358. *
  4359. * @since 4.7.0
  4360. * @access private
  4361. *
  4362. * @param {jQuery} headerElement Header element.
  4363. * @returns {number} Height.
  4364. */
  4365. getHeaderHeight = function( headerElement ) {
  4366. var height = headerElement.data( 'height' );
  4367. if ( ! height ) {
  4368. height = headerElement.outerHeight();
  4369. headerElement.data( 'height', height );
  4370. }
  4371. return height;
  4372. };
  4373. /**
  4374. * Reposition header on throttled `scroll` event.
  4375. *
  4376. * @since 4.7.0
  4377. * @access private
  4378. *
  4379. * @param {object} header Header.
  4380. * @param {number} scrollTop Scroll top.
  4381. * @param {boolean} isScrollingUp Is scrolling up?
  4382. * @returns {void}
  4383. */
  4384. positionStickyHeader = function( header, scrollTop, isScrollingUp ) {
  4385. var headerElement = header.element,
  4386. headerParent = header.parent,
  4387. headerHeight = header.height,
  4388. headerTop = parseInt( headerElement.css( 'top' ), 10 ),
  4389. maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
  4390. isSticky = headerElement.hasClass( 'is-sticky' ),
  4391. isInView = headerElement.hasClass( 'is-in-view' );
  4392. // When scrolling down, gradually hide sticky header.
  4393. if ( ! isScrollingUp ) {
  4394. if ( isSticky ) {
  4395. headerTop = scrollTop;
  4396. headerElement
  4397. .removeClass( 'is-sticky' )
  4398. .css( {
  4399. top: headerTop + 'px',
  4400. width: ''
  4401. } );
  4402. }
  4403. if ( isInView && scrollTop > headerTop + headerHeight ) {
  4404. headerElement.removeClass( 'is-in-view' );
  4405. headerParent.css( 'padding-top', '' );
  4406. }
  4407. return;
  4408. }
  4409. // Scrolling up.
  4410. if ( ! maybeSticky && scrollTop >= headerHeight ) {
  4411. maybeSticky = true;
  4412. headerElement.addClass( 'maybe-sticky' );
  4413. } else if ( 0 === scrollTop ) {
  4414. // Reset header in base position.
  4415. headerElement
  4416. .removeClass( 'maybe-sticky is-in-view is-sticky' )
  4417. .css( {
  4418. top: '',
  4419. width: ''
  4420. } );
  4421. headerParent.css( 'padding-top', '' );
  4422. return;
  4423. }
  4424. if ( isInView && ! isSticky ) {
  4425. // Header is in the view but is not yet sticky.
  4426. if ( headerTop >= scrollTop ) {
  4427. // Header is fully visible.
  4428. headerElement
  4429. .addClass( 'is-sticky' )
  4430. .css( {
  4431. top: '',
  4432. width: headerParent.outerWidth() + 'px'
  4433. } );
  4434. }
  4435. } else if ( maybeSticky && ! isInView ) {
  4436. // Header is out of the view.
  4437. headerElement
  4438. .addClass( 'is-in-view' )
  4439. .css( 'top', ( scrollTop - headerHeight ) + 'px' );
  4440. headerParent.css( 'padding-top', headerHeight + 'px' );
  4441. }
  4442. };
  4443. }());
  4444. // Previewed device bindings.
  4445. api.previewedDevice = new api.Value();
  4446. // Set the default device.
  4447. api.bind( 'ready', function() {
  4448. _.find( api.settings.previewableDevices, function( value, key ) {
  4449. if ( true === value['default'] ) {
  4450. api.previewedDevice.set( key );
  4451. return true;
  4452. }
  4453. } );
  4454. } );
  4455. // Set the toggled device.
  4456. footerActions.find( '.devices button' ).on( 'click', function( event ) {
  4457. api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
  4458. });
  4459. // Bind device changes.
  4460. api.previewedDevice.bind( function( newDevice ) {
  4461. var overlay = $( '.wp-full-overlay' ),
  4462. devices = '';
  4463. footerActions.find( '.devices button' )
  4464. .removeClass( 'active' )
  4465. .attr( 'aria-pressed', false );
  4466. footerActions.find( '.devices .preview-' + newDevice )
  4467. .addClass( 'active' )
  4468. .attr( 'aria-pressed', true );
  4469. $.each( api.settings.previewableDevices, function( device ) {
  4470. devices += ' preview-' + device;
  4471. } );
  4472. overlay
  4473. .removeClass( devices )
  4474. .addClass( 'preview-' + newDevice );
  4475. } );
  4476. // Bind site title display to the corresponding field.
  4477. if ( title.length ) {
  4478. api( 'blogname', function( setting ) {
  4479. var updateTitle = function() {
  4480. title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
  4481. };
  4482. setting.bind( updateTitle );
  4483. updateTitle();
  4484. } );
  4485. }
  4486. /*
  4487. * Create a postMessage connection with a parent frame,
  4488. * in case the Customizer frame was opened with the Customize loader.
  4489. *
  4490. * @see wp.customize.Loader
  4491. */
  4492. parent = new api.Messenger({
  4493. url: api.settings.url.parent,
  4494. channel: 'loader'
  4495. });
  4496. /*
  4497. * If we receive a 'back' event, we're inside an iframe.
  4498. * Send any clicks to the 'Return' link to the parent page.
  4499. */
  4500. parent.bind( 'back', function() {
  4501. closeBtn.on( 'click.customize-controls-close', function( event ) {
  4502. event.preventDefault();
  4503. parent.send( 'close' );
  4504. });
  4505. });
  4506. // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
  4507. $( window ).on( 'beforeunload.customize-confirm', function () {
  4508. if ( ! api.state( 'saved' )() ) {
  4509. setTimeout( function() {
  4510. overlay.removeClass( 'customize-loading' );
  4511. }, 1 );
  4512. return api.l10n.saveAlert;
  4513. }
  4514. } );
  4515. // Pass events through to the parent.
  4516. $.each( [ 'saved', 'change' ], function ( i, event ) {
  4517. api.bind( event, function() {
  4518. parent.send( event );
  4519. });
  4520. } );
  4521. // Pass titles to the parent
  4522. api.bind( 'title', function( newTitle ) {
  4523. parent.send( 'title', newTitle );
  4524. });
  4525. parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  4526. // Initialize the connection with the parent frame.
  4527. parent.send( 'ready' );
  4528. // Control visibility for default controls
  4529. $.each({
  4530. 'background_image': {
  4531. controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
  4532. callback: function( to ) { return !! to; }
  4533. },
  4534. 'show_on_front': {
  4535. controls: [ 'page_on_front', 'page_for_posts' ],
  4536. callback: function( to ) { return 'page' === to; }
  4537. },
  4538. 'header_textcolor': {
  4539. controls: [ 'header_textcolor' ],
  4540. callback: function( to ) { return 'blank' !== to; }
  4541. }
  4542. }, function( settingId, o ) {
  4543. api( settingId, function( setting ) {
  4544. $.each( o.controls, function( i, controlId ) {
  4545. api.control( controlId, function( control ) {
  4546. var visibility = function( to ) {
  4547. control.container.toggle( o.callback( to ) );
  4548. };
  4549. visibility( setting.get() );
  4550. setting.bind( visibility );
  4551. });
  4552. });
  4553. });
  4554. });
  4555. api.control( 'background_preset', function( control ) {
  4556. var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
  4557. visibility = { // position, size, repeat, attachment
  4558. 'default': [ false, false, false, false ],
  4559. 'fill': [ true, false, false, false ],
  4560. 'fit': [ true, false, true, false ],
  4561. 'repeat': [ true, false, false, true ],
  4562. 'custom': [ true, true, true, true ]
  4563. };
  4564. defaultValues = [
  4565. _wpCustomizeBackground.defaults['default-position-x'],
  4566. _wpCustomizeBackground.defaults['default-position-y'],
  4567. _wpCustomizeBackground.defaults['default-size'],
  4568. _wpCustomizeBackground.defaults['default-repeat'],
  4569. _wpCustomizeBackground.defaults['default-attachment']
  4570. ];
  4571. values = { // position_x, position_y, size, repeat, attachment
  4572. 'default': defaultValues,
  4573. 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
  4574. 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
  4575. 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
  4576. };
  4577. // @todo These should actually toggle the active state, but without the preview overriding the state in data.activeControls.
  4578. toggleVisibility = function( preset ) {
  4579. _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
  4580. var control = api.control( controlId );
  4581. if ( control ) {
  4582. control.container.toggle( visibility[ preset ][ i ] );
  4583. }
  4584. } );
  4585. };
  4586. updateSettings = function( preset ) {
  4587. _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
  4588. var setting = api( settingId );
  4589. if ( setting ) {
  4590. setting.set( values[ preset ][ i ] );
  4591. }
  4592. } );
  4593. };
  4594. preset = control.setting.get();
  4595. toggleVisibility( preset );
  4596. control.setting.bind( 'change', function( preset ) {
  4597. toggleVisibility( preset );
  4598. if ( 'custom' !== preset ) {
  4599. updateSettings( preset );
  4600. }
  4601. } );
  4602. } );
  4603. api.control( 'background_repeat', function( control ) {
  4604. control.elements[0].unsync( api( 'background_repeat' ) );
  4605. control.element = new api.Element( control.container.find( 'input' ) );
  4606. control.element.set( 'no-repeat' !== control.setting() );
  4607. control.element.bind( function( to ) {
  4608. control.setting.set( to ? 'repeat' : 'no-repeat' );
  4609. } );
  4610. control.setting.bind( function( to ) {
  4611. control.element.set( 'no-repeat' !== to );
  4612. } );
  4613. } );
  4614. api.control( 'background_attachment', function( control ) {
  4615. control.elements[0].unsync( api( 'background_attachment' ) );
  4616. control.element = new api.Element( control.container.find( 'input' ) );
  4617. control.element.set( 'fixed' !== control.setting() );
  4618. control.element.bind( function( to ) {
  4619. control.setting.set( to ? 'scroll' : 'fixed' );
  4620. } );
  4621. control.setting.bind( function( to ) {
  4622. control.element.set( 'fixed' !== to );
  4623. } );
  4624. } );
  4625. // Juggle the two controls that use header_textcolor
  4626. api.control( 'display_header_text', function( control ) {
  4627. var last = '';
  4628. control.elements[0].unsync( api( 'header_textcolor' ) );
  4629. control.element = new api.Element( control.container.find('input') );
  4630. control.element.set( 'blank' !== control.setting() );
  4631. control.element.bind( function( to ) {
  4632. if ( ! to )
  4633. last = api( 'header_textcolor' ).get();
  4634. control.setting.set( to ? last : 'blank' );
  4635. });
  4636. control.setting.bind( function( to ) {
  4637. control.element.set( 'blank' !== to );
  4638. });
  4639. });
  4640. // Change previewed URL to the homepage when changing the page_on_front.
  4641. api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
  4642. var updatePreviewUrl = function() {
  4643. if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
  4644. api.previewer.previewUrl.set( api.settings.url.home );
  4645. }
  4646. };
  4647. showOnFront.bind( updatePreviewUrl );
  4648. pageOnFront.bind( updatePreviewUrl );
  4649. });
  4650. // Change the previewed URL to the selected page when changing the page_for_posts.
  4651. api( 'page_for_posts', function( setting ) {
  4652. setting.bind(function( pageId ) {
  4653. pageId = parseInt( pageId, 10 );
  4654. if ( pageId > 0 ) {
  4655. api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
  4656. }
  4657. });
  4658. });
  4659. // Allow tabs to be entered in Custom CSS textarea.
  4660. api.control( 'custom_css', function setupCustomCssControl( control ) {
  4661. control.deferred.embedded.done( function allowTabs() {
  4662. var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
  4663. $textarea.on( 'blur', function onBlur() {
  4664. $textarea.data( 'next-tab-blurs', false );
  4665. } );
  4666. $textarea.on( 'keydown', function onKeydown( event ) {
  4667. var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
  4668. if ( escKeyCode === event.keyCode ) {
  4669. if ( ! $textarea.data( 'next-tab-blurs' ) ) {
  4670. $textarea.data( 'next-tab-blurs', true );
  4671. event.stopPropagation(); // Prevent collapsing the section.
  4672. }
  4673. return;
  4674. }
  4675. // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
  4676. if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
  4677. return;
  4678. }
  4679. // Prevent capturing Tab characters if Esc was pressed.
  4680. if ( $textarea.data( 'next-tab-blurs' ) ) {
  4681. return;
  4682. }
  4683. selectionStart = textarea.selectionStart;
  4684. selectionEnd = textarea.selectionEnd;
  4685. value = textarea.value;
  4686. if ( selectionStart >= 0 ) {
  4687. textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
  4688. $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
  4689. }
  4690. event.stopPropagation();
  4691. event.preventDefault();
  4692. } );
  4693. } );
  4694. } );
  4695. // Toggle visibility of Header Video notice when active state change.
  4696. api.control( 'header_video', function( headerVideoControl ) {
  4697. headerVideoControl.deferred.embedded.done( function() {
  4698. var toggleNotice = function() {
  4699. var section = api.section( headerVideoControl.section() ), notice;
  4700. if ( ! section ) {
  4701. return;
  4702. }
  4703. notice = section.container.find( '.header-video-not-currently-previewable:first' );
  4704. if ( headerVideoControl.active.get() ) {
  4705. notice.stop().slideUp( 'fast' );
  4706. } else {
  4707. notice.stop().slideDown( 'fast' );
  4708. }
  4709. };
  4710. toggleNotice();
  4711. headerVideoControl.active.bind( toggleNotice );
  4712. } );
  4713. } );
  4714. // Update the setting validities.
  4715. api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
  4716. api._handleSettingValidities( {
  4717. settingValidities: settingValidities,
  4718. focusInvalidControl: false
  4719. } );
  4720. } );
  4721. // Focus on the control that is associated with the given setting.
  4722. api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
  4723. var matchedControls = [];
  4724. api.control.each( function( control ) {
  4725. var settingIds = _.pluck( control.settings, 'id' );
  4726. if ( -1 !== _.indexOf( settingIds, settingId ) ) {
  4727. matchedControls.push( control );
  4728. }
  4729. } );
  4730. // Focus on the matched control with the lowest priority (appearing higher).
  4731. if ( matchedControls.length ) {
  4732. matchedControls.sort( function( a, b ) {
  4733. return a.priority() - b.priority();
  4734. } );
  4735. matchedControls[0].focus();
  4736. }
  4737. } );
  4738. // Refresh the preview when it requests.
  4739. api.previewer.bind( 'refresh', function() {
  4740. api.previewer.refresh();
  4741. });
  4742. // Update the edit shortcut visibility state.
  4743. api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
  4744. var isMobileScreen;
  4745. if ( window.matchMedia ) {
  4746. isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
  4747. } else {
  4748. isMobileScreen = $( window ).width() <= 640;
  4749. }
  4750. api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
  4751. } );
  4752. if ( window.matchMedia ) {
  4753. window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
  4754. var state = api.state( 'paneVisible' );
  4755. state.callbacks.fireWith( state, [ state.get(), state.get() ] );
  4756. } );
  4757. }
  4758. api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
  4759. api.state( 'editShortcutVisibility' ).set( visibility );
  4760. } );
  4761. api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
  4762. api.previewer.send( 'edit-shortcut-visibility', visibility );
  4763. } );
  4764. // Autosave changeset.
  4765. ( function() {
  4766. var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
  4767. /**
  4768. * Request changeset update and then re-schedule the next changeset update time.
  4769. *
  4770. * @since 4.7.0
  4771. * @private
  4772. */
  4773. updateChangesetWithReschedule = function() {
  4774. if ( ! updatePending ) {
  4775. updatePending = true;
  4776. api.requestChangesetUpdate().always( function() {
  4777. updatePending = false;
  4778. } );
  4779. }
  4780. scheduleChangesetUpdate();
  4781. };
  4782. /**
  4783. * Schedule changeset update.
  4784. *
  4785. * @since 4.7.0
  4786. * @private
  4787. */
  4788. scheduleChangesetUpdate = function() {
  4789. clearTimeout( timeoutId );
  4790. timeoutId = setTimeout( function() {
  4791. updateChangesetWithReschedule();
  4792. }, api.settings.timeouts.changesetAutoSave );
  4793. };
  4794. // Start auto-save interval for updating changeset.
  4795. scheduleChangesetUpdate();
  4796. // Save changeset when focus removed from window.
  4797. $( window ).on( 'blur.wp-customize-changeset-update', function() {
  4798. updateChangesetWithReschedule();
  4799. } );
  4800. // Save changeset before unloading window.
  4801. $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
  4802. updateChangesetWithReschedule();
  4803. } );
  4804. } ());
  4805. // Make sure TinyMCE dialogs appear above Customizer UI.
  4806. $( document ).one( 'wp-before-tinymce-init', function() {
  4807. if ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) {
  4808. window.tinymce.ui.FloatPanel.zIndex = 500001;
  4809. }
  4810. } );
  4811. api.trigger( 'ready' );
  4812. });
  4813. })( wp, jQuery );