PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB`Mini Shell

HOME


Mini Shell 1.0
DIR:/home/sharklogisticsinc.com/www/wp-content/plugins/so-css/js/
Upload File :
Current File : //home/sharklogisticsinc.com/www/wp-content/plugins/so-css/js/editor.js
/* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */

( function ( $, _, socssOptions ) {
	
	var socss = {
		model: {},
		collection: {},
		view: {},
		fn: {}
	};
	
	window.socss = socss;
	
	socss.model.CustomCssModel = Backbone.Model.extend( {
		defaults: {
			postId: null,
			postTitle: null,
			css: null,
		},
		
		urlRoot: socssOptions.postCssUrlRoot,
		
		url: function () {
			return this.urlRoot + '&postId=' + this.get( 'postId' );
		}
	} );
	
	socss.model.CustomCssCollection = Backbone.Collection.extend( {
		model: socss.model.CustomCssModel,
		
		modelId: function( attrs ) {
			return attrs.postId;
		},
	} );
	
	socss.model.CSSEditorModel = Backbone.Model.extend( {
		defaults: {
			customCssPosts: null,
		}
	} );
	
	/**
	 * The toolbar view
	 */
	socss.view.toolbar = Backbone.View.extend( {
		
		button: _.template( '<li><a href="#<%= action %>" class="toolbar-button socss-button"><%= text %></a></li>' ),
		
		events: {
			'click .socss-button': 'triggerEvent',
		},
		
		triggerEvent: function ( event ) {
			event.preventDefault();
			var $target = $( event.currentTarget );
			$target.blur();
			var value = $target.attr( 'href' ).replace( '#', '' );
			this.$el.trigger( 'click_' + value );
		},
		
		addButton: function ( text, action ) {
			var button = $( this.button( { text: text, action: action } ) )
			.appendTo( this.$( '.toolbar-function-buttons .toolbar-buttons' ) );
			
			return button;
		},
	} );
	
	/**
	 * The editor view, which handles codemirror stuff
	 *
	 * model: socss.model.CSSEditorModel
	 *
	 */
	socss.view.editor = Backbone.View.extend( {
		
		codeMirror: null,
		snippets: null,
		toolbar: null,
		visualProperties: null,
		
		inspector: null,
		
		cssSelectors: [],
		
		initValue: null,
		
		events: {
			'click_expand .custom-css-toolbar': 'toggleExpand',
			'click_visual .custom-css-toolbar': 'showVisualEditor',
			'submit': 'onSubmit',
		},
		
		initialize: function ( options ) {
			
			this.listenTo( this.model, 'change:selectedPost', this.getSelectedPostCss );
			
			this.getSelectedPostCss().then( function () {
				
				if ( options.openVisualEditor ) {
					this.showVisualEditor();
				}
			}.bind( this ) );
			
		},
		
		getSelectedPostCss: function () {
			var selectedPost = this.model.get( 'selectedPost' );
			var promise;
			if ( selectedPost && ! selectedPost.has( 'css' ) ) {
				promise = selectedPost.fetch();
			} else {
				promise = new $.Deferred().resolve();
			}
			
			return promise.then( this.render.bind( this ) );
		},
		
		render: function () {
			
			var selectedPost = this.model.get( 'selectedPost' );
			
			if ( selectedPost && !selectedPost.has( 'css' ) ) {
				return this;
			}
			
			if ( !this.codeMirror ) {
				this.setupEditor();
			}
			
			if ( ! this.toolbar ) {
				this.toolbar = new socss.view.toolbar( {
					el: this.$( '.custom-css-toolbar' ),
					model: this.model,
				} );
				this.toolbar.render();
			}
			
			if ( !this.visualProperties ) {
				this.visualProperties = new socss.view.properties( {
					editor: this,
					el: $( '#so-custom-css-properties' )
				} );
				this.visualProperties.render();
			}
			
			if ( !this.preview ) {
				this.preview = new socss.view.preview( {
					editor: this,
					model: this.model,
					el: this.$( '.custom-css-preview' ),
					initURL: socssOptions.homeURL,
				} );
				this.preview.render();
			}
			
			if ( selectedPost ) {
				this.codeMirror.setValue( selectedPost.get( 'css' ) );
				this.codeMirror.clearHistory();
			}
			
			return this;
		},
		
		/**
		 * Do the initial setup of the CodeMirror editor
		 */
		setupEditor: function () {
			this.registerCodeMirrorAutocomplete();
			
			// Setup the Codemirror instance
			var $textArea = this.$( 'textarea.css-editor' );
			this.initValue = $textArea.val();
			// Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste
			// options in context menu.
			var newlineMatches = this.initValue.match( /\n/gm );
			var lineCount = newlineMatches ? newlineMatches.length + 1 : 1;
			var paddedValue = this.initValue;
			$textArea.val( paddedValue );
			this.codeMirror = CodeMirror.fromTextArea( $textArea.get( 0 ), {
				tabSize: 2,
				lineNumbers: true,
				mode: 'css',
				theme: 'neat',
				inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc.
				gutters: [
					"CodeMirror-lint-markers"
				],
				lint: true,
				search: true,
				dialog: true,
				annotateScrollbar: true,
				extraKeys: {
					'Ctrl-F': 'findPersistent',
					'Alt-G': 'jumpToLine',
				}
			} );
			
			this.codeMirror.on( 'change', function ( cm, change ) {
				var selectedPost = this.model.get( 'selectedPost' );
				if ( selectedPost && selectedPost.get( 'css' ) !== cm.getValue().trim() ) {
					selectedPost.set( 'css', cm.getValue().trim() );
				}
			}.bind( this ) );
			
			// Make sure the user doesn't leave without saving
			$( window ).on( 'beforeunload', function () {
				var editorValue = this.codeMirror.getValue().trim();
				if ( editorValue !== this.initValue ) {
					return socssOptions.loc.leave;
				}
			}.bind( this ) );
			
			
			// Set the container to visible overflow once the editor is setup
			this.$el.find( '.custom-css-container' ).css( 'overflow', 'visible' );
			this.scaleEditor();
			
			// Scale the editor whenever the window is resized
			$( window ).on( 'resize', function () {
				this.scaleEditor();
			}.bind( this ) );
			
			// Setup the extensions
			this.setupCodeMirrorExtensions();
		},
		
		onSubmit: function () {
			this.initValue = this.codeMirror.getValue().trim();
		},
		
		/**
		 * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder.
		 */
		registerCodeMirrorAutocomplete: function () {
			var pseudoClasses = {
				link: 1, visited: 1, active: 1, hover: 1, focus: 1,
				"first-letter": 1, "first-line": 1, "first-child": 1,
				before: 1, after: 1, lang: 1
			};
			
			CodeMirror.registerHelper( "hint", "css", function ( cm ) {
				var cur = cm.getCursor(), token = cm.getTokenAt( cur );
				var inner = CodeMirror.innerMode( cm.getMode(), token.state );
				if ( inner.mode.name !== "css" ) {
					return;
				}
				
				if ( token.type === "keyword" && "!important".indexOf( token.string ) === 0 ) {
					return {
						list: [ "!important" ], from: CodeMirror.Pos( cur.line, token.start ),
						to: CodeMirror.Pos( cur.line, token.end )
					};
				}
				
				var start = token.start, end = cur.ch, word = token.string.slice( 0, end - start );
				if ( /[^\w$_-]/.test( word ) ) {
					word = "";
					start = end = cur.ch;
				}
				
				var spec = CodeMirror.resolveMode( "text/css" );
				
				var result = [];
				
				function add( keywords ) {
					for ( var name in keywords ) {
						if ( !word || name.lastIndexOf( word, 0 ) === 0 ) {
							result.push( name );
						}
					}
				}
				
				var st = inner.state.state;
				
				if ( st === 'top' ) {
					// We're going to autocomplete the selector using our own set of rules
					var line = cm.getLine( cur.line ).trim();
					
					var selectors = this.cssSelectors;
					for ( var i = 0; i < selectors.length; i++ ) {
						if ( selectors[ i ].selector.indexOf( line ) !== -1 ) {
							result.push( selectors[ i ].selector );
						}
					}
					
					if ( result.length ) {
						return {
							list: result,
							from: CodeMirror.Pos( cur.line, 0 ),
							to: CodeMirror.Pos( cur.line, end )
						};
					}
				}
				else {
					
					if ( st === "pseudo" || token.type === "variable-3" ) {
						add( pseudoClasses );
					}
					else if ( st === "block" || st === "maybeprop" ) {
						add( spec.propertyKeywords );
					}
					else if ( st === "prop" || st === "parens" || st === "at" || st === "params" ) {
						add( spec.valueKeywords );
						add( spec.colorKeywords );
					}
					else if ( st === "media" || st === "media_parens" ) {
						add( spec.mediaTypes );
						add( spec.mediaFeatures );
					}
					
					if ( result.length ) {
						return {
							list: result,
							from: CodeMirror.Pos( cur.line, start ),
							to: CodeMirror.Pos( cur.line, end )
						};
					}
					
				}
				
			}.bind( this ) );
		},
		
		setupCodeMirrorExtensions: function () {
			
			this.codeMirror.on( 'cursorActivity', function ( cm ) {
				var cur = cm.getCursor(), token = cm.getTokenAt( cur );
				var inner = CodeMirror.innerMode( cm.getMode(), token.state );
				
				// If we have a qualifier selected, then highlight that in the preview
				if ( token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin' ) {
					var line = cm.getLine( cur.line );
					var selector = line.substring( 0, token.end );
					
					this.preview.highlight( selector );
				}
				else {
					this.preview.clearHighlight();
				}
			}.bind( this ) );
			
			// This sets up automatic autocompletion at all times
			this.codeMirror.on( 'keyup', function ( cm, e ) {
				if (
					( e.keyCode >= 65 && e.keyCode <= 90 ) ||
					( e.keyCode === 189 && !e.shiftKey ) ||
					( e.keyCode === 190 && !e.shiftKey ) ||
					( e.keyCode === 51 && e.shiftKey ) ||
					( e.keyCode === 189 && e.shiftKey )
				) {
					cm.showHint( {
						completeSingle: false
					} );
				}
			} );
		},
		
		/**
		 * Scale the size of the editor depending on whether it's expanded or not
		 */
		scaleEditor: function () {
			var windowHeight = $( window ).outerHeight();
			if ( this.$el.hasClass( 'expanded' ) ) {
				// If we're in the expanded view, then resize the editor
				this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', '' );
				this.codeMirror.setSize( '100%', windowHeight - this.$( '.custom-css-toolbar' ).outerHeight() );
			}
			else {
				// Attempt to calculate approximate space available for editor when not expanded.
				var $form = $( '#so-custom-css-form' );
				var otherEltsHeight = $( '#wpadminbar' ).outerHeight( true ) +
					$( '#siteorigin-custom-css' ).find( '> h2' ).outerHeight( true ) +
					$form.find( '> .custom-css-toolbar' ).outerHeight( true ) +
					$form.find( '> p.description' ).outerHeight( true ) +
					$form.find( '> p.submit' ).outerHeight( true ) +
					parseFloat( $( '#wpbody-content' ).css( 'padding-bottom' ) );
				this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', windowHeight - otherEltsHeight );
				this.codeMirror.setSize( '100%', 'auto' );
			}
		},
		
		/**
		 * Check if the editor is in expanded mode
		 * @returns bool
		 */
		isExpanded: function () {
			return this.$el.hasClass( 'expanded' );
		},
		
		/**
		 * Toggle if this is expanded or not
		 */
		toggleExpand: function () {
			this.$el.toggleClass( 'expanded' );
			this.scaleEditor();
		},
		
		/**
		 * Set the expanded state of the editor
		 * @param expanded
		 */
		setExpand: function ( expanded ) {
			if ( expanded ) {
				this.$el.addClass( 'expanded' );
			}
			else {
				this.$el.removeClass( 'expanded' );
			}
			this.scaleEditor();
		},
		
		/**
		 * Show the visual editor view.
		 */
		showVisualEditor: function () {
			this.visualProperties.loadCSS( this.codeMirror.getValue().trim() );
			this.visualProperties.show();
		},
		
		/**
		 * Set the snippets available to this editor
		 */
		setSnippets: function ( snippets ) {
			if ( !_.isEmpty( snippets ) ) {
				
				this.snippets = new socss.view.snippets( {
					snippets: snippets
				} );
				this.snippets.editor = this;
				
				this.snippets.render();
				this.toolbar.addButton( 'Snippets', 'snippets' );
				this.toolbar.on( 'click_snippets', function () {
					this.snippets.show();
				}.bind( this ) );
			}
		},
		
		/**
		 * Add some CSS to the editor.
		 * @param css
		 */
		addCode: function ( css ) {
			var editor = this.codeMirror;
			
			var before_css = '';
			if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
				before_css = "";
			}
			else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
				before_css = "\n";
			}
			else {
				before_css = "\n\n";
			}
			
			// Now insert the code in the editor
			editor.doc.setCursor(
				editor.doc.lastLine(),
				editor.doc.getLine( editor.doc.lastLine() ).length
			);
			editor.doc.replaceSelection( before_css + css );
		},
		
		addEmptySelector: function ( selector ) {
			this.addCode( selector + " {\n  \n}" );
		},
		
		/**
		 * Sets the inspector view that's being used by the editor
		 */
		setInspector: function ( inspector ) {
			this.inspector = inspector;
			this.cssSelectors = inspector.pageSelectors;
			
			// A selector is clicked in the inspector
			inspector.on( 'click_selector', function ( selector ) {
				if ( this.visualProperties.isVisible() ) {
					this.visualProperties.addSelector( selector );
				}
				else {
					this.addEmptySelector( selector );
				}
			}.bind( this ) );
			
			// A property is clicked in the inspector
			inspector.on( 'click_property', function ( property ) {
				if ( !this.visualProperties.isVisible() ) {
					this.codeMirror.replaceSelection( property + ";\n  " );
				}
			}.bind( this ) );
			
			inspector.on( 'set_active_element', function ( el, selectors ) {
				if ( this.visualProperties.isVisible() && selectors.length ) {
					this.visualProperties.addSelector( selectors[ 0 ].selector );
				}
			}.bind( this ) );
		}
		
	} );
	
	/**
	 * The preview.
	 */
	socss.view.preview = Backbone.View.extend( {
		
		template: _.template( $( '#template-preview-window' ).html() ),
		editor: null,
		originalUri: null,
		currentUri: null,
		
		events: {
			'mouseleave #preview-iframe': 'clearHighlight',
			'keydown #preview-navigator input[type="text"]': 'reloadPreview',
		},
		
		initialize: function ( attr ) {
			this.editor = attr.editor;
			
			this.listenTo( this.model, 'change:selectedPost', this.render.bind( this ) );
			
			this.originalUri = new URI( attr.initURL );
			this.currentUri = new URI( attr.initURL );
			
			this.editor.codeMirror.on( 'change', function ( cm, c ) {
				this.updatePreviewCss();
			}.bind( this ) );
		},
		
		render: function () {
			
			var selectedPost = this.model.get( 'selectedPost' );
			
			if ( selectedPost && !selectedPost.has( 'postUrl' ) ) {
				selectedPost.fetch().then( this.render.bind( this ) );
				return this;
			}
			
			this.$el.html( this.template() );
			
			if ( selectedPost ) {
				this.currentUri = new URI( selectedPost.get( 'postUrl' ) );
			}
			
			this.currentUri.removeQuery( 'so_css_preview', 1 );
			this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
			this.currentUri.addQuery( 'so_css_preview', 1 );
			
			this.$( '#preview-iframe' )
				.attr( 'src', this.currentUri.toString() )
				// 'load' event doesn't bubble so can't be used in the events hash
				.on( 'load', this.initPreview.bind( this ) );
		},
		
		initPreview: function () {
			var $$ = this.$( '#preview-iframe' );
			
			// Update the current URI with the iframe URI
			this.currentUri = new URI( $$.contents().get( 0 ).location.href );
			this.currentUri.removeQuery( 'so_css_preview' );
			this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
			this.currentUri.addQuery( 'so_css_preview', 1 );
			
			$$.contents().find( 'a' ).each( function () {
				var href = $( this ).attr( 'href' );
				if ( href === undefined ) {
					return true;
				}
				
				var firstSeperator = ( href.indexOf( '?' ) === -1 ? '?' : '&' );
				$( this ).attr( 'href', href + firstSeperator + 'so_css_preview=1' );
			} );
			
			this.updatePreviewCss();
		},
		
		reloadPreview: function ( e ) {
			var $$ = this.$( '#preview-navigator input[type="text"]' );
			
			if ( e.keyCode === 13 ) {
				e.preventDefault();
				
				var newUri = new URI( $$.val() );
				
				// Validate the URI
				if (
					this.originalUri.host() !== newUri.host() ||
					this.originalUri.protocol() !== newUri.protocol()
				) {
					$$.blur();
					alert( $$.data( 'invalid-uri' ) );
					$$.focus();
				}
				else {
					newUri.addQuery( 'so_css_preview', 1 );
					this.$( '#preview-iframe' ).attr( 'src', newUri.toString() );
				}
			}
		},
		
		/**
		 * Update the preview CSS from the CodeMirror value in the editor
		 */
		updatePreviewCss: function () {
			var preview = this.$( '#preview-iframe' );
			if ( preview.length === 0 ) {
				return;
			}
			
			var head = preview.contents().find( 'head' );
			if ( head.find( 'style.siteorigin-custom-css' ).length === 0 ) {
				head.append( '<style class="siteorigin-custom-css" type="text/css"></style>' );
			}
			var style = head.find( 'style.siteorigin-custom-css' );
			
			// Update the CSS after a short delay
			var css = this.editor.codeMirror.getValue().trim();
			style.html( css );
		},
		
		/**
		 * Highlight all elements with a given selector
		 */
		highlight: function ( selector ) {
			try {
				this.editor.inspector.hl.highlight( selector );
			}
			catch ( err ) {
				console.log( 'No inspector to highlight with' );
			}
		},
		
		/**
		 * Clear the currently highlighted elements in preview
		 */
		clearHighlight: function () {
			try {
				this.editor.inspector.hl.clear();
			}
			catch ( err ) {
				console.log( 'No inspector to highlight with' );
			}
		}
		
	} );
	
	/**
	 * The dialog for the snippets browser
	 */
	socss.view.snippets = Backbone.View.extend( {
		template: _.template( $( '#template-snippet-browser' ).html() ),
		snippet: _.template( '<li class="snippet"><%- name %></li>' ),
		className: 'css-editor-snippet-browser',
		snippets: null,
		editor: null,
		
		events: {
			'click .close': 'hide',
			'click .buttons .insert-snippet': 'insertSnippet',
			'click .snippet': 'clickSnippet',
		},
		
		currentSnippet: null,
		
		initialize: function ( args ) {
			this.snippets = args.snippets;
		},
		
		render: function () {
			this.$el.html( this.template() );
			for ( var i = 0; i < this.snippets.length; i++ ) {
				$( this.snippet( { name: this.snippets[ i ].Name } ) )
				.data( {
					'description': this.snippets[ i ].Description,
					'css': this.snippets[ i ].css
				} )
				.appendTo( this.$( 'ul.snippets' ) );
			}
			
			// Click on the first one
			this.$( '.snippets li.snippet' ).eq( 0 ).click();
			
			this.attach();
			return this;
		},
		
		clickSnippet: function ( event ) {
			event.preventDefault();
			var $$ = $( event.currentTarget );
			
			this.$( '.snippets li.snippet' ).removeClass( 'active' );
			$( this ).addClass( 'active' );
			this.viewSnippet( {
				name: $$.html(),
				description: $$.data( 'description' ),
				css: $$.data( 'css' )
			} );
		},
		
		viewSnippet: function ( args ) {
			var w = this.$( '.main .snippet-view' );
			
			w.find( '.snippet-title' ).html( args.name );
			w.find( '.snippet-description' ).html( args.description );
			w.find( '.snippet-code' ).html( args.css );
			
			this.currentSnippet = args;
		},
		
		insertSnippet: function () {
			var editor = this.editor.codeMirror;
			var css = this.currentSnippet.css;
			
			var before_css = '';
			if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
				before_css = "";
			}
			else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
				before_css = "\n";
			}
			else {
				before_css = "\n\n";
			}
			
			// Now insert the code in the editor
			editor.doc.setCursor(
				editor.doc.lastLine(),
				editor.doc.getLine( editor.doc.lastLine() ).length
			);
			editor.doc.replaceSelection( before_css + css );
			
			this.hide();
		},
		
		attach: function () {
			this.$el.appendTo( 'body' );
		},
		
		show: function () {
			this.$el.show();
		},
		
		hide: function () {
			this.$el.hide();
		}
	} );
	
	
	/**
	 * The visual properties editor
	 */
	socss.view.properties = Backbone.View.extend( {
		
		tabTemplate: _.template( '<li data-section="<%- id %>"><span class="fa fa-<%- icon %>"></span> <%- title %></li>' ),
		sectionTemplate: _.template( '<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>' ),
		controllerTemplate: _.template( '<tr><th scope="row"><%- title %></th><td></td></tr>' ),
		
		/**
		 * The controllers for each of the properties
		 */
		propertyControllers: [],
		
		/**
		 * The editor view
		 */
		editor: null,
		
		/**
		 * The current, raw CSS
		 */
		css: '',
		
		/**
		 * Parsed CSS
		 */
		parsed: {},
		
		/**
		 * The current active selector
		 */
		activeSelector: '',
		
		/**
		 * Was the editor expanded before we went into the property editor
		 */
		editorExpandedBefore: false,
		
		events: {
			'click .close': 'hide',
			'click .section-tabs li': 'onTabClick',
			'change .toolbar select': 'onToolbarSelectChange',
		},
		
		/**
		 * Initialize the properties editor with a new model
		 */
		initialize: function ( options ) {
			this.parser = window.css;
			this.editor = options.editor;
		},
		
		/**
		 * Render the property editor
		 */
		render: function () {
			// Clean up for potential re-renders
			this.$( '.section-tabs' ).empty();
			this.$( '.sections' ).empty();
			this.$( '.toolbar select' ).off();
			this.propertyControllers = [];
			
			var controllers = socssOptions.propertyControllers;
			
			for ( var id in controllers ) {
				// Create the tabs
				var $t = $( this.tabTemplate( {
					id: id,
					icon: controllers[ id ].icon,
					title: controllers[ id ].title
				} ) ).appendTo( this.$( '.section-tabs' ) );
				
				// Create the section wrapper
				var $s = $( this.sectionTemplate( {
					id: id
				} ) ).appendTo( this.$( '.sections' ) );
				
				// Now lets add the controllers
				if ( !_.isEmpty( controllers[ id ].controllers ) ) {
					
					for ( var i = 0; i < controllers[ id ].controllers.length; i++ ) {
						
						var $c = $( this.controllerTemplate( {
							title: controllers[ id ].controllers[ i ].title
						} ) ).appendTo( $s.find( 'tbody' ) );
						
						var controllerAtts = controllers[ id ].controllers[ i ];
						var controller;
						
						if ( typeof socss.view.properties.controllers[ controllerAtts.type ] === 'undefined' ) {
							// Setup a default controller
							controller = new socss.view.propertyController( {
								el: $c.find( 'td' ),
								propertiesView: this,
								args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
							} );
						}
						else {
							// Setup a specific controller
							controller = new socss.view.properties.controllers[ controllerAtts.type ]( {
								el: $c.find( 'td' ),
								propertiesView: this,
								args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
							} );
						}
						
						this.propertyControllers.push( controller );
						
						// Setup and render the controller
						controller.render();
					}
				}
			}
			
			// Switch to the first tab.
			this.$( '.section-tabs li' ).eq( 0 ).click();
		},
		
		onTabClick: function ( event ) {
			var $$ = $( event.currentTarget );
			var show = this.$( '.sections .section[data-section="' + $$.data( 'section' ) + '"]' );
			
			this.$( '.sections .section' ).not( show ).hide().removeClass( 'active' );
			show.show().addClass( 'active' );
			
			this.$( '.section-tabs li' ).not( $$ ).removeClass( 'active' );
			$$.addClass( 'active' );
		},
		
		onToolbarSelectChange: function ( event ) {
			this.setActiveSelector( $( event.currentTarget ).find( ':selected' ).data( 'selector' ) );
		},
		
		/**
		 * Sets the rule value for the active selector
		 * @param rule
		 * @param value
		 */
		setRuleValue: function ( rule, value ) {
			if (
				typeof this.activeSelector === 'undefined' ||
				typeof this.activeSelector.declarations === 'undefined'
			) {
				return;
			}
			
			var declarations = this.activeSelector.declarations;
			var newRule = true;
			var valueChanged = false;
			for ( var i = 0; i < declarations.length; i++ ) {
				if ( declarations[ i ].property === rule ) {
					newRule = false;
					var declaration = declarations[ i ];
					if ( declaration.value !== value ) {
						declaration.value = value;
						valueChanged = true;
					}
					
					// Remove empty declarations
					if ( _.isEmpty( declaration.value ) ) {
						declarations.splice( declarations.indexOf( declaration ) );
					}
					break;
				}
			}
			
			if ( newRule && !_.isEmpty( value ) ) {
				declarations.push( {
					property: rule,
					value: value,
					type: 'declaration',
				} );
				valueChanged = true;
			}
			
			if ( valueChanged ) {
				this.updateMainEditor( false );
			}
		},
		
		/**
		 * Adds the @import rule value if it doesn't already exist.
		 *
		 * @param newRule
		 *
		 */
		addImport: function ( newRule ) {
			
			// get @import rules
			// check if any have the same value
			// if not, then add the new @ rule
			
			var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule ) {
				return rule.type === 'import';
			} );
			var exists = _.any( importRules, function ( rule ) {
				return rule.import === newRule.import;
			} );
			
			if ( !exists ) {
				// Add it to the top!
				// @import statements must precede other rule types.
				this.parsed.stylesheet.rules.unshift( newRule );
				this.updateMainEditor( false );
			}
			
		},
		
		/**
		 * Find @import which completely or partially contains the specified value.
		 *
		 * @param value
		 */
		findImport: function ( value ) {
			return _.find( this.parsed.stylesheet.rules, function ( rule ) {
				return rule.type === 'import' && rule.import.indexOf( value ) > -1;
			} );
		},
		
		/**
		 * Find @import which completely or partially contains the identifier value and update it's import property.
		 *
		 * @param identifier
		 * @param value
		 */
		updateImport: function ( identifier, value ) {
			var importRule = this.findImport( identifier );
			if ( importRule.import !== value.import ) {
				importRule.import = value.import;
				this.updateMainEditor( false );
			}
		},
		
		/**
		 * Find @import which completely or partially contains the identifier value and remove it.
		 *
		 * @param identifier
		 */
		removeImport: function ( identifier ) {
			var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) {
				return rule.type === 'import' && rule.import.indexOf( identifier ) > -1;
			} );
			if ( importIndex > -1 ) {
				this.parsed.stylesheet.rules.splice( importIndex, 1 );
			}
		},
		
		/**
		 * Get the rule value for the active selector
		 * @param rule
		 */
		getRuleValue: function ( rule ) {
			if ( typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined' ) {
				return '';
			}
			
			var declarations = this.activeSelector.declarations;
			for ( var i = 0; i < declarations.length; i++ ) {
				if ( declarations[ i ].property === rule ) {
					return declarations[ i ].value;
				}
			}
			return '';
		},
		
		/**
		 * Update the main editor with the value of the parsed CSS
		 */
		updateMainEditor: function ( compress ) {
			//TODO: add back compress option to remove/merge duplicated CSS selectors.
			this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) );
		},
		
		/**
		 * Show the properties editor
		 */
		show: function () {
			this.editorExpandedBefore = this.editor.isExpanded();
			this.editor.setExpand( true );
			
			this.$el.show().animate( { 'left': 0 }, 'fast' );
		},
		
		/**
		 * Hide the properties editor
		 */
		hide: function () {
			this.editor.setExpand( this.editorExpandedBefore );
			this.$el.animate( { 'left': -338 }, 'fast', function () {
				$( this ).hide();
			} );
			
			// Update the main editor with compressed CSS when we close the properties editor
			this.updateMainEditor( true );
		},
		
		/**
		 * @returns boolean
		 */
		isVisible: function () {
			return this.$el.is( ':visible' );
		},
		
		/**
		 * Loads a single CSS selector and associated properties into the model
		 * @param css
		 */
		loadCSS: function ( css, activeSelector ) {
			this.css = css;
			
			// Load the CSS
			this.parsed = this.parser.parse( css, {
				silent: true
			} );
			var rules = this.parsed.stylesheet.rules;
			
			// Add the dropdown menu items
			var dropdown = this.$( '.toolbar select' ).empty();
			for ( var i = 0; i < rules.length; i++ ) {
				var rule = rules[ i ];
				
				// Exclude @import statements
				if ( !_.contains( [ 'rule', 'media' ], rule.type ) ) {
					continue;
				}
				
				if ( rule.type === 'media' ) {
					
					for ( var j = 0; j < rule.rules.length; j++ ) {
						var mediaRule = '@media ' + rule.media;
						var subRule = rule.rules[ j ];
						if ( subRule.type != 'rule' ) {
							continue;
						}
						dropdown.append(
							$( '<option>' )
							.html( mediaRule + ': ' + subRule.selectors.join( ',' ) )
							.attr( 'val', mediaRule + ': ' + subRule.selectors.join( ',' ) )
							.data( 'selector', subRule )
						);
					}
					
				}
				else {
					dropdown.append(
						$( '<option>' )
						.html( rule.selectors.join( ',' ) )
						.attr( 'val', rule.selectors.join( ',' ) )
						.data( 'selector', rule )
					);
				}
			}
			
			if ( typeof activeSelector === 'undefined' ) {
				activeSelector = dropdown.find( 'option' ).eq( 0 ).attr( 'val' );
			}
			if ( !_.isEmpty( activeSelector ) ) {
				dropdown.val( activeSelector ).change();
			}
		},
		
		/**
		 * Set the selector that we're currently dealing with
		 * @param selector
		 */
		setActiveSelector: function ( selector ) {
			this.activeSelector = selector;
			for ( var i = 0; i < this.propertyControllers.length; i++ ) {
				this.propertyControllers[ i ].refreshFromRule();
			}
		},
		
		/**
		 * Add or select a selector.
		 *
		 * @param selector
		 */
		addSelector: function ( selector ) {
			// Check if this selector already exists
			var dropdown = this.$( '.toolbar select' );
			dropdown.val( selector );
			
			if ( dropdown.val() === selector ) {
				// Trigger a change event to load the existing selector
				dropdown.change();
			}
			else {
				// The selector doesn't exist, so add it to the CSS, then reload
				this.editor.addEmptySelector( selector );
				this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
			}
			
			dropdown.addClass( 'highlighted' );
			setTimeout( function () {
				dropdown.removeClass( 'highlighted' );
			}, 2000 );
		}
		
	} );
	
	// The basic property controller
	socss.view.propertyController = Backbone.View.extend( {
		
		template: _.template( '<input type="text" value="" class="socss-property-controller-input"/>' ),
		activeRule: null,
		args: null,
		propertiesView: null,
		
		events: {
			'change .socss-property-controller-input': 'onChange',
			'keyup input.socss-property-controller-input': 'onChange',
		},
		
		initialize: function ( args ) {
			
			this.args = args.args;
			this.propertiesView = args.propertiesView;
			
			// If sub-views items define their own events hash with the same keys as above they will override those on
			// the above events hash.
			this.events = _.extend( socss.view.propertyController.prototype.events, this.events );
			this.delegateEvents( this.events );
			
			// By default, update the active rule whenever things change
			this.on( 'set_value', this.updateRule, this );
			this.on( 'change', this.updateRule, this );
		},
		
		/**
		 * Render the property field controller
		 */
		render: function () {
			this.$el.append( $( this.template( {} ) ) );
			this.field = this.$( 'input.socss-property-controller-input' );
		},
		
		onChange: function () {
			this.trigger( 'change', this.field.val() );
		},
		
		/**
		 * Update the value of an active rule
		 */
		updateRule: function () {
			this.propertiesView.setRuleValue(
				this.args.property,
				this.getValue()
			);
		},
		
		/**
		 * This is called when the selector changes
		 */
		refreshFromRule: function () {
			var value = this.propertiesView.getRuleValue( this.args.property );
			this.setValue( value, { silent: true } );
		},
		
		/**
		 * Get the current value
		 * @return string
		 */
		getValue: function () {
			return this.field.val();
		},
		
		/**
		 * Set the current value
		 * @param socss.view.properties val
		 */
		setValue: function ( val, options ) {
			options = _.extend( { silent: false }, options );
			
			this.field.val( val );
			
			if ( !options.silent ) {
				this.trigger( 'set_value', val );
			}
		},
		
		/**
		 * Reset the current value
		 */
		reset: function ( options ) {
			options = _.extend( { silent: false }, options );
			
			this.setValue( '', options );
		}
		
	} );
	
	// All the value controllers
	socss.view.properties.controllers = {};
	
	// The color controller
	socss.view.properties.controllers.color = socss.view.propertyController.extend( {
		
		render: function () {
			socss.view.propertyController.prototype.render.apply( this, arguments );
			// Set this up as a color picker
			this.field.minicolors( {} );
			
		},
		
		onChange: function () {
			this.trigger( 'change', this.field.minicolors( 'value' ) );
		},
		
		getValue: function () {
			return this.field.minicolors( 'value' ).trim();
		},
		
		setValue: function ( val, options ) {
			options = _.extend( { silent: false }, options );
			
			this.field.minicolors( 'value', val );
			
			if ( !options.silent ) {
				this.trigger( 'set_value', val );
			}
		}
		
	} );
	
	// The dropdown select box controller
	socss.view.properties.controllers.select = socss.view.propertyController.extend( {
		template: _.template( '<select class="socss-property-controller-input"></select>' ),
		
		events: {
			'click .select-tab': 'onSelect',
		},
		
		render: function () {
			this.$el.append( $( this.template( {} ) ) );
			this.field = this.$( 'select' );
			
			// Add the unchanged option
			this.field.append( $( '<option value=""></option>' ).html( '' ) );
			
			// Add all the options to the dropdown
			for ( var k in this.args.options ) {
				this.field.append( $( '<option></option>' ).attr( 'value', k ).html( this.args.options[ k ] ) );
			}
			
			if ( typeof this.args.option_icons !== 'undefined' ) {
				this.setupVisualSelect();
			}
		},
		
		setupVisualSelect: function () {
			this.field.hide();
			
			var $tc = $( '<div class="select-tabs"></div>' ).appendTo( this.$el );
			
			// Add the none value
			$( '<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>' ).appendTo( $tc );
			
			// Now add one for each of the option icons
			for ( var k in this.args.option_icons ) {
				$( '<div class="select-tab"></div>' )
				.appendTo( $tc )
				.append(
					$( '<span class="fa"></span>' )
					.addClass( 'fa-' + this.args.option_icons[ k ] )
				)
				.attr( 'data-value', k )
				;
			}
			
			$tc.find( '.select-tab' ).css( 'width', 100 / ( $tc.find( '>div' ).length ) + "%" );
		},
		
		onSelect: function ( event ) {
			this.$( '.select-tab' ).removeClass( 'active' );
			var $t = $( event.currentTarget );
			$t.addClass( 'active' );
			this.field.val( $t.data( 'value' ) ).trigger( 'change' );
		},
		
		/**
		 * Set the current value
		 * @param socss.view.properties val
		 */
		setValue: function ( val, options ) {
			options = _.extend( { silent: false }, options );
			
			this.field.val( val );
			
			this.$( '.select-tabs .select-tab' ).removeClass( 'active' ).filter( '[data-value="' + val + '"]' ).addClass( 'active' );
			
			if ( !options.silent ) {
				this.trigger( 'set_value', val );
			}
		}
		
	} );
	
	// A field that lets a user upload an image
	socss.view.properties.controllers.image = socss.view.propertyController.extend( {
		template: _.template( '<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>' ),
		
		events: {
			'click .select': 'openMedia',
		},
		
		render: function () {
			this.media = wp.media( {
				// Set the title of the modal.
				title: socssOptions.loc.select_image,
				
				// Tell the modal to show only images.
				library: {
					type: 'image'
				},
				
				// Customize the submit button.
				button: {
					// Set the text of the button.
					text: socssOptions.loc.select,
					// Tell the button not to close the modal, since we're
					// going to refresh the page when the image is selected.
					close: false
				}
			} );
			
			this.$el.append( $( this.template( {
				select: socssOptions.loc.select
			} ) ) );
			
			this.field = this.$el.find( 'input' );
			
			this.media.on( 'select', function () {
				// Grab the selected attachment.
				var attachment = this.media.state().get( 'selection' ).first().attributes;
				var val = this.args.value.replace( '{{url}}', attachment.url );
				
				// Change the field value and trigger a change event
				this.field.val( val ).change();
				
				// Close the image selector
				this.media.close();
				
			}.bind( this ) );
		},
		
		openMedia: function () {
			this.media.open();
		},
		
	} );
	
	// A simple measurement field
	socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
		
		wrapperClass: 'socss-field-measurement',
		
		events: {
			'click .toggle-dropdown': 'toggleUnitDropdown',
			'click .dropdown li': 'onSelectUnit',
			'keydown .socss-field-input': 'onInputKeyPress',
			'keyup .socss-field-input': 'onInputKeyUp',
		},
		
		render: function () {
			socss.view.propertyController.prototype.render.apply( this, arguments );
			
			this.setupMeasurementField();
		},
		
		setValue: function ( val, options ) {
			options = _.extend( { silent: false }, options );
			this.field.val( val ).trigger( 'measurement_refresh' );
			if ( !options.silent ) {
				this.trigger( 'set_value', val );
			}
		},
		
		units: [
			'px',
			'%',
			'em',
			'cm',
			'mm',
			'in',
			'pt',
			'pc',
			'ex',
			'ch',
			'rem',
			'vw',
			'vh',
			'vmin',
			'vmax'
		],
		
		parseUnits: function ( value ) {
			var escapeRegExp = function ( str ) {
				return str.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" );
			};
			
			var regexUnits = this.units.map( escapeRegExp );
			var regex = new RegExp( '([0-9\\.\\-]+)(' + regexUnits.join( '|' ) + ')?', 'i' );
			var result = regex.exec( value );
			
			if ( result === null ) {
				return {
					value: '',
					unit: ''
				};
			}
			else {
				return {
					value: result[ 1 ],
					unit: result[ 2 ] === undefined ? '' : result[ 2 ]
				};
			}
		},
		
		setupMeasurementField: function () {
			var defaultUnit = 'px';
			
			this.field.hide();
			this.$el.addClass( this.wrapperClass ).data( 'unit', defaultUnit );
			
			// Create the fake input field
			var $fi = $( '<input type="text" class="socss-field-input"/>' ).appendTo( this.$el );
			$( '<span class="toggle-dropdown dashicons dashicons-arrow-down"></span>' ).appendTo( this.$el );
			var $dd = $( '<ul class="dropdown"></ul>' ).appendTo( this.$el );
			var $u = $( '<span class="units"></span>' ).html( defaultUnit ).appendTo( this.$el );
			
			for ( var i = 0; i < this.units.length; i++ ) {
				var $o = $( '<li></li>' ).html( this.units[ i ] ).data( 'unit', this.units[ i ] );
				if ( this.units[ i ] === defaultUnit ) {
					$o.addClass( 'active' );
				}
				$dd.append( $o );
			}
			
			this.field.on( 'measurement_refresh', function () {
				var value = this.parseUnits( this.field.val() );
				$fi.val( value.value );
				
				var unit = value.unit === '' ? defaultUnit : value.unit;
				this.$el.data( 'unit', unit );
				$u.html( unit );
				
				var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
				.css( {
					'font-size': '14px'
				} )
				.html( value.value )
				.appendTo( 'body' );
				var width = $pl.width();
				width = Math.min( width, 63 );
				$pl.remove();
				
				$u.css( 'left', width + 12 );
			}.bind( this ) );
			
			// Now add the increment/decrement buttons
			var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
			var $dec = $( '<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>' ).appendTo( $diw );
			var $inc = $( '<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>' ).appendTo( $diw );
			
			this.setupStepButton( $dec );
			this.setupStepButton( $inc );
			
		},
		
		updateValue: function () {
			var $fi = this.$( '.socss-field-input' );
			var value = this.parseUnits( $fi.val() );
			
			if ( value.unit !== '' && value.unit !== this.$el.data( 'unit' ) ) {
				$fi.val( value.value );
				this.setUnit( value.unit );
			}
			
			if ( value.value === '' ) {
				this.field.val( '' );
			}
			else {
				this.field.val( value.value + this.$el.data( 'unit' ) );
			}
			this.field.trigger( 'change' );
		},
		
		setUnit: function ( unit ) {
			this.$( '.units' ).html( unit );
			this.$el.data( 'unit', unit );
			this.$( '.socss-field-input' ).trigger( 'keydown' );
		},
		
		toggleUnitDropdown: function () {
			this.$( '.dropdown' ).toggle();
		},
		
		onSelectUnit: function ( event ) {
			this.toggleUnitDropdown();
			this.setUnit( $( event.currentTarget ).data( 'unit' ) );
			this.updateValue();
		},
		
		onInputKeyUp: function( event ) {
			this.onInputKeyPress( event );
			this.updateValue();
		},
		
		onInputKeyPress: function ( event ) {
			var $fi = this.$( '.socss-field-input' );
			
			var char = '';
			if ( event.type === 'keydown' ) {
				if ( event.keyCode >= 48 && event.keyCode <= 57 ) {
					char = String.fromCharCode( event.keyCode );
				}
				else if ( event.keyCode === 189 ) {
					char = '-';
				}
				else if ( event.keyCode === 190 ) {
					char = '.';
				}
			}
			
			var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
			.css( {
				'font-size': '14px'
			} )
			.html( $fi.val() + char )
			.appendTo( 'body' );
			var width = $pl.width();
			width = Math.min( width, 63 );
			$pl.remove();
			
			this.$( '.units' ).css( 'left', width + 12 );
		},
		
		stepValue: function ( direction ) {
			var value = Number.parseInt( this.parseUnits( this.field.val() ).value );
			
			if ( Number.isNaN( value ) ) {
				value = 0;
			}
			
			var newVal = value + direction;
			
			this.$( '.socss-field-input' ).val( newVal );
			this.updateValue();
			this.field.trigger( 'measurement_refresh' );
		},
		
		setupStepButton: function ( $button ) {
			var direction = $button.is( '.dec-button' ) ? -1 : 1;
			var intervalId;
			var timeoutId;
			$button.mousedown( function () {
				this.stepValue( direction );
				timeoutId = setTimeout( function () {
					intervalId = setInterval( function () {
						this.stepValue( direction );
					}.bind( this ), 50 );
				}.bind( this ), 500 );
			}.bind( this ) ).on( 'mouseup mouseout', function () {
				if ( timeoutId ) {
					clearTimeout( timeoutId );
					timeoutId = null;
				}
				if ( intervalId ) {
					clearInterval( intervalId );
					intervalId = null;
				}
			} );
		},
	} );
	
	// A simple measurement field
	socss.view.properties.controllers.number = socss.view.propertyController.extend( {
		
		initialize: function ( args ) {
			socss.view.propertyController.prototype.initialize.apply( this, arguments );
			
			this.args = _.extend( {
				change: null,
				default: 0,
				increment: 1,
				decrement: -1,
				max: null,
				min: null
			}, args.args );
		},
		
		render: function () {
			socss.view.propertyController.prototype.render.apply( this, arguments );
			
			this.setupNumberField();
		},
		
		setupNumberField: function () {
			
			this.$el.addClass( 'socss-field-number' );
			
			// Now add the increment/decrement buttons
			var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
			var $dec = $( '<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>' ).appendTo( $diw );
			var $inc = $( '<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>' ).appendTo( $diw );
			
			this.setupStepButton( $dec );
			this.setupStepButton( $inc );
			
			return this;
		},
		
		stepValue: function ( direction ) {
			var value = Number.parseFloat( this.field.val() );
			
			if ( Number.isNaN( value ) ) {
				value = this.args.default;
			}
			
			var newVal = value + direction;
			
			newVal = Math.round( newVal * 100 ) / 100;

				if ( this.args.max !== null ) {
					newVal = Math.min( this.args.max, newVal );
				}

				if ( this.args.min !== null ) {
					newVal = Math.max( this.args.min, newVal );
				}
			
			this.field.val( newVal );
			this.field.trigger( 'change' );
		},
		
		setupStepButton: function ( $button ) {
			var direction = $button.is( '.dec-button' ) ? this.args.decrement : this.args.increment;
			var intervalId;
			var timeoutId;
			$button.mousedown( function () {
				this.stepValue( direction );
				timeoutId = setTimeout( function () {
					intervalId = setInterval( function () {
						this.stepValue( direction );
					}.bind( this ), 50 );
				}.bind( this ), 500 );
			}.bind( this ) ).on( 'mouseup mouseout', function () {
				if ( timeoutId ) {
					clearTimeout( timeoutId );
					timeoutId = null;
				}
				if ( intervalId ) {
					clearInterval( intervalId );
					intervalId = null;
				}
			} );
		},
		
	} );
	
	
	socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
		
		template: _.template( $( '#template-sides-field' ).html().trim() ),
		
		controllers: [],
		
		events: {
			'click .select-tab': 'onTabClick',
		},
		
		render: function () {
			
			socss.view.propertyController.prototype.render.apply( this, arguments );
			
			if ( !this.args.hasAll ) {
				this.$( '.select-tab' ).eq( 0 ).remove();
				this.$( '.select-tab' ).css( 'width', '25%' );
			}
			
			this.$( '.select-tab' ).each( function ( index, element ) {
				var dir = $( element ).data( 'direction' );
				
				var container = $( '<li class="side">' )
				.appendTo( this.$( '.sides' ) )
				.hide();
				
				for ( var i = 0; i < this.args.controllers.length; i++ ) {
					
					var controllerArgs = this.args.controllers[ i ];
					
					if ( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
						
						// Create the measurement view
						var property = '';
						if ( dir === 'all' ) {
							property = controllerArgs.args.propertyAll;
						}
						else {
							property = controllerArgs.args.property.replace( '{dir}', dir );
						}
						
						var theseControllerArgs = _.extend( {}, controllerArgs.args, { property: property } );
						
						var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
							el: $( '<div>' ).appendTo( container ),
							propertiesView: this.propertiesView,
							args: theseControllerArgs
						} );
						
						// Setup and render the measurement controller and register it with the properties view
						controller.render();
						this.propertiesView.propertyControllers.push( controller );
					}
				}
				
			}.bind( this ) );
			
			// Select the first tab by default
			this.$( '.select-tab' ).eq( 0 ).click();
		},
		
		onTabClick: function ( event ) {
			var $tabs = this.$( '.select-tab' );
			$tabs.removeClass( 'active' );
			
			var $tab = $( event.currentTarget );
			$tab.addClass( 'active' );
			
			var $sides = this.$( '.sides .side' )
			$sides.hide();
			
			$sides.eq( $tabs.index( $tab ) ).show();
		},
	} );

	// This is a placeholder for the full font_select in SiteOrigin Premium
	socss.view.properties.controllers.font_select = socss.view.propertyController.extend( {
		template: _.template( $('#template-webfont-teaser').html().trim() )
	});
	
} )( jQuery, _, socssOptions );

// Setup the main editor
jQuery( function ( $ ) {
	var socss = window.socss;
	
	var editorModel = new socss.model.CSSEditorModel( {
		customCssPosts: socssOptions.customCssPosts,
	} );
	
	// Setup the editor
	var editor = new socss.view.editor( {
		el: $( '#so-custom-css-form' ).get( 0 ),
		model: editorModel,
		openVisualEditor: socssOptions.openVisualEditor,
	} );
	// editor.render();
	editor.setSnippets( socssOptions.snippets );
	
	// This is for hiding the getting started video
	$( '#so-custom-css-getting-started a.hide' ).click( function ( e ) {
		e.preventDefault();
		$( '#so-custom-css-getting-started' ).slideUp();
		$.get( $( this ).attr( 'href' ) );
	} );
	
	window.socss.mainEditor = editor;
	$( socss ).trigger( 'initialized' );
} );