From d7304444a120991d988989fb539081d2b6838a3e Mon Sep 17 00:00:00 2001 From: benweet Date: Wed, 29 May 2013 20:55:23 +0100 Subject: [PATCH] New extension pattern --- css/default.css | 10 +- doc/img/architecture.png | Bin 0 -> 15385 bytes img/glyphicons-halflings-white.png | Bin 28008 -> 32109 bytes js/async-runner.js | 339 ++++---- js/blogger-provider.js | 96 +-- js/config.js | 32 +- js/core.js | 885 ++++++++++--------- js/download-provider.js | 84 +- js/dropbox-helper.js | 619 +++++++------- js/dropbox-provider.js | 445 +++++----- js/extension-manager.js | 315 ++++--- js/extensions/button-publish.js | 149 ++-- js/extensions/button-share.js | 132 +-- js/extensions/button-stat.js | 142 ++-- js/extensions/button-sync.js | 146 ++-- js/extensions/document-selector.js | 159 ++-- js/extensions/document-title.js | 114 +-- js/extensions/email-converter.js | 33 +- js/extensions/manage-publication.js | 122 +-- js/extensions/manage-synchronization.js | 114 +-- js/extensions/markdown-extra.js | 54 +- js/extensions/notifications.js | 218 ++--- js/extensions/scroll-link.js | 389 ++++----- js/extensions/toc.js | 212 +++-- js/extensions/working-indicator.js | 43 +- js/file-manager.js | 635 +++++++------- js/gdrive-provider.js | 529 ++++++------ js/gist-provider.js | 73 +- js/github-helper.js | 478 +++++------ js/github-provider.js | 52 +- js/google-helper.js | 1027 ++++++++++++----------- js/publisher.js | 455 +++++----- js/settings.js | 47 +- js/sharing.js | 243 +++--- js/ssh-helper.js | 146 ++-- js/ssh-provider.js | 65 +- js/storage.js | 251 +++--- js/synchronizer.js | 470 +++++------ js/tumblr-helper.js | 309 ++++--- js/tumblr-provider.js | 74 +- js/utils.js | 799 +++++++++++------- js/wordpress-helper.js | 303 ++++--- js/wordpress-provider.js | 75 +- tools/eclipse-formatter-config.xml | 267 ++++++ 44 files changed, 5801 insertions(+), 5349 deletions(-) create mode 100644 doc/img/architecture.png create mode 100644 tools/eclipse-formatter-config.xml diff --git a/css/default.css b/css/default.css index 8f13ff58..249510e7 100644 --- a/css/default.css +++ b/css/default.css @@ -327,7 +327,6 @@ hr { div.dropdown-menu { padding: 5px 20px; - white-space: normal; } div.dropdown-menu p, @@ -335,12 +334,17 @@ div.dropdown-menu blockquote { margin: 10px 0; } +div.dropdown-menu .stat { + margin-bottom: 10px; +} + div.dropdown-menu i { margin-right: 0; } #link-container { min-width: 210px; + white-space: normal; } #link-container .link-list { @@ -518,6 +522,10 @@ div.dropdown-menu i { text-align: left; } +#modal-settings .accordion-inner .form-inline .label-text { + margin: 0 10px; +} + .accordion-toggle { cursor: help; } diff --git a/doc/img/architecture.png b/doc/img/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..812278e07335f154ccabf78cd08bd46c7e94f31c GIT binary patch literal 15385 zcma*OcTiM8_boc8phU??5L9v+G7MRA9?3bVgki`-l&l0LqaYwKbd!> zumlYn)7X(@nlUwXWLm#rkriKer=6+(q|($e(4jrZ5hDQt8Gq{tJQuzCqar(($-1@J zc`bl`00JG}dKN$^d$kw;j%+RYWZt}o(kBM>%QrsbVEm(+GHT&^#={l_%Ko70Aky%h zy=^4xcwC8fXNTVMY(V3rU3i!A+??~3l;Qk>qumEy5J(t`Tu+E4UHq72XJMAh*S6cW zE7o~BtlkoEy1!e|xUheH`Hks{KVYkV=3<6H!qfim0CyEpm1Vm`oA1Hz-DtiJV>Lg^ z#dl@+AW%|-&>uz-Oo33&w0Y^Wrm3;9F>BqAM%Vk%FA)gDtF1YaYe`K_O(K|`-P^&z z!7Gn5a0CSi#6?krvR*#9I^U^Eu%bBKALI*sv)(!~GQtc6fr<^04zINbX9dN9|eu<8@Q!;x|{zXAeQCxJ$NSF!_nz zgF^Vut6<`3BdX*OrSU}@W6cb1xab03@adrD+Rv|{)xCQi)@%J|<>6Ae#yhk}0VcKF z8C5IazkkmfI%_LlmJWu1OODfdkLcm9Y>ex@ns%U7TVX)-q!3nxpVhC2)KS*i zc%)z|S*MgLK0MbJ*wnSH4Zgk-YY&zMmb{hv-rcp1z%kG2Ji)KEBQj>+@umC@|LiXJ zCteLTJV`A4)VbF~f7PUvm}s{Tx3e-)>zz4$U|ZULlG#b=)qY?;xSAC#v)98?(lwse zuESOO^Y@0KIf09^OffUA?cbNQoK0(=-b<0e0sczAg$(kd8IHdmyz z)C-;JOSxC3jI53d_R*3%M2r4A?MeCJUrhM^U|N0NjWQM%6igHbsMsUGM+V5im9B6? zj|idm?nnqm=b1Js9wN}J#bJkgi1Lu|i2Tz%USCuRxS!jo((Q>qYI%A2Q9NWMOLmbR z0k!9N%?J?-@%y9fn0#40sD-H;9ohasW{NzSdKn(Hk|KrGXWWQ}h{$PJZ?*`8yF?$< z3c=8BD}E6Kf7d384jLfuB5G5T=&_2(2tBLmq0eDgw$p8rvX7vsNC+7_Hf$jW?8m#| zHe7B;%#Pi(U1^{|Jb|K4^O!CT9NBV$RRhw73tv-LItC9@YmZh9@EQ+BLVgABh+|@& zpyQ(T2-CgZ-MLhFL%Ls{A&-opK=dTXiYE~RFVD#_E){7ssjefotWC`j2Kp`RiNCW>jBU*F7i zGXv7w(-X%e?jFWHV~uD5tH}KuG0NjL1T&@#BDkJoLogQJ+T{D1nfPO^6c;7l0vUdL zY=K2*NPgorYar@8-H)QK{F(jqA3vagx$W|;{_Twb3`Mt$c&6B z@xzI?#Po*ARpy;Rm|&-0J+a@uKIflr@hZD+JRT7lneETaBKOghuI%*Lo^Spw&irdQ zU92i#dmGbgWSVlu`+LtVdLk%maFsRUfZqAeLh^fmr?xq3&#iW%^N-eD2iL8B%HZwpmo_50fEW>5rlM$7TKv za0{@Hf;@2wFnvBV^eOV+adS$CJj;~oSGdbY-pmsHcE(REEiS6ucP)R-uXqN!L)C;@5(?@pT{kX5(VU=5h+aA zY3w^QB8_v>z-IeNf#DQPHItyhso@k~Lz>~ZRBOH?(Cmj|V3G=QfUn$0;l?Vc-}p(l zADqx)H{V0}EZdq23!IKCBoSCe$8K8vFfqWkpePe|m3YP}->W(UEKyfwlDFQc3=ksI z)W;(x7_;iHnGA5=u{v_i7GEB1&e*FQjmVr21y_jM&BtweMeB9`0t_k^9HShvkW~S_?JH~w9BDDK7XK+&mg%2-g3|vV9)+({Pgn-xFcQ)yJGOyXRzIUOn zP=)5XP!~2vGH6bby%kr_fK?(2lHOrCe2=$MB;}|@X(d!@W>{oV#25)ZEr40nKl3|X zUJO32%?cC}5ZHT)X;52hUd=q%2rX9|#1ou})&|>)T9&jiGn7ps5yj`lz}^Fsp=ATjZ?>*Edx9 zgJB(Mh2C}zg;*SDr8Om>Qde4+UL4`8qM(Hz!Dlgf0*j+;|N3bRJR zEh$9wL>%x6${=~B$>3TCGFbH~uS4^@HPP5KXL_%|q5RJ}Rp}c4I+#8}*T@&At{o$O zNi4Nq6nlB9ZLFJHWgT}EM?TfD)?N}Qa|?ga7$;;d@NTBu?sPlCqwXSQvL=QuI)>bI zVIw(tCoRq~LK%ghLCr4LS_a1vfoB+HW@8Lo2|&Q9zJ=Ws&`N)l088gEc%-MN_ib$r z4(0&ta@6Ykr>1~`7?6HFWcDuuLR1cEki-hucI~ZPY@VoAS_H)$4^*pXt`4qOg-RvY z%sGNDb_CLsu?sVz@SUlO>@*ge5X{G?PuR_xBRrg(He*iaG#312cCRk|b?X>*>KE^< z3h4>HQE5re3_LcGMP_Rd*`V+%C{{l(t9(b%A*Q*|JApg?kt1a(MGlB{kp)Mlw!hfu zD~8u~4=RkRERW34CVECD=H`dYGtKC2*Dwg5Mawdu8V)MeP@E&NmIfBwy|$7v+tOWP zs6MXP+JY)EOicEDLqEVgUNsHXG5YfSL{?N( z^q3dxsS?{@*l-gpfhD0va9zXi>wC?rXD+!6OQ7IEd_aSl_tZCza-GfbjBR?<8R6hQ zdOv9QcIQ+{{`C)hL<1pa^AYq<9bg{?4?L}q4se-(zx<+)Z}7rR+^o~)7Sbw@sFg0h z8!hn9RccTcnI@@SoQ?!nDj^p(wuATk*=;@|S7{OQzr=&DF5G~{1_NeD`4+*^-g$VtEIAjK?SE#V5PlOP<@ zMRv?(C+H9key6EIFw&bnBOTn|ukr9q5$s;u4@klSNSGs)^&KQMP`${M8EQ*_VSS9g ziquOCWH>QWL`t(hGp=hn{zW7!tIqPcH@1AWRdm^F6`ipJcs}J!oA7@-R<46gJIphw z)%k;vc1@(tt>Qp9RE-OEb4F%JctugO-$YdBrxY$}f<5r%)5D2p=NeGW@x>SLs5;U> z+$ayOXb21jyWF;Btxyxed01)Hxh-4RuNnIFd6U%v*yA7ClqS&{@+jt z0D5_l>%)tK5s!VpzIw-*w*Vf|$8GH3e0(BoZI~;p7EGfxhW+RU_$G?;MilefCqai1 z(l|#%;X}P%frO;j&P$on{@zJVc`V}M;?3*TK!2h}01fSTxcRt@uX-sX<#FX(-4r@e zYXv(@}Mx`D28|d7p&`Qf*V*CP9I9mcy>w>k$LXoY4_>7Z)`W+W-l&GC zEf?td^^58?2;J?Z^6@xY_L|-EnkXUlx5tit%uprgr^rir0hmR%xTvVIYIWvI7UL91 zeZJxvDw3kCMqPygKt*K$|_uA6@#AV@umSHXwK89>{ z{D*7jFx4g*4cPn!NpX>Ni@XgIkd@te##pY0lpj1N~x^U6kmwzkKCO5ePC3=*hj+ZrgujLC|O~X?mD*ak?VR> zf{g%X%)$5SSN}x!P2b3w!E3nG;C@{4<|8y?Bn2qD1pARF`0qC+5&-`F(GcdG;y`b$ z8vuS3c;<~rs~+OP2l6q{>xJkE+gsWe8#hto4#++RV$tMx#05Ab5GX>3n}FEp9i-k| zqYOyLZXP2cBKWF)5M`18sfQkXP+{ErHKSb6a?`-@ zJz-zmzUt@SjEGwxERO=xK_}?)Xr_9E`$2#OR{woOA)aq%;Q6uY3yDE>g6vR;Mo*lk zLKGA{X3=GjEOaE?YlV`{l*c%l6+#k6lZ{294rB)-UKZr1cIPQZ5y)GU(51N;fuZ3; z2hf2R+z&?{#BGO$LCTwTO_oN6x{V&x)yO9&QQ=jThuE-(yXy^E8R+-MGch#{Xw(?= zT=qW;+7C`=IOBbc@!>6>oBskeFfS6+)$bd~O*|1lSm-2N=&S`$s6{m*T{Jp>b$}X1 zI@0`^Ka;==?qb^J7X&NL46e}-yC}f8mAU!*Jk0V>n)srzsBB=`8~1RKK};`o?v5@W zR}z(*nb}btFQj37f8U)I!=J|Uf{SzJ`MkFh%)86p>n%wM&__6IF$*Fy!|%)cL*!F)AlVgOd0s5dV)j7l=Xkj ztcmShrC#&f?V!j|;?EU4>{}r9l(rvNZ_b3V-Y6M6Wq-T>$=k1CrOFXPa8C8)|2zq= zuWs^q!9-7gzT>kPn38y}`W8F{vhf%5y$F*SdX+DScn!IsPgZDs*|W&NL{XHm)WqSg z7bk~e*s$W5S2}~LAng0U`rm=%%>c>dA{9W z%!o}9`9939xAaO5+21beC*71~jlz%Zi-@3T;X>Cv+VN5%A?N}3^N9Nig$&E76YgK- zpY`BGQ5T(L!Yl*!lz_%GtFiQAZ_`DcqXM9Vx|H5kYzQbo6!z^L;WV~UZFbeRm-oxf zk3B8y+HPSlYT(z!Q$~(@A_)5T){3%@VWSSo6<1S>O6n5^NG^r-dic8XbOgntH_!1F zfKX&XRRs4{|7aSL8+_zLE0z;cdmF1C%ujmK-Ux<4aw@Qg`+dA#^pBw&QIEO?DY5xP^}5LQ z(u~y9C8yi};eKF-h=xE9^$W_q$P3Ckg^t*slQ{ydzZet9hVl05Ify{2cy^pP$64zzWtv$H0$9y{~R zi3GKG5Pr0~9(==1zsnkE{HUI;l<~~0e+|9+t-`GFtZL*eQ|CbL$9|w5s)QG{>5QUl zlO(_neLX!`70KDF9q*nEkM_7hP5`>-6IqEkcsuCV3SQv`2p1RMg^9)hKEyV_Gus|% z>hq%c;d1kil&c`b??JNx?t*B&u~6)>-M_T^2|B?PNn^2ZV1m{6+2nZ1tAVuOR9L!b zu^miCVeA1A=cQ(wippVX2RP!S?PB17kUT>|s%xHp->#hd{Nmeyz}5-;(Zcr2mY~bC zfxVBp)b#IKzkUP*DST-cKUIXd_x#@G$b`r7;u5%Occ(sBuc?e@%g1JGAo{EV(JO2b zd3GdjP?01`HeuVOr%JHK2)l_wtmz(|#cjmotZ#AnHHd0CM^O1#}o!v;98lAPYJ)b|K z(P#F}%NAR+0A2^Pmi~OVlbjdiG#$kqP#sMPn@1#ab+=KuWk!iq?yIk(V>fgM{TQPu zR}T+8c>yY;@mK^w221`$#fvUq;Vpxd4e*)t3*FH!fOu_3S%xB2kVci}{qu90^tw@{ zp?RoOZ%wnf0W$~6?v>eMDN5?11q$@c8OiT60twb&w<4B@gPLXY(h~Wq(x2ii}=l7?6fFLl@N9lQXu(=vWHLTPI4S89E|)MpAPS z7TEqx>^Hbqe<7BRJi-(~Cbq`GmBk)txrJ@>T+?;1^80|X8)e*&s;wj*FEdxb&x@Q8 z`Dkr|t?Pck+TVgQF``{p%`oVI+~^lwu%|6UUE$l2wUyyNfNbOmbiI5!QmC}7XsDX} zDH7ouzd(XY#g!D)hN!=9VULbt2m|2XBeZe@kTSO9zV}blDWk1RBeK!(m?+e`TF#L` zl?bA?<(m%-Pq7!viO-rO-+ebk614p4h}`gMBzmvk?CWbYra_@)+{^i5Zk#e+_UiBM z4G_Gp&_IrABm9`$pcW&Cq$f<-eS zl2PrJEs%D?!$}|x02qf2+Wp8N=H+V^bC^srD-?HVbkl@aBU0U~{Ffr~R0oqMQM|dr z@xws;$;~@e5T#0~!XAcli;kqLd(0|v@73J1iAmZH#H=DAUUKyc;$MmZM&No2$nGJ& zq|wyJVrob=jsW{9tW6U$llv#sS_|<@7X%mC0)AIm91h*}eSl|mz{!1{oHp-8~1Y|MZVfq{P|>THe~H?IOR`7m;n_^57Z z?ijUc6ba>w+i0?(LA2gcjZxgf$+(zYcEoj2jj`fQwz2zXEH9|8sggBgQ5^+0y$7r{ zDQtI+K7t})R3=fpp_E$*nZ|IFsNe6rzT7##zF*Yht)fQZs6CC;8x1|IF|B{7rI&UX z1)LZ8qS32PEjXgIY#LP>tM0WGcZ>KgKox~?;meDQ$|Fsq1P2ZxYKvac><2T+1biAN za^!#01T1&to&9kQK5?~t9z-E7qKdmuQrHSv-Wm!)paDQZ&P&TLRF(b+U3!zV_&xm` z+Kv2GZtCn8NQRJ4csfMh#1^SH2&MFUMiD}Ed`-q)ZX)CffEjh+@+aSgfHpby$`!ha zD9#rSY6F0>*y=!Ck&Cw`j>6X=6R*sa2}~m=E^G4H$!wYYH{6j6FBmMUj3iHE1Ep4L znj!Bq$v7>$Q`)OW>(NYlW^rgedsa11YnV!@%Z;SuRm5fEv}pGe*>Cqv=;|(Ui%$sQ zO*GWiiB7RdpuY2CM)=u&$9dSsgCd&^OiZAlj7&#no)3!Lxc^Y%u)^YnmX?;diZcWde#oe9=-!N19M4mY@u=XSHNmnLqi4{B-aR@)#i9I(Z1m2PKkXh102#WinpRJ^Try>k5DGrkao5|okigtfRR0Q zeWJ>d`&QxWc}65^sbGX6*KL01ta;+lzdOn+_Osj-^Yv8H^*SjmdDQ|{Qa~Ox{uVO$ zEAg3(&S_KUZyRdbPbP%Q-|UTuoH>&|QsyiHH3y|4@VRQV%Cup^tdtlpd2dlxkg2iu zMq$n8&p(aKD+$BBz@kRH8^qd6f3eJ{DL*% z@uFOn0aPhQs-2gA^5P)~oN6*rETQ1dux3|f8c3y;6F@XCYty`;KTFSfpk< z+h7>>@U9tAm=^#jx{ zI6t}oeRz`#p`r%ZuKSereMYGVEb3Rh)4dmp!EV{y}d&R#?L z2iKeJXgRPl859yOGo%RX+7CEe0}}F^;YORs5;DpwhKs3lFVjA|VgCaX6EfQ`P5wt9 ze)e~MT+$Ev9_j-N;U zo=HLWPpDQ_)~qt`S%|HfSq( zitsl~gVJ{I8lriZ*|7QEIIUDUgiYz4}K6eovDCAh#D zLv>$|WimP$fN6ZVmSBh0KX-!D#Yj0K4J3UI|BUrjoP3c(I!k|8l5ys$v_r~nMgB2H ze7Nol_WIp9Za39~>oJ4MrfZ~1*9*#dR|G;Ri(o{IOY0@PgT0zuO)mTWG@~`QB>RfP zi@byD+VT3fH=w3N4kz(tjw9e#2tvn+4p=IcQPxOS8RW*g0n_3nSDO>vT>AFzn+l`v z9fY?Il5HXlXJR)**MmQCZ1`}oF1%<-a=2J>WY=0J+DHpW-OQN&L^_cmbGip!^(}^W z@0-Fq62uh&HT#WO=vnJMLqlvN&=($nabmn)(01Hn5mZpBCv!=Ubv3Swtx~Fc@tOk-9p_40L$;qSY4yp{e8jJsxJZ(HwSSG`;q^q zASaluLOR+oF*LWU=P?>MZn&zP3~LLSQl#~xA}&jCT`}Fcmxlfcp#&Lh9G}z({q9Q}?g`Mk z{&m)c%2nBu>m*hvM)*6$>#kfy!qM)@qR%8ty=T)@hiuj@^a@Z53%P#3D& zx|bAYg5Bf}i*bB=v!AjklPE|dh5N|EBmq9FOD2C=z*+!%QM&>}bMiPMn`qr%jUvh< z@jr2dDkEMh8esQkln@SV6)&fhBX5xUH+nIb$tO_U=}{j?_bb#K-f9>qOz{d~)JCjW4HcXpWXE`FBXkfqj{UR>_V^_G(d30b6Zj&uaWTlufS}G$i?45u8ofCfhWq2A}AWD8Vmq(9pQ|g;<|NGW=fQGkmuW(j(j%V`LKDz&1 z-0FuY$D`BqLEZlvsw@gWFXAAl@QdS8Jy_m+{dRP8w1RVKdC2MlZv zx|zHuWibiV)YQ7nPWIy6>Y6GyAC?37lr9d7`s?2XBUH4HBi@tJj^zy5Q)sEnUZ0L+ zaD#V_uHh_BL$sZ_Y}D?oS+q%JxL3GRE>quWA>`-F3? z(J;`(T(*`=|5e(q*^L($4ANlgUTeRKx~0kaV>`BZUR6q=Go(mFz*h2Ee9YG3 zqY-`^EdhRm@fV+_#S~Hx-vs$y+sJ_Nw*Um)zAm)xjZ4jt#uLw1I~~>uI_H2ITnxY> zYC#B5+h>Z-_M3E&mOqqtA}#yqige5#aPs?5jfrKab2PPbxm8r?U3pNkuZUTlDk0yS zA=jrzA4(-1$=;La9+q^{I9!^Xq+};imZwVPLKq=*gAP)xQ25Q={UrNk2+TdB7oYTv zag^oEf>y|YcLbkql|i0m$!Gz15GaGAH*a>w1&YeXyGzcZF0jh{L~eu#YIZb?a)Dd4n*V8Mul z7kvaIY)_19sxzZLU+hOiTEG4m2hj$dV0oSj(g zGyZpaN9luU;k^G9fN}ZKA*ls;N|fm5WU_!ArhdxIJf5j&Sx({E2m+*E5u2Yy#Pp)& z$h?2>$9cX90eL|5)~wOx=-TiSmRXx9jS$@a2_^FZ}vRULRr z#Fg=WBV_BOIe-ycu8JIp82AFp18TSY7wzZCfiL4xWiVya$^U;A!2LehQdThk|62i9 zC5%{C$m?vqoYu1xP2!Z4-x|C$P>wV`98Zp zzb2i$HLKBS_kV#{m#*r(I!oJmzL0+CYtk)V#852>I2Z9nH9I_D9ek)td6r!)gRSe> z%x5Z1bc(Y+(SeBv67TD+}UG6{?GjWnXoT3f9>0}4X8D1 zmQg5jeGs~yC>n0W>9FTCFR-OlsdKqb73ncBzvdkdw)$0=>;8voD8&M$2H1Z@EnVHF z8w(;?E;&`p0Aun9?PB$d%_zi@aNlhfSgT_{_tB@{)JMq>b3UbBXy`B}8staVNW!qHopuvkF_d3W30{j9cIp+Z|bQf`kLFa7fN3O)A zMUv}U=4++IP!<^LQ%C;#mAJu&om6K|Gl1es0ey*UFQpycsloZ@CDI-6nm%=a7yG&* zRpo&2V-GaBKly=#0rE3Ar!aqS4ySuamRQp`4-~n7aZQ8x(avZIbHl3EepR%&5^ikxi191<5 zTN6}u5?0BelbuCbDeukiyC(plwu|vu9OjX2N3%rg)#k-|isab%`OR55=qGE|a}xtn z`fG_hrTAcq%w0Q?Uy~8f$lHKR2ioUbK^J?y+=K6cipUf$5nKfmW8+=3nzW3P=^S{5 z`sakT;vb~H)7F3s2ikbk<-3v3W4AO(F>rLB2#*53p<2bzt|*8yUmb!<{N|40~p!HwAYomVWT5 zSEbS@Ce?QkdZj^-l>uSa7=55eZ$9ty99Xza}ku^@q$;lU5uUW^Q zrx(!PGQACYEwqFoE3fkbO4HmRTbR}pD`m1m)4+AzB-FY*dD7=lFveu2(tHMCbcYx; zI&PTho!RJ}!SpEun>P+X_zTSPxBkS}@aoI>l;pNUAHGW$pqTJFjfKt`RW@x&RWVpT z-|926LpQxc&|^5Gf-(NIZFAnX*0@aTPuvncvi9j76)0QoPDO6y^N<*;v9hEBMCX%b zuY=#iO91wI%h!3a@8<5VtNWLUPkMLUtIGCFzgVd?d&HTG?tapI2twP7S(f77c5*86 z5V#x?FLlhNv+>~gyCZ+(8Ur~l2q?DH7fM(e8j7(}E9+Z_RieaDL zld^-_DX-1?pLg$3g0dASMM_`nEuzZdDICOtv$vg1%3MV)hP^i*;3t+>@ExpqSVB zC8?>--}>}0jX!xt!k?=Rl~RULP3q=toddk4>KbtQhkLkLv~iB*nb!~W$cVM#UEx77 zq%gD5ubhGV(|%)SUx(IV9K4`WU-BoXPGWMSA|4IAm|8@@=}Jn6)DJ;`G6|U)a$weF zApm83A#e*q$EhaTioKWkHboU_s^&SHEc;cJ+@M2{SEt1TC)=Qgs3(`2Ma>^*F>`-f zd?J~ZYpOxft2Va{rs8u8WY6-3|E`6DfV@9$smp#gVag*y&?t{)`Lt+xeOQp!5?;lK z8LA)mb)lL^wgB5}JU$Ov(wuuB2Neqb_iS}IM`6Vq$%tp5Vk+m=sU8sSsFg2QlN6=_ zq-{o1vHkN$$29p_xSxtJ<1>7`*Ak}bdUc{Xv3KMs?j?gjQ@OA4c*Ng`<^}-OIiPjY z)$Dj2;%s{RPj8MgK-$d^+Ql$YpP3pdTo9L%kONf)E(q{oHWaIr)__ui0}{+#;TS3n zZ^+Hi|1Jl#SuK*iTx0l!92FHV(b{w5{0h<*UtiCi!gp5qF%O@iRObgxhOpraOyfJt zNwb(n$&2xV^r56b1LdrhhALxM|Esl=wCnvYx>0o+cH9A!HP@+tvA6cxvD;x_eE=%m z&OH(DS4)ctgDPc2(79)wV_g$Y1NZ-hKS> zJFf>?VIO{*_RprjqW>`Wa*zuR0NAPzR+F$l)f@s$z?H37n{SJGuGUBiCIEOR3BOaN z0F}DHD`)V&-0`|*kCOVumCb^9+fBnhz=Qqx#b@)oz{Pg!4#DYy^G#LyP4#i`@ut<) ze##PHYb|irQG9hFkp9#h^2L);Q=D1euR5qymX=))6BO-9T4nrAxkFmJ=)0S60p{bY zk}4ALJ^EySN|_i6%08Sf<(F*R+VFI2ce zFe@mCBkB3CbvctCktIs>RKlQaqPjRe-?Ri`P(kiPHvJ_Y9-c=~d){^NoOEhX>1GY4 zG8s08X&^{19Da+#_237?=p3-$W|?#c`rOsUm1eOBhk5*Lo=Lsjtv$-%R9zxaeRi>Y z+Bn~vxE6CINmKiGgs%lnQwgv+$!vRYoNVt9H3f>&kIGcV4Pm*h)KQdnpJi8hw>ULU zctFAxMO8|Cg=NC%BNO+GBlVAtf< zw@#t68hD~OT+S-$ve&d}G^_a7A@5Cbe*4R$UeDUX7q^F#r_MB6Y;dycE3~yx_qYTN z?1ONNqM+`Yr9@Uir?yT;pCR?9wWdeT@`)qROsQtlI)=P)p@e+4avdxvJMoTnuIaeo z2W?uYZhShaT-D|?Yq$0NGfQZ$goHinTGJngW0tBMf;eP4vzE6$wJ36VT=X!uB z%lHl*bBb{4=xxyZyjCMeWfACHzM6sqalOGG2Sn+LFHp0(q{0+9S$t56Z@XSP!((ik z1_I^bAEp9z)%S(*)+Nju^P>W@P14VT=}+UY|K1N4j2+KWc;{e+XTt_6dG`hc1by5F z8Idv7F{{a2748>$FUP_Ppa1@?xodGT?e@bCXQK_bRy%9Vmzd2Bqf6s5CB}9J{_5LK z72`iKa0kRua&9O`L#;onk-IL+TQjJ$&tFqFbLVaEe8_HSXkk3^_)@uE*sEha=-H!Mk2l`~X4Sow3r!L$<^HzeIR>zQ`gJADVQ;wLSINC_P0&Sc0CeVNE#=O3X59_T$}rjQs@c|8E&L{#S- z3QEEadpZg#F*OdXc)wCt|BwJMriVO+rZVQM;cR#rbye!AhMUI?@>bv^KFvI+MOyMHYt zVpBa6sT;-El6Ukp8*pBW?q1{Yv>C-G7sJ~D{Kf03WbCQuYUk8GfJ|Ap4p^(TC?OImlXND}?Jf5Av$FjH>J+zO1B&d(Ne2 zl-lGtd=KkzXI-Lx>ggM7vD$;a5t@l{++$k`;bp*Ju;#2Re=d1iHMI)Lq#R`#8FV!@ zwFv?|a&m{DPcQH$l$@v6i~gC%;4gH*!XoG9>Bk^FW| z4)UB$&sfI!uuIP2m|EWRk}8mZ%3w$4o1sh3NB=&ZkyIpqTAH9tViqQ*S?4KTm$;`- zo|rfdma`b0?o42nvkb4f>DlvGLm7?!*4$VQFL0} z-X@&FC{Tv=n%@@<;(S@bDELe;2AODawcV8=^oHbtHk08R(&Yy6B58B$JKz@tQgcR{q`S7JVmG)^=#Zn44HyPzmSu>)SdEBReO1Qbt4OzO|vWA z@UklpuXF^y>yD(_7icMlkB^OwZO$L8bnIbwML4)uRaN1bjoodJ<=)K}kBxAJ(?q?w z^e{Jn(Wu9k0KYb1X6xs85#sln+BY+k=C9<^!k=NZ1{2I*8T2u-V{fo$`RHSMS!qbK z;q^0tq+##Y%UL>B4}Di_jl5g#xSs9~j|JS+{eFHGV{@hcaCsYC0-@7!l`5fO4NZy^ z#t?3oC9m_7iXnl=khcM*k^)oi%7LyUQ-+IF8avr5^W_JYQ=->SbWs{btwI+>ys_*&A{o-H+u4|h|O#?0& z<-2&6w#!ra((P7Or1$9Ps2$$$arIQ^`}}dKq?|UGZI`ebFE8)X$l@Y4WP%Oz>Tdt` z7(P{^zeFwjJD7Zj!vFc*#cHI?R(*sqM*spiZ&tz0m zo4}#A;%A#7&Zp`gw$s@zC~3ZY0r6f$j-`J|A!DKRzA$tZqP*}FAtWR`;RiPkoD_I* zCC2_)38)qxGvy|EFxU=d&;6FBaPH@ek_hko_}zr0sHn*GWK;%SzS3%@ykl=~?+txS zQ0K=Ht|FbW$|Fsd0Ar5evomKF?dsjq$7h@7p*L>*SXVLM(xLC#*Y(As|Cp2N`P`f; zx}}w{o4Id5>6?gQ zP4@fR`)}HQ7@D(bS=cvAPlTByX)CL%pZ$GVGtYqu>3JS@YC1R^n+Odz#Ql`aWYp_D z_H@EV!f}s{mZRrRAKvWc*52OP2}y{JB}9FIG$hu6+*&_CVu>9nkY_KcjCp(2a_8Z? z))U2WT|_6#$jrQo)~3sEF`g$nkjz_hax$hE2!kmRqJo03Kh}9!b0^0rybbq9PQuLvwXo0 z=TFOb7or@WniADfQu=N_&hE4`t`D!bw#*@o?C#}KivP}?0B;Pxdnn&A7#ENr5uYHNXm874%V5AThn6g(mvHjxe6*Y)FxL&+WMrN zMPiIg==NfDzjO7F4EkAHdlBdHw{G)&$&{ru<6^+g8Y`@*$m{C*c(~=`{$f+NmqNP8 z%_E3B;d3lo$e%*}n;i`sq}D&3p$`178N}DDbozr8MQ^y=z|SCZ6-r~a^X$?<_0>H+ zJ?+g`$wk`@AXmvJj3DR#U1`7GM`F5(^8Ig^==BPYO9Tn0X*r%X@rkg|*|ZV3|2JnP zW&&Yyn}@pn$!T&BrbWcKISCbag4JtrtE#{`q@oxp%GQ zZ#{enF+0_)Zt)Or5b5Ce+1^|nSb$z7y57t@HjADoNBwM~gBF^LZkwsaGZki$&U_;s zB|bNnV@f8EVV(d3C8W^pQAlRb%a<>o4*Ynv>Pw+laDyjxScLkR2!i5Sv>v2wve{esa3G4N z=9l;X{+)dA1=7&e+}hrE36>N;cW+WvQd%CA>6t24iKBa=qoYGEM~BbI!Xo+k^XHjP zP+=g*8LTGr%aiTFjVf0mR#gACDCa#%FRz=qr+Tg{Pa0hIMgBVMjOS}W%l4*%+3#Px z&3+%7i&fLnI$4o`+6izC%QN=-HG0qh(avp1khZ&q>_sZ?~9O+B3QUw z5iU=pGXDOxzCWsmeoA})EIK(kO~7ELH(dL{{Z*9f>;#tVoD;a0BAXpEKVA$XhFqGD zf1hlR4URe%*_jJK#Ii&Wp{1|kn^tf*d>h<99g~6g1NFt6Q`+IfYMG3G;bq7CJ%?8hsT1Ysuf2UTSo(_($qbXfXEMeZYoS4ePo6x(#UN*I z4{)Ys+D^3a34ZHr2J3M+D>3Ox=0_g7T#&jgBuHD0ABIIN4H$T0!|+IYzKsO|!R}I$JjHDK;Z&Kyx_XDJV(%bLcAafSqQH;>auR{SKnNW24~yPiE1C|cHR+6T#m2^-)DeYCubjGgsMZ zNh+|D_V<1kVM}^LcNvz)*&GyS)3gk2i$`_2xeUT%Tie@Z3fA$n zDWAW?VE@1cCMG9ut4~!DNe>|d)FdIjk#PL#8vqmc{= z(fL}7gEFdV}c-ggY};uusl zf*WHyIV3&JUd-C0wFH{%s=i6DZ3u&sepp^~Z4 z_I7dNhtlXsa+RDaH-k3bV>X$;ZG5h@wK*p|Mc*HzYN`mcM)h=(z~h!r19vHfPetIkuoKTem;Ehe zZLG)3#WjBhwu}f(|B*|fXC|XM;nSxj%@=<{HSUjdG$uBEz|8f^0}B`B;R6)-) z<3x%Wy1mI1%C!)f1PHJG8o+3dHoyz3<>UgK7=LG=c`-^Ob|4213J`@n%%`wpS1pGmEZq;V-&4Sa~5OY z7gr$>PO2e694FRQx8|9mEA)|+y=JU$EDpa}5o=Tfb8@>|+qegt+*jwS`YMBl^wpsV(de2V?hE=S^iqp4|{z<&^hI#M3k|0|ntm$jK>d z6m^axFK_+{Q#5|T;Ke#+`A7oVAi(2`K-QTa!mQ&p#;*Ival@Km3<*^99m!u>)rkdC z`}9)^i=l38zIUj;IXO8sNCro1y&A+*e~}M2no3H!ryiBYoi~|y=JJUn@$1sQi%I&3_gMs8LW;An?~&IPKn1*uuVrLpc%l1ENLyUD zL7L()#1D=5E;gau(!#&fzO~%fag(P3EW+g zDUM0E@{{r;yybGD1oWs(k@S)i)hI=3T;f+IeT3I@EjMHUR$Y6y5NwXJfs^E|v0IyG z*QqlY`(!v(tQ*5xzm>W1i7RBHKx4Hhik6tu6cYeNR?wAdw^g&C0IeY^_As{*;_rOX zK2A>DpangXZEEHu#{39i*AH&Xc(Su@$!~_a%tvd>h1Uux3$GDK2BN-H(_xcJM4Mf~ znQvNdPfLot>BnBpI>Sij#_?FLewqHYqNJgTHc6zD&9vo{i>8ZW>E9^cd)Cm9QviFr zV$0{H>Fnwb80ja)V_IcPG|(kMeZKW@M6 z{`hKVd;3*OJpa5_b7Ub4hrRn+G)^5|y4CeFnn!iN^ra;oK?6xSVlT$SQ>#_=sm&AB zLZFIaI6FHxlZAGsRJ*9(QV2TB*#|G}<|d{mmg-TBKf@4bk9(_36Ga{B$%G&LRr`io zw*`PS&^8IBp6N~n2fZ2ND%Wr1fZuzCNm8N$+xxb8DK+EQMKh+^=#A15ShaeZz%julL0XR$x4k{2Irs@fYA!LMgKJuJ5bmI5iLipxn z40J?Ba%>f*I2zq49}fmG*3cQ-`0SE)zv`de-Ju}{Nk!n$?d6=uU}tZHg6 zNW(?`M0;8kV$Yf&G*e}Jexvtvo_(Xn5aYe5AYouOL#s*U5SNq$umC?;l8P4Sk#MO_j*sKw$@0L{q?zK%vHuKy(aZM#;>GGs zOc3CrOFUw{4$wZmR=lWYll!SOy8oZm@11R?*tbU;s$PlD^re&|=4MiYelk9SewLQn zHk*ho@~EnPn}R>5)j>J2zO@LAS*`5_of$9-(LR!z&*q4zOvZ*ry?w%DlWVZYv!~pK zM6PzJ$b3ZeVNCDy2|Y!YhN0`OFPavRoQ7IeWLVg#&|F5E)zPb|q5*DZhNIr)bmZkx zV~&T=!PQOVgHO)wRFT~_*eDNzBx&U9ya|FOlY%~}iix!iLX?hTJ~_zKsIO6|`SN|3 z;*$P>yQ|-uOGlKvaBnG2E`PHfV;y*qzWIpfVJ00xbs|hG%Wci4m#n zu}v~@8Gu7mS93kdn~0GBEHdkK5wFWCo`kPJtOAD&euEbS?r!(*y23~}**(s7bCC>= zHwLHc9CC}Qd)L@ry!bK&@4UNNx}#{^NV06+XpdG`zulXwq5Ahra1R{ue96xD=6F{@ zWg&4C*WLL2wQjV|Eo~Da;vH-Pt1Q4uFGgQ8<~j#BDf^gq4j zh3A~^NO)rLCc=qw7Ce1G#sN1w2W;1n%pZnhkEE3Emzj%8P0Rsp5C*^$|?dj47B*PHAzUH-iM@J^6mxmO%b575IuSF`5v($K&{2Y4lMJ&xkS^7HzTdU=8KDD3^CEr zb((mmkLc5YAS$2hR{({(a}FO5II|gz2AM8k3GLg~% zMn_Nx&U&9@QH56|{^E|_MA$~inQ%UyxDGc$fVR=Pd7O%6aQBX_2?eS}Y$PKs@3zqs znXK0I)MwY#AqL9JIa;)3Vd}S%G>3MUmT;-fG>^YhkaN@B?UiWrTJ$^h=}&WooEnz& zrJ0P*F{fy8d21E7xx<_90zUxJAX=owpzXn@r1)m zN1Q0a!^7nX7_eDo6N0R{Bw8S9S^PBDKM$c+pX+X)e<%v^9yE`-Vw#V1f5xJAg6dqn+I=97wyy`iWcG5@e98i!hGyA<`{->BDLD z%Z~ift zjZ;rye@I4rJ}N`?nT?sb;^q16#pB11`^m`2L{s?e{@4Jy36Q_?uF&V$DLpF*P+Hwfw{QNQwq8nl2cSGa$Y^`X)~J=$lnwPepGx z1|m@ByCSPMC*9VMC79sor<2-3|4&>yZzAo-MYp1FF*QYTs6?znmtYbhnIj%R<6_0D zQ@`^xyG#3bai*}arEa<2-JHL8_Xc-H{M&{N#k9c6VK-H7bu|ws<`To$^mfyaYar7~ zN?rgZdxRFl&vK?^=Hl*hs^#`;P;AT1o{fy|xP0x8RBOR2UbD%3vY9-DBTVvZXXw+Z zn(FEgsGo|d@bHu^Ety~HHM)*i#so`#Ea1;8Ep!MNgo9dV=zuj&{+$j#xX8K}=seLm z<_ape$MnGRsp*r6rUR%1)VCLja_?D);1!f+8j%Vnh#wp~TU%SlwF$}B2ptvSiB#B# z$1$=!3><#E20$C~l=}>W`vF-ZcrDZzCIQFAjuN~@OM{IR1&gz~6;-z-YMY!_lIh$N zI&2iSt0pMmhj35PBB?4S^Lz$)Z0wcwJRcy#!28&pX>ef!OUeRipd8On`D59Sr6k1M z^t7u1XeTC6DbVRQUZ~{Aaa5Q;e)I@v7^QT+zGUS>QLxj;g+I?$;^|-hPOv?jF{1@29bBK{ zaVme0rn#SM`YI|CfbToE%+!!~8v>f2mzQT-p3}F^xsy;f2)bX0;(MA4n4ZB zcm^)?v%LIwwYTXDe>g+8FML(Cg6{mp!~46qpkZ!hf2y)Vc3GNnam+Y0J)&QK_3(o3 zq*c%j5pCl3P^4(ka6le@kWvcdS)Qa2dYqH3&BN@kvpL8 zi?XxxCa7BrB;Qu~j!~!LtO;X7^$Dfs9fpMy!bv#k*GdaNL#!9a>33nE<)(<**NW|7 zz`cyy;inO{h6c9Zis9Q7f_xGF-K%x&wHpCrdXJVjNjBq=YL`qFI1rR5wz(?hK%FoE z^mv&MdsyfgGbAL$c0Eqx4=@bw_{pu=Q-kkC>~Bj~i|6;thl`uraBsFs;l%WXyFL+H zBFDC3Af~YWeE0ks_fDF8O#@bwjLm|N(W5kWuANr7I(qE(Lan25!<7>I9zfz1j5Prq* zG@#O;zRpk)S0uG+YtK;`!&3K(;dedYr@aSVA&MODS~}5h+H$@JAeUU1l zt~>Q&VrE1BzgYmH&^F-Zz)xu$=9D|!QpOdk3JTye(b3U!##(nUYv>Sf+Xuuv*Q~}?CHw|a z<6``n`nK52zqL1Z)NXlgGo-a(P|fG4G^7;Iy=AYUWK(m|cI;^j#E3Woq~$`BdOG-R zZHjt&X&n(G2c(CJJ}~{`gyvpzn>~)17ibeek7(Z!d);Sy%(o%!BPvT%GSLrn)S; zdDlpY)EBS1v|KH`K_~M_ANjOqiENqRhQ7nK8$fwXAu@O_?5W(*LVLvM!JI&w3xf-s zo}8>Tig;ewa*7)%u<|8y@M9XV`0_xjET7Afn%)U=Lp|sau#flvE{N2Qc}DXCt+}*x zMd9XFPq1LtDnEw0bxSG6lO`kc+2v~^oJd4lw3!tM<*VruC1Z@#PcWk&AI>vl+ zrn&a-)t=~;Box{g6cybZG#(E*RPZx7>dL%(_gKd^Pag$yn46&I2Oj3|(>tZu6<^V% zQ!0_~<>Y;4FD^IKFo&b(Rzh~KR{9D%eD~&ptpb+z6=mSNfAVwF+JU4OmZ0bU5ZKc- z+e$S^jJzTYkaXtnezulIi?iLC<6*)5##N@R03!;_yOvC&2`;SBPlQrZQqSw~KLT6X zc5EAqXXO`c1Q?H>EG-*sfM6V65 zT~o6!V!ws~iiVtP)8f^NI7e)bIj$5QcsCGMDT|vgEe3!@FISt;gDM-{tn%SQh%v`Z z+w}|TkdMQV*+*e#{Cc!DQ9={UByh64V5MHa?jvPsnV?2jv{kh36N%|}bQg7b_Q5%2 z@B)~JY{~3~4}h(1B`*j4tg1S_6umu+G`-;6Qwq->#9tNs{dh5xF~FW*#KBQnQ!@x) zaGr1~(OVrdKMGtQGc&U>EwfaozaZ%#0vt0wpg|8){9IO;IVkCcpSt5=phP%WB;|-H z6MjlEEx?RPU*)5Vk#$>Y_Ietdkg$*B8xRoicnB~I+e`Q%ka>&$xGiEve(1Mjx+1WPVhkxLZn?cg z0GFOA3<-r-^D6!^C|Y2l765-l_>Ng05t3>wTycjM9xR5@nho9JhjxB{n+>EcB_$Q1IKY&93(+%3B63B zlkJ2z(bK0L`{xee_FLDj$0Jm76{=QM*&dXRVjfx~odv^S`qlJtNoS|EUA-F@^R{{Z z%QD*9b0$8w$LUKHa#|AGs?TNHLFR(YSgW}#FYnVB@Ur^++kVpKT8P+>Q-Sv5`{&Qc zIfW+>;4*{1O3_yB-Dsj-(?r>Fv9n9ttxV~8>@C-TVh|jE#+oeX^7od_Yx~`St5AtY z)1pnonreSo;-OKmarqWtvoga1@WL@&`?tW9IshU%OD(XDsDx2dnG#c*^74A@7(Zih zD4@R|Qi?S!NMW-*+sEMLLvLMYRL(Cd3da;4exw!u%6V6l^Rb9=M_?59QJ0;L`wR!C zB4dHdpra?G#GsMb^K8|J#xo^pz!3tnb=d;T! z+JuS><=4DNey9+B1H#KsEL(YbtW>YFnQh7J*VNy=ztShli7UsRRNY*iXJux#M|e&? zsLg6ty&a;<6D4TMRmIaz?uG> zU7o)kj7NqE@}-rDP+}x}E2!GvgaZ0-wVJwmd?X2H*N}s+F>wnsQezzd<*P}}5<=j% zW76^)Xc;Xh+BnaCfLa+qVn|E7qp<>Nd=e~KIF#a$Iqls-Mcxf^R&S!ef;<2PNSxFH zR&Nn#F@yY*3*dv*!&>fsZTJ3{HP_eowr%|L%Js=Gd!@b5Hio-(;l|#JySkMn76tAS zw(g!~f&8UBuIcqU?rmIh-%J5rR(BV}Ko^i3M=u1VeYf&;gtA`f@R*_mD z|AwNpXDmq<@`uHk?ox9(VC;3;EHGEi3M{KV$`KFzboM-;*V$y{y+j`zV4OGKQZ5sf zv6GgP)}kKA8^^xQwdY6e$jueF`~KBON|_G$CC}XXrEBaI*Q7=Jv%JWzEgzAfvCLC@ zSVyhUO$-vyyG_fZ7y?`4f1tQwPuu?q{C>2j>c8c+!Y9GT!;beaF_^Z4ZLn@SLG#m< zb-!)jPIQ@Nhn}mNAMj+kxO}xpKzvzD__g$PkzzRzzs*NHk%3v_T25;NCR`fX97jRS0Ag2xO@$X zHn_AFV$G&H-$UQgxZIzBOxown?ADaaHY1fqi6JUqXb?rvRR1qE3^b3~n1E6AbHBuA= z8{Uk5igkB)2YH1b*QWr+N>I~;bg$BHuCm$=z3L5XQj;Cy0`&MNNcFhWJOcWq+$HFb z4fXZ)Tidzm&20}`a?F7_DHADrd*Qq)fTvtFx>!68EYG)e_$MH;CXFJNN1gfo-Dg}H z8t~GjGgjRQS`Im5=9vP^NQ^hefG?2@|zid;B@TGkvkfhDyCe?}o?s3M9W|>0<~ZyOAJN zR8(RfD-f!j!E`6`1yRm`I+wgMoQ*-->MnQ5snaoVibht8jWh1e&H(E04ubs5lY6Ql z>qUoOhufsRCX0>G5Iz}^!20s~C;8oUXa!QM99hebcL5WVGQ)+nv`w3o|7 zZ>F3;P_7~(af#yC?5~>Dw=$cuq9*T(6Ds@ZT(rUT471|6b^Lw1OY_m^yvB~?EiC;9 z?oC^}yH=#4-q&^3j9d4ZL=-p@430ix6>1LI=LtOi3Cs5Y4f#l2BAs^aQ?pjLsh%eC6;L2bwI>qo2mF>nBlfvn5rd z_$%h?9L%ac$hUeN{MAHRIMv{v-2G#mL2@uG>7RAVaA~j%WwK6m8bOjoS6vVBZ-}N) z$arg%h1V3x)Q5Y=YaWCH9Y8p6lxuL-XD65u0DZg7-ARw1*ZHb_WrgK&>u5GSH(XA= zog{F(YeDDTQcH(&%Ks#XMr{_*Rm;G#83WNUIE8>}WmT0j3sF{DTAGMLRc>xBKV{@t zny`lzcSmU6YK8>7`q@2n4B4 zuy0^XRhCn92^UWj8-eBsE{|-qB#;CDdHycG(5a_o{JYFA$FD%v%eS90>)PgMZ;8c= zF8k~lF82Pz-2(egovSV@^AEFIksH})I$+Onh+DP!Nab1_eF!hiWb96@U{3#X#2oS` zqrt%$e8hUOLQc|7F)(7Ax4}Yk(B2h2YRBizIni`;DjZ9Amf%E4%IXRh! zfAzV!Jk9Md$5c~$=cwBP%mt7$H2M!fEU8wx0~s986qf!V=`0Xx{FfxtB+BKq$Yd$^ zl!yKMO#%vTzJ8-JVxZ|-+6`JYvL>AVn6Wwj<)f1f!Y9BtiSZC|PshGU049St3z2^& z<2t}X!~C=-x0=ltMVm)XY}jQlRYW=aIgBd@^KT1a0OH6et86<90$I2Y00^~!RKa2| zFNt*}q~+x{e;dU_j`fB^SMtuzPK}?uoKL}1K%;>J%BE}I``mvcT#}|2-Rsh&M;A+e}8CMTKaqHo~t=!3ald_!bsOnTb~1L1jLs6EP2W+qW~xQ0X&db zYo=)36B_14ZoQAj_*~5`sUHg6WsEt610hF;xinKR-U8&D$TJs6^ENFrvQ%vf%C?L&2~^J4?xg9D?8idiSN}RTZAoN z*gf=JX}_Myjzy!S{*&ANguF- zE@?~dqH~p`x10u&L^m_Q@8_lBx2I#np1`#_1+?ctl8-zf#~dV$N4T(nZte=$uB5{T zmu4g2F`=__b4bet>>ME#Hww{o|Gu;vJ32p~!o^&4;>9b@&hD(% z0ft2e$fIkz?&RlIPmPaXx6<5w=O3~27k$@&f;7O}J{iV|~6%j7x`jx-5~d5nig zCu^T67)QoqrD+mt=rn>tU1`{XrXdcvDX`5yZroFulyMj8M(g;_<3?c6EnZY$aV&Z4 z3m57&)mLX{ufI*(WL~Izl1oGPAL4@_{7a6u09++3?OCdpQ=R6&ozDW`_1N-b_LKow zB!5-!KqUqH1vod$w-vi!8inBA!%%(V?0W-ThB6!(uJG zxS?51Qxlk_QE%uBL?S>WoagiGiL!~wI;P+}TjN1{6q$AH$_bz+$Gjn#%b?tCU;lM! z5u~RAjpY&Om;+=jF_&gril2s3_>UPB(=ee=78ZWm*b@U`rwC6F=Kp_ipsenP_J<0< zxC7G}6Xi#}HpT&&b#){Y6BBr{(cT@$*Y9hrB!nmxMaxxmbOubkT|!%0zC@v=Jn?qwvT+N3b*eh#G9%F1|Vdq2Sh$huKi*ve5aTwP16R{FvBf5XW+Aa9HZFfUuy zxz>Sc!k(YGYXgvrOub#Amel;c*Lv`Py`{cNDrCR*hHo|Hv+>HXJFCj@JX1f`V#e2e zc+G;dHexhO=CF^sq}G%cSiuXn9azUQk(A-U0A;njvIliE4Y()i6K|tcE#*E-NZ@th zNtHclVw3w}G)1z$T*UQp>&cdUeLhm@mrca?$MYszgPLhZ+bIPQh5}X?V~E4ZReh_~ z<7RYB+UgbkIMHPLWJAM*aDl>r3G%{2A^j*DJ>I^)wQ_8_RN#S3WB&YYO?3YBNhaf@ zaZE*wn5WP6O^TqxuntrIt9{XqGN3-#gm$k|)uke_gnY~c`S$|g4#9nc2~eJ4P;Z^( z#r&HfTS?#6JmwbLjcjO{BINj=xvmFRG4}tPu); zUM|+$^9zv?hdyNpd0g_{jJ~xywnz>M=_hXEgKT~9HE{`B#V1={HM|`Ut3rE({3KN+ z>XePH(RgsW?m9PyS^B>wr)*z>^$R?7Vf`bLN4G+KccHesJZryypx1#wYtHfyhQ6@1iR*1qHE z@o^x9pP);6L}A?~b^UYC7|U=7>B-;e@m-Gn_mHRtVdr?i6V$v(RF`88B0)+YKJ0vo z96=fiqJ?f3P;d5a(CXyl*YTy4=bLlCj}py6u>khSC-W4gf_9#&u%2m73KA<&3jSO# zN%dzkcbAoV@>bT>Ybj_^Y+pf#6X^}*yEiZ)qENB;b-g0Jdct=JXG3QuTGA{;$iDW; z88DISq@3&~K-r29yI1W1$X)4WNZ&QLvXTT_H4P*$(g1!TPL6BP^O24I{)SE`MiFES zfSX_De!4Sz3ZH7Z0reHN*Z8*@Vl%@_UI@+L79{F{_6L3|>FeubEI$uyjLVVu0767_ z`oPz<*>n@>Ircn&gD>_9G)lZZfXo@OZgk}0=%{1-@87X(xA!v~VA7#QN13qmC8ERD z-U-UV`I3=n9n0Mf*fZqK+Rp)^Uil?1DIcF5X+eL{A>FBWSU1BmTa^!5dBAhfEP#a* z2(ZsD`CA0!yj`zHBa0769>oFxO0=w)7P!HZKh0-fjQvbaO(j6mfqoLU@<=Hg$dOl* zY_hU@BafBsf&45XcHj#Tp#j5Qiv66!%ppgI2DyWY*LB#r*C5RO-yA<+4PmskAk=>e ze26JH2vK`~0ExeTz%vzrvK@hVE}I!2PlS(Rh>fR)uMARKPe7TXla01KjRe8CX(<*W z2Noa89v6`I13@}Q((w0SRzM6_9K`n}CG8pvpURJ&!C-HhHHzoWzh(vniFb)fs+NHn z6yT4|#7BM;Jx&7@YXI2?6;BGZN+)OM-|&(a1#n}6uMNL$ycw&^cvPg-Z%@mjDJ}U* zO8I}GW*>JVnKM*Dz>FL_dF4It`ZL@p<41Iqk@vA7EuTIKK36Qf`}i6|nkx+rhB!F( zXS{Q~;QK5@?~c>gj)v!eCq8qu-cOrzv%9^W0XS0SjZU+tnomesZ&)JqGP1i z=;!YsNNCsrq7I=!YV|{X3>*!R{GZ=H#~}+#8171cn0pN}Ic8sFE53zHGKJWKQ6^w> zm+Jb+kJ*9|5}+!qmiM!E0I!48SSt-NQR02t!rHLQF}iuoA01f?XJloS1_N-ABosZ{p8-Kxiq%vDsF z|0yma85?afbKXN6se=LmA~NUHhWZqOKo9Z<%UvG@0?y3*`l&m*t`D>5@G*B6ixIM~ zeAljBDr1Nm>yZXDEiL&R+;Ug2f-?_YuYL9#_M7#QN@;yK=LJDX3oIm%mm9%+VTNp$ z>%|$BUHBnzWqF~#SRXfBdaT}fM@3flp%CRTCG<4ypOvNWuPP*f%nySB$k$|P)#Qu? zyc5V4qgBhAkTK-XZ;+Nh+XRbb6o`+BK!HY6Q2@$3C|$`wPIV#w+BOD=Q#nf(&~`ER z1xE)7cJkj{qd=fXd}fosxbX%l?py>vjX1d1Xss&1x&W}IN(scqz+vX)y*a(Zgy(fiS-q#lnMN>Ew0w)MC#s}Pu6ok#;&Z4V?2XyZUgAI(6U zCW;YgDG?XJ7$WI7-@bjDt~6IM)74$z78+(#RXdIN#Puj*mAp4FsPIv1qCKxfyE6jm zdmYh$9XYtiXv%~oTxe!CBpLEx3h+l)0-4nf@_%NJkbE~bHiqxB`@PTzm{L?#S9fNG zz@*^<{Pew;*q14!yBmm-yQHe7=3e%J1G1v;IsDMa+μ+AuD1gV~H?{T7kg$w?l9 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 literal 28008 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 Ziy!k#Ngqq(zdM+mg$mYB{|8~MJ~;pY diff --git a/js/async-runner.js b/js/async-runner.js index 972ea156..4e8edffb 100644 --- a/js/async-runner.js +++ b/js/async-runner.js @@ -9,184 +9,185 @@ define([ "extension-manager" ], function(_, core, utils, extensionMgr) { - var asyncRunner = {}; + var asyncRunner = {}; - var taskQueue = []; - var asyncRunning = false; - var currentTask = undefined; - var currentTaskRunning = false; - var currentTaskStartTime = 0; + var taskQueue = []; + var asyncRunning = false; + var currentTask = undefined; + var currentTaskRunning = false; + var currentTaskStartTime = 0; - asyncRunner.createTask = function() { - var task = {}; - task.finished = false; - task.timeout = ASYNC_TASK_DEFAULT_TIMEOUT; - task.retryCounter = 0; - /** - * onRun callbacks are called by chain(). These callbacks have to call - * chain() themselves to chain with next onRun callback or error() to - * throw an exception or retry() to restart the task. - */ - // Run callbacks - task.runCallbacks = []; - task.onRun = function(callback) { - task.runCallbacks.push(callback); - }; - /** - * onSuccess callbacks are called when every onRun callbacks have - * succeed. - */ - task.successCallbacks = []; - task.onSuccess = function(callback) { - task.successCallbacks.push(callback); - }; - /** - * onError callbacks are called when error() is called in a onRun - * callback. - */ - task.errorCallbacks = []; - task.onError = function(callback) { - task.errorCallbacks.push(callback); - }; - /** - * chain() calls the next onRun callback or the onSuccess callbacks when - * finished. The optional callback parameter can be used to pass an onRun - * callback during execution. - */ - task.chain = function(callback) { - if (task.finished === true) { - return; - } - // If first execution - if (task.queue === undefined) { - // Create a copy of the onRun callbacks - task.queue = task.runCallbacks.slice(); - } - // If a callback is passed as a parameter - if(callback !== undefined) { - callback(); - return; - } - // If all callbacks have been run - if (task.queue.length === 0) { - // Run the onSuccess callbacks - runSafe(task, task.successCallbacks); - return; - } - // Run the next callback - var runCallback = task.queue.shift(); - runCallback(); - }; - /** - * error() calls the onError callbacks passing the error parameter and ends - * the task by throwing an exception. - */ - task.error = function(error) { - if (task.finished === true) { - return; - } - error = error || new Error("Unknown error"); - if(error.message) { - extensionMgr.onError(error); - } - runSafe(task, task.errorCallbacks, error); - // Exit the current call stack - throw error; - }; - /** - * retry() can be called in an onRun callback to restart the task - */ - task.retry = function(error, maxRetryCounter) { - if (task.finished === true) { - return; - } - maxRetryCounter = maxRetryCounter || 5; - task.queue = undefined; - if (task.retryCounter >= maxRetryCounter) { - task.error(error); - return; - } - // Implement an exponential backoff - var delay = Math.pow(2, task.retryCounter++) * 1000; - currentTaskStartTime = utils.currentTime + delay; - currentTaskRunning = false; - asyncRunner.runTask(); - }; - return task; - }; + asyncRunner.createTask = function() { + var task = {}; + task.finished = false; + task.timeout = ASYNC_TASK_DEFAULT_TIMEOUT; + task.retryCounter = 0; + /** + * onRun callbacks are called by chain(). These callbacks have to call + * chain() themselves to chain with next onRun callback or error() to + * throw an exception or retry() to restart the task. + */ + // Run callbacks + task.runCallbacks = []; + task.onRun = function(callback) { + task.runCallbacks.push(callback); + }; + /** + * onSuccess callbacks are called when every onRun callbacks have + * succeed. + */ + task.successCallbacks = []; + task.onSuccess = function(callback) { + task.successCallbacks.push(callback); + }; + /** + * onError callbacks are called when error() is called in a onRun + * callback. + */ + task.errorCallbacks = []; + task.onError = function(callback) { + task.errorCallbacks.push(callback); + }; + /** + * chain() calls the next onRun callback or the onSuccess callbacks when + * finished. The optional callback parameter can be used to pass an + * onRun callback during execution. + */ + task.chain = function(callback) { + if(task.finished === true) { + return; + } + // If first execution + if(task.queue === undefined) { + // Create a copy of the onRun callbacks + task.queue = task.runCallbacks.slice(); + } + // If a callback is passed as a parameter + if(callback !== undefined) { + callback(); + return; + } + // If all callbacks have been run + if(task.queue.length === 0) { + // Run the onSuccess callbacks + runSafe(task, task.successCallbacks); + return; + } + // Run the next callback + var runCallback = task.queue.shift(); + runCallback(); + }; + /** + * error() calls the onError callbacks passing the error parameter and + * ends the task by throwing an exception. + */ + task.error = function(error) { + if(task.finished === true) { + return; + } + error = error || new Error("Unknown error"); + if(error.message) { + extensionMgr.onError(error); + } + runSafe(task, task.errorCallbacks, error); + // Exit the current call stack + throw error; + }; + /** + * retry() can be called in an onRun callback to restart the task + */ + task.retry = function(error, maxRetryCounter) { + if(task.finished === true) { + return; + } + maxRetryCounter = maxRetryCounter || 5; + task.queue = undefined; + if(task.retryCounter >= maxRetryCounter) { + task.error(error); + return; + } + // Implement an exponential backoff + var delay = Math.pow(2, task.retryCounter++) * 1000; + currentTaskStartTime = utils.currentTime + delay; + currentTaskRunning = false; + asyncRunner.runTask(); + }; + return task; + }; - // Run the next task in the queue if any and no other running - asyncRunner.runTask = function() { - // Use defer to avoid stack overflow - _.defer(function() { + // Run the next task in the queue if any and no other running + asyncRunner.runTask = function() { + // Use defer to avoid stack overflow + _.defer(function() { - // If there is a task currently running - if (currentTaskRunning === true) { - // If the current task takes too long - if (currentTaskStartTime + currentTask.timeout < utils.currentTime) { - currentTask.error(new Error("A timeout occurred.")); - } - return; - } + // If there is a task currently running + if(currentTaskRunning === true) { + // If the current task takes too long + if(currentTaskStartTime + currentTask.timeout < utils.currentTime) { + currentTask.error(new Error("A timeout occurred.")); + } + return; + } - if (currentTask === undefined) { - // If no task in the queue - if (taskQueue.length === 0) { - return; - } + if(currentTask === undefined) { + // If no task in the queue + if(taskQueue.length === 0) { + return; + } - // Dequeue an enqueued task - currentTask = taskQueue.shift(); - currentTaskStartTime = utils.currentTime; - if(asyncRunning === false) { - asyncRunning = true; - extensionMgr.onAsyncRunning(true); - } - } + // Dequeue an enqueued task + currentTask = taskQueue.shift(); + currentTaskStartTime = utils.currentTime; + if(asyncRunning === false) { + asyncRunning = true; + extensionMgr.onAsyncRunning(true); + } + } - // Run the task - if (currentTaskStartTime <= utils.currentTime) { - currentTaskRunning = true; - currentTask.chain(); - } - }); - }; - // Run runTask function periodically - core.addPeriodicCallback(asyncRunner.runTask); + // Run the task + if(currentTaskStartTime <= utils.currentTime) { + currentTaskRunning = true; + currentTask.chain(); + } + }); + }; + // Run runTask function periodically + core.addPeriodicCallback(asyncRunner.runTask); - function runSafe(task, callbacks, param) { - try { - _.each(callbacks, function(callback) { - callback(param); - }); - } - finally { - task.finished = true; - if (currentTask === task) { - currentTask = undefined; - currentTaskRunning = false; - } - if (taskQueue.length === 0) { - asyncRunning = false; - extensionMgr.onAsyncRunning(false); - } else { - asyncRunner.runTask(); - } - } - } + function runSafe(task, callbacks, param) { + try { + _.each(callbacks, function(callback) { + callback(param); + }); + } + finally { + task.finished = true; + if(currentTask === task) { + currentTask = undefined; + currentTaskRunning = false; + } + if(taskQueue.length === 0) { + asyncRunning = false; + extensionMgr.onAsyncRunning(false); + } + else { + asyncRunner.runTask(); + } + } + } - // Add a task to the queue - asyncRunner.addTask = function(task) { - taskQueue.push(task); - asyncRunner.runTask(); - }; + // Add a task to the queue + asyncRunner.addTask = function(task) { + taskQueue.push(task); + asyncRunner.runTask(); + }; - // Change current task timeout - asyncRunner.setCurrentTaskTimeout = function(timeout) { - if (currentTask !== undefined) { - currentTask.timeout = timeout; - } - }; + // Change current task timeout + asyncRunner.setCurrentTaskTimeout = function(timeout) { + if(currentTask !== undefined) { + currentTask.timeout = timeout; + } + }; - return asyncRunner; + return asyncRunner; }); diff --git a/js/blogger-provider.js b/js/blogger-provider.js index 8e65ce81..c63ae202 100644 --- a/js/blogger-provider.js +++ b/js/blogger-provider.js @@ -1,59 +1,51 @@ define([ - "underscore", + "underscore", "utils", "google-helper" ], function(_, utils, googleHelper) { - - var PROVIDER_BLOGGER = "blogger"; - var bloggerProvider = { - providerId: PROVIDER_BLOGGER, - providerName: "Blogger", - defaultPublishFormat: "html", - publishPreferencesInputIds: ["blogger-url"] - }; - - bloggerProvider.publish = function(publishAttributes, title, content, callback) { - googleHelper.uploadBlogger( - publishAttributes.blogUrl, - publishAttributes.blogId, - publishAttributes.postId, - publishAttributes.labelList, - title, - content, - function(error, blogId, postId) { - if(error) { - callback(error); - return; - } - publishAttributes.blogId = blogId; - publishAttributes.postId = postId; - callback(); - } - ); - }; - - bloggerProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - var blogUrl = utils.getInputTextValue("#input-publish-blogger-url", event); - if(blogUrl !== undefined) { - publishAttributes.blogUrl = utils.checkUrl(blogUrl); - } - publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); - publishAttributes.labelList = []; - var labels = utils.getInputTextValue("#input-publish-labels"); - if(labels !== undefined) { - publishAttributes.labelList = _.chain( - labels.split(",") - ).map(function(label) { - return utils.trim(label); - }).compact().value(); - } - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + var PROVIDER_BLOGGER = "blogger"; - return bloggerProvider; + var bloggerProvider = { + providerId: PROVIDER_BLOGGER, + providerName: "Blogger", + defaultPublishFormat: "html", + publishPreferencesInputIds: [ + "blogger-url" + ] + }; + + bloggerProvider.publish = function(publishAttributes, title, content, callback) { + googleHelper.uploadBlogger(publishAttributes.blogUrl, publishAttributes.blogId, publishAttributes.postId, publishAttributes.labelList, title, content, function(error, blogId, postId) { + if(error) { + callback(error); + return; + } + publishAttributes.blogId = blogId; + publishAttributes.postId = postId; + callback(); + }); + }; + + bloggerProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + var blogUrl = utils.getInputTextValue("#input-publish-blogger-url", event); + if(blogUrl !== undefined) { + publishAttributes.blogUrl = utils.checkUrl(blogUrl); + } + publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); + publishAttributes.labelList = []; + var labels = utils.getInputTextValue("#input-publish-labels"); + if(labels !== undefined) { + publishAttributes.labelList = _.chain(labels.split(",")).map(function(label) { + return utils.trim(label); + }).compact().value(); + } + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + return bloggerProvider; }); \ No newline at end of file diff --git a/js/config.js b/js/config.js index e73fa82c..03b54de6 100644 --- a/js/config.js +++ b/js/config.js @@ -1,8 +1,10 @@ var MAIN_URL = "http://benweet.github.io/stackedit/"; var GOOGLE_API_KEY = "AIzaSyAeCU8CGcSkn0z9js6iocHuPBX4f_mMWkw"; -var GOOGLE_SCOPES = [ "https://www.googleapis.com/auth/drive.install", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/blogger" ]; +var GOOGLE_SCOPES = [ + "https://www.googleapis.com/auth/drive.install", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/blogger" +]; var GOOGLE_DRIVE_APP_ID = "241271498917"; var DROPBOX_APP_KEY = "lq6mwopab8wskas"; var DROPBOX_APP_SECRET = "851fgnucpezy84t"; @@ -14,7 +16,7 @@ var AJAX_TIMEOUT = 30000; var ASYNC_TASK_DEFAULT_TIMEOUT = 60000; var ASYNC_TASK_LONG_TIMEOUT = 120000; var SYNC_PERIOD = 180000; -var USER_IDLE_THRESHOLD = 300000; +var USER_IDLE_THRESHOLD = 300000; var TEMPORARY_FILE_INDEX = "file.tempIndex"; var WELCOME_DOCUMENT_TITLE = "Welcome document"; var DOWNLOAD_PROXY_URL = "http://stackedit-download-proxy.herokuapp.com/"; @@ -25,9 +27,9 @@ var SSH_PROXY_URL = "http://stackedit-ssh-proxy.herokuapp.com/"; // Use by Google's client.js var delayedFunction = undefined; function runDelayedFunction() { - if (delayedFunction !== undefined) { - delayedFunction(); - } + if(delayedFunction !== undefined) { + delayedFunction(); + } } // Site dependent @@ -38,15 +40,15 @@ var GATEKEEPER_URL = "http://stackedit-gatekeeper-localhost.herokuapp.com/"; var TUMBLR_PROXY_URL = "http://stackedit-tumblr-proxy-local.herokuapp.com/"; if(location.hostname.indexOf("benweet.github.io") === 0) { - BASE_URL = MAIN_URL; - GOOGLE_CLIENT_ID = '241271498917-jpto9lls9fqnem1e4h6ppds9uob8rpvu.apps.googleusercontent.com'; - GITHUB_CLIENT_ID = 'fa0d09514da8377ee32e'; - GATEKEEPER_URL = "http://stackedit-gatekeeper.herokuapp.com/"; - TUMBLR_PROXY_URL = "http://stackedit-tumblr-proxy.herokuapp.com/"; + BASE_URL = MAIN_URL; + GOOGLE_CLIENT_ID = '241271498917-jpto9lls9fqnem1e4h6ppds9uob8rpvu.apps.googleusercontent.com'; + GITHUB_CLIENT_ID = 'fa0d09514da8377ee32e'; + GATEKEEPER_URL = "http://stackedit-gatekeeper.herokuapp.com/"; + TUMBLR_PROXY_URL = "http://stackedit-tumblr-proxy.herokuapp.com/"; } var THEME_LIST = { - "": "Default", - "blue-gray": "Blue-Gray", - "night": "Night" + "": "Default", + "blue-gray": "Blue-Gray", + "night": "Night" }; diff --git a/js/core.js b/js/core.js index 4d748c3c..b406b6f2 100644 --- a/js/core.js +++ b/js/core.js @@ -1,6 +1,6 @@ define([ "jquery", - "underscore", + "underscore", "utils", "settings", "extension-manager", @@ -10,461 +10,446 @@ define([ "lib/layout", "lib/Markdown.Editor" ], function($, _, utils, settings, extensionMgr) { - - var core = {}; - - // Used for periodic tasks - var intervalId = undefined; - var periodicCallbacks = []; - core.addPeriodicCallback = function(callback) { - periodicCallbacks.push(callback); - }; - - // Used to detect user activity - var userReal = false; - var userActive = false; - var windowUnique = true; - var userLastActivity = 0; - function setUserActive() { - userReal = true; - userActive = true; - userLastActivity = utils.currentTime; - }; - function isUserActive() { - if(userActive === true - && utils.currentTime - userLastActivity > USER_IDLE_THRESHOLD) { - userActive = false; - } - return userActive && windowUnique; - } - - // Used to only have 1 window of the application in the same browser - var windowId = undefined; - function checkWindowUnique() { - if(userReal === false || windowUnique === false) { - return; - } - if(windowId === undefined) { - windowId = utils.randomString(); - localStorage["frontWindowId"] = windowId; - } - var frontWindowId = localStorage["frontWindowId"]; - if(frontWindowId != windowId) { - windowUnique = false; - if(intervalId !== undefined) { - clearInterval(intervalId); - } - $(".modal").modal("hide"); - $('#modal-non-unique').modal({ - backdrop: "static", - keyboard: false - }); - } - } - - // Offline management - core.isOffline = false; - var offlineTime = utils.currentTime; - core.setOffline = function() { - offlineTime = utils.currentTime; - if(core.isOffline === false) { - core.isOffline = true; - extensionMgr.onOfflineChanged(true); - } - }; - function setOnline() { - if(core.isOffline === true) { - core.isOffline = false; - extensionMgr.onOfflineChanged(false); - } - } - function checkOnline() { - // Try to reconnect if we are offline but we have some network - if (core.isOffline === true && navigator.onLine === true - && offlineTime + CHECK_ONLINE_PERIOD < utils.currentTime) { - offlineTime = utils.currentTime; - // Try to download anything to test the connection - $.ajax({ - url : "//www.google.com/jsapi", - timeout : AJAX_TIMEOUT, dataType : "script" - }).done(function() { - setOnline(); - }); - } - } - - // Load settings in settings dialog - function loadSettings() { - - // Layout orientation - utils.setInputRadio("radio-layout-orientation", settings.layoutOrientation); - // Theme - utils.setInputValue("#input-settings-theme", localStorage.theme); - // Lazy rendering - utils.setInputChecked("#input-settings-lazy-rendering", settings.lazyRendering); - // Editor font size - utils.setInputValue("#input-settings-editor-font-size", settings.editorFontSize); - // Default content - utils.setInputValue("#textarea-settings-default-content", settings.defaultContent); - // Commit message - utils.setInputValue("#input-settings-publish-commit-msg", settings.commitMsg); - // Template - utils.setInputValue("#textarea-settings-publish-template", settings.template); - // SSH proxy - utils.setInputValue("#input-settings-ssh-proxy", settings.sshProxy); - - // Load extension settings - extensionMgr.onLoadSettings(); - } - // Save settings from settings dialog - function saveSettings(event) { - var newSettings = {}; - - // Layout orientation - newSettings.layoutOrientation = utils.getInputRadio("radio-layout-orientation"); - // Theme - var theme = utils.getInputValue("#input-settings-theme"); - // Lazy Rendering - newSettings.lazyRendering = utils.getInputChecked("#input-settings-lazy-rendering"); - // Editor font size - newSettings.editorFontSize = utils.getInputIntValue("#input-settings-editor-font-size", event, 1, 99); - // Default content - newSettings.defaultContent = utils.getInputValue("#textarea-settings-default-content"); - // Commit message - newSettings.commitMsg = utils.getInputTextValue("#input-settings-publish-commit-msg", event); - // Template - newSettings.template = utils.getInputTextValue("#textarea-settings-publish-template", event); - // SSH proxy - newSettings.sshProxy = utils.checkUrl(utils.getInputTextValue("#input-settings-ssh-proxy", event), true); - - // Save extension settings - newSettings.extensionSettings = {}; - extensionMgr.onSaveSettings(newSettings.extensionSettings, event); - - if(!event.isPropagationStopped()) { - $.extend(settings, newSettings); - localStorage.settings = JSON.stringify(settings); - localStorage.theme = theme; - } - } - - // Create the layout - var layout = undefined; - core.createLayout = function() { - if(viewerMode === true) { - return; - } - var layoutGlobalConfig = { - closable : true, - resizable : false, - slidable : false, - livePaneResizing : true, - enableCursorHotkey : false, - spacing_open : 15, - spacing_closed : 15, - togglerLength_open : 90, - togglerLength_closed : 90, - stateManagement__enabled : false, - center__minWidth : 200, - center__minHeight : 200 - }; - extensionMgr.onLayoutConfigure(layoutGlobalConfig); - if (settings.layoutOrientation == "horizontal") { - $(".ui-layout-south").remove(); - $(".ui-layout-east").addClass("well").prop("id", "wmd-preview"); - layout = $('body').layout( - $.extend(layoutGlobalConfig, { - east__resizable : true, - east__size : .5, - east__minSize : 200 - }) - ); - } else if (settings.layoutOrientation == "vertical") { - $(".ui-layout-east").remove(); - $(".ui-layout-south").addClass("well").prop("id", "wmd-preview"); - layout = $('body').layout( - $.extend(layoutGlobalConfig, { - south__resizable : true, - south__size : .5, - south__minSize : 200 - }) - ); - } - $(".ui-layout-toggler-north").addClass("btn").append( - $("").addClass("caret")); - $(".ui-layout-toggler-south").addClass("btn").append( - $("").addClass("caret")); - $(".ui-layout-toggler-east").addClass("btn").append( - $("").addClass("caret")); - $("#navbar").click(function() { - layout.allowOverflow('north'); - }); - - extensionMgr.onLayoutCreated(layout); - }; - - // Create the PageDown editor - var insertLinkCallback = undefined; - core.createEditor = function(onTextChange) { - var converter = new Markdown.Converter(); - var editor = new Markdown.Editor(converter); - // Custom insert link dialog - editor.hooks.set("insertLinkDialog", function (callback) { - insertLinkCallback = callback; - utils.resetModalInputs(); - $("#modal-insert-link").modal(); - _.defer(function() { - $("#input-insert-link").focus(); - }); - return true; - }); - // Custom insert image dialog - editor.hooks.set("insertImageDialog", function (callback) { - insertLinkCallback = callback; - utils.resetModalInputs(); - $("#modal-insert-image").modal(); - _.defer(function() { - $("#input-insert-image").focus(); - }); - return true; - }); - - var documentContent = undefined; - function checkDocumentChanges() { - var newDocumentContent = $("#wmd-input").val(); - if(documentContent !== undefined && documentContent != newDocumentContent) { - onTextChange(); - } - documentContent = newDocumentContent; - } - var previewWrapper = undefined; - if(settings.lazyRendering === true) { - previewWrapper = function(makePreview) { - var debouncedMakePreview = _.debounce(makePreview, 500); - return function() { - if(documentContent === undefined) { - makePreview(); - } - else { - debouncedMakePreview(); - } - checkDocumentChanges(); - }; - }; - } - else { - previewWrapper = function(makePreview) { - return function() { - checkDocumentChanges(); - makePreview(); - }; - }; - } - editor.hooks.chain("onPreviewRefresh", extensionMgr.onAsyncPreview); - extensionMgr.onEditorConfigure(editor); - - $("#wmd-input, #wmd-preview").scrollTop(0); - $("#wmd-button-bar").empty(); - editor.run(previewWrapper); - firstChange = false; + var core = {}; - // Hide default buttons - $(".wmd-button-row").addClass("btn-group").find("li:not(.wmd-spacer)") - .addClass("btn").css("left", 0).find("span").hide(); - - // Add customized buttons - $("#wmd-bold-button").append($("").addClass("icon-bold")); - $("#wmd-italic-button").append($("").addClass("icon-italic")); - $("#wmd-link-button").append($("").addClass("icon-globe")); - $("#wmd-quote-button").append($("").addClass("icon-indent-left")); - $("#wmd-code-button").append($("").addClass("icon-code")); - $("#wmd-image-button").append($("").addClass("icon-picture")); - $("#wmd-olist-button").append($("").addClass("icon-numbered-list")); - $("#wmd-ulist-button").append($("").addClass("icon-list")); - $("#wmd-heading-button").append($("").addClass("icon-text-height")); - $("#wmd-hr-button").append($("").addClass("icon-hr")); - $("#wmd-undo-button").append($("").addClass("icon-undo")); - $("#wmd-redo-button").append($("").addClass("icon-share-alt")); - }; + // Used for periodic tasks + var intervalId = undefined; + var periodicCallbacks = []; + core.addPeriodicCallback = function(callback) { + periodicCallbacks.push(callback); + }; - // onReady event callbacks - var readyCallbacks = []; - core.onReady = function(callback) { - readyCallbacks.push(callback); - runReadyCallbacks(); - }; - var ready = false; - core.setReady = function() { - ready = true; - runReadyCallbacks(); - }; - function runReadyCallbacks() { - if(ready === true) { - _.each(readyCallbacks, function(callback) { - callback(); - }); - readyCallbacks = []; - } - } - - core.onReady(extensionMgr.onReady); - core.onReady(function() { - - // Load theme list - _.each(THEME_LIST, function(name, value) { - $("#input-settings-theme").append($('')); - }); - - // listen to online/offline events - $(window).on('offline', core.setOffline); - $(window).on('online', setOnline); - if (navigator.onLine === false) { - core.setOffline(); - } - - // Detect user activity - $(document).mousemove(setUserActive).keypress(setUserActive); - - // Avoid dropdown to close when clicking on submenu - $(".dropdown-submenu > a").click(function(e) { - e.stopPropagation(); - }); - - // Click events on "insert link" and "insert image" dialog buttons - $(".action-insert-link").click(function(e) { - var value = utils.getInputTextValue($("#input-insert-link"), e); - if(value !== undefined) { - insertLinkCallback(value); - } - }); - $(".action-insert-image").click(function(e) { - var value = utils.getInputTextValue($("#input-insert-image"), e); - if(value !== undefined) { - insertLinkCallback(value); - } - }); - $(".action-close-insert-link").click(function(e) { - insertLinkCallback(null); - }); + // Used to detect user activity + var userReal = false; + var userActive = false; + var windowUnique = true; + var userLastActivity = 0; + function setUserActive() { + userReal = true; + userActive = true; + userLastActivity = utils.currentTime; + } - // Settings loading/saving - $(".action-load-settings").click(function() { - loadSettings(); - }); - $(".action-apply-settings").click(function(e) { - saveSettings(e); - if(!e.isPropagationStopped()) { - window.location.reload(); - } - }); - - $(".action-default-settings").click(function() { - localStorage.removeItem("settings"); - localStorage.removeItem("theme"); - window.location.reload(); - }); - - $(".action-app-reset").click(function() { - localStorage.clear(); - window.location.reload(); - }); - - // UI layout - $("#menu-bar, .ui-layout-center, .ui-layout-east, .ui-layout-south").removeClass("hide"); - core.createLayout(); + function isUserActive() { + if(userActive === true && utils.currentTime - userLastActivity > USER_IDLE_THRESHOLD) { + userActive = false; + } + return userActive && windowUnique; + } - // Editor's textarea - $("#wmd-input, #md-section-helper").css({ - // Apply editor font size - "font-size": settings.editorFontSize + "px", - "line-height": Math.round(settings.editorFontSize * (20/14)) + "px" - }); - - // Manage tab key - $("#wmd-input").keydown(function(e) { - if(e.keyCode === 9) { - var value = $(this).val(); - var start = this.selectionStart; - var end = this.selectionEnd; - // IE8 does not support selection attributes - if(start === undefined || end === undefined) { - return; - } - $(this).val(value.substring(0, start) + "\t" + value.substring(end)); - this.selectionStart = this.selectionEnd = start + 1; - e.preventDefault(); - } - }); + // Used to only have 1 window of the application in the same browser + var windowId = undefined; + function checkWindowUnique() { + if(userReal === false || windowUnique === false) { + return; + } + if(windowId === undefined) { + windowId = utils.randomString(); + localStorage["frontWindowId"] = windowId; + } + var frontWindowId = localStorage["frontWindowId"]; + if(frontWindowId != windowId) { + windowUnique = false; + if(intervalId !== undefined) { + clearInterval(intervalId); + } + $(".modal").modal("hide"); + $('#modal-non-unique').modal({ + backdrop: "static", + keyboard: false + }); + } + } - // Tooltips - $(".tooltip-scroll-link").tooltip({ - html: true, - container: '#modal-settings', - placement: 'right', - title: ['Scroll Link is a feature that binds together editor and preview scrollbars. ', - 'It allows you to keep an eye on the preview while scrolling the editor and vice versa. ', - '

', - 'The mapping between Markdown and HTML is based on the position of the title elements (h1, h2, ...) in the page. ', - 'Therefore, if your document does not contain any title, the mapping will be linear and consequently less efficient.', - ].join("") - }); - $(".tooltip-lazy-rendering").tooltip({ - container: '#modal-settings', - placement: 'right', - title: 'Disable preview rendering while typing in order to offload CPU. Refresh preview after 500 ms of inactivity.' - }); - $(".tooltip-default-content").tooltip({ - html: true, - container: '#modal-settings', - placement: 'right', - title: 'Thanks for supporting StackEdit by adding a backlink in your documents!' - }); - $(".tooltip-template").tooltip({ - html: true, - container: '#modal-settings', - placement: 'right', - trigger: 'manual', - title: ['Available variables:
', - '
  • documentTitle: document title
  • ', - '
  • documentMarkdown: document in Markdown format
  • ', - '
  • documentHTML: document in HTML format
  • ', - '
  • publishAttributes: attributes of the publish location (undefined when using "Save")
', - 'Examples:
', - _.escape('<%= documentTitle %>'), - '
', - _.escape('
<%- documentHTML %>
'), - '
', - _.escape('<% if(publishAttributes.provider == "github") print(documentMarkdown); %>'), - '

More info', - ].join("") - }).click(function(e) { - $(this).tooltip('show'); - e.stopPropagation(); - }); - - $(document).click(function(e) { - $(".tooltip-template").tooltip('hide'); - }); + // Offline management + core.isOffline = false; + var offlineTime = utils.currentTime; + core.setOffline = function() { + offlineTime = utils.currentTime; + if(core.isOffline === false) { + core.isOffline = true; + extensionMgr.onOfflineChanged(true); + } + }; + function setOnline() { + if(core.isOffline === true) { + core.isOffline = false; + extensionMgr.onOfflineChanged(false); + } + } + function checkOnline() { + // Try to reconnect if we are offline but we have some network + if(core.isOffline === true && navigator.onLine === true && offlineTime + CHECK_ONLINE_PERIOD < utils.currentTime) { + offlineTime = utils.currentTime; + // Try to download anything to test the connection + $.ajax({ + url: "//www.google.com/jsapi", + timeout: AJAX_TIMEOUT, + dataType: "script" + }).done(function() { + setOnline(); + }); + } + } - // Reset inputs - $(".action-reset-input").click(function() { - utils.resetModalInputs(); - }); - - // Do periodic tasks - intervalId = window.setInterval(function() { - utils.updateCurrentTime(); - checkWindowUnique(); - if(isUserActive() === true || viewerMode === true) { - _.each(periodicCallbacks, function(callback) { - callback(); - }); - checkOnline(); - } - }, 1000); - }); + // Load settings in settings dialog + function loadSettings() { - return core; + // Layout orientation + utils.setInputRadio("radio-layout-orientation", settings.layoutOrientation); + // Theme + utils.setInputValue("#input-settings-theme", localStorage.theme); + // Lazy rendering + utils.setInputChecked("#input-settings-lazy-rendering", settings.lazyRendering); + // Editor font size + utils.setInputValue("#input-settings-editor-font-size", settings.editorFontSize); + // Default content + utils.setInputValue("#textarea-settings-default-content", settings.defaultContent); + // Commit message + utils.setInputValue("#input-settings-publish-commit-msg", settings.commitMsg); + // Template + utils.setInputValue("#textarea-settings-publish-template", settings.template); + // SSH proxy + utils.setInputValue("#input-settings-ssh-proxy", settings.sshProxy); + + // Load extension settings + extensionMgr.onLoadSettings(); + } + + // Save settings from settings dialog + function saveSettings(event) { + var newSettings = {}; + + // Layout orientation + newSettings.layoutOrientation = utils.getInputRadio("radio-layout-orientation"); + // Theme + var theme = utils.getInputValue("#input-settings-theme"); + // Lazy Rendering + newSettings.lazyRendering = utils.getInputChecked("#input-settings-lazy-rendering"); + // Editor font size + newSettings.editorFontSize = utils.getInputIntValue("#input-settings-editor-font-size", event, 1, 99); + // Default content + newSettings.defaultContent = utils.getInputValue("#textarea-settings-default-content"); + // Commit message + newSettings.commitMsg = utils.getInputTextValue("#input-settings-publish-commit-msg", event); + // Template + newSettings.template = utils.getInputTextValue("#textarea-settings-publish-template", event); + // SSH proxy + newSettings.sshProxy = utils.checkUrl(utils.getInputTextValue("#input-settings-ssh-proxy", event), true); + + // Save extension settings + newSettings.extensionSettings = {}; + extensionMgr.onSaveSettings(newSettings.extensionSettings, event); + + if(!event.isPropagationStopped()) { + $.extend(settings, newSettings); + localStorage.settings = JSON.stringify(settings); + localStorage.theme = theme; + } + } + + // Create the layout + var layout = undefined; + core.createLayout = function() { + if(viewerMode === true) { + return; + } + var layoutGlobalConfig = { + closable: true, + resizable: false, + slidable: false, + livePaneResizing: true, + enableCursorHotkey: false, + spacing_open: 15, + spacing_closed: 15, + togglerLength_open: 90, + togglerLength_closed: 90, + stateManagement__enabled: false, + center__minWidth: 200, + center__minHeight: 200 + }; + extensionMgr.onLayoutConfigure(layoutGlobalConfig); + if(settings.layoutOrientation == "horizontal") { + $(".ui-layout-south").remove(); + $(".ui-layout-east").addClass("well").prop("id", "wmd-preview"); + layout = $('body').layout($.extend(layoutGlobalConfig, { + east__resizable: true, + east__size: .5, + east__minSize: 200 + })); + } + else if(settings.layoutOrientation == "vertical") { + $(".ui-layout-east").remove(); + $(".ui-layout-south").addClass("well").prop("id", "wmd-preview"); + layout = $('body').layout($.extend(layoutGlobalConfig, { + south__resizable: true, + south__size: .5, + south__minSize: 200 + })); + } + $(".ui-layout-toggler-north").addClass("btn").append($("").addClass("caret")); + $(".ui-layout-toggler-south").addClass("btn").append($("").addClass("caret")); + $(".ui-layout-toggler-east").addClass("btn").append($("").addClass("caret")); + $("#navbar").click(function() { + layout.allowOverflow('north'); + }); + + extensionMgr.onLayoutCreated(layout); + }; + + // Create the PageDown editor + var insertLinkCallback = undefined; + core.createEditor = function(onTextChange) { + var converter = new Markdown.Converter(); + var editor = new Markdown.Editor(converter); + // Custom insert link dialog + editor.hooks.set("insertLinkDialog", function(callback) { + insertLinkCallback = callback; + utils.resetModalInputs(); + $("#modal-insert-link").modal(); + _.defer(function() { + $("#input-insert-link").focus(); + }); + return true; + }); + // Custom insert image dialog + editor.hooks.set("insertImageDialog", function(callback) { + insertLinkCallback = callback; + utils.resetModalInputs(); + $("#modal-insert-image").modal(); + _.defer(function() { + $("#input-insert-image").focus(); + }); + return true; + }); + + var documentContent = undefined; + function checkDocumentChanges() { + var newDocumentContent = $("#wmd-input").val(); + if(documentContent !== undefined && documentContent != newDocumentContent) { + onTextChange(); + } + documentContent = newDocumentContent; + } + var previewWrapper = undefined; + if(settings.lazyRendering === true) { + previewWrapper = function(makePreview) { + var debouncedMakePreview = _.debounce(makePreview, 500); + return function() { + if(documentContent === undefined) { + makePreview(); + } + else { + debouncedMakePreview(); + } + checkDocumentChanges(); + }; + }; + } + else { + previewWrapper = function(makePreview) { + return function() { + checkDocumentChanges(); + makePreview(); + }; + }; + } + editor.hooks.chain("onPreviewRefresh", extensionMgr.onAsyncPreview); + extensionMgr.onEditorConfigure(editor); + + $("#wmd-input, #wmd-preview").scrollTop(0); + $("#wmd-button-bar").empty(); + editor.run(previewWrapper); + firstChange = false; + + // Hide default buttons + $(".wmd-button-row").addClass("btn-group").find("li:not(.wmd-spacer)").addClass("btn").css("left", 0).find("span").hide(); + + // Add customized buttons + $("#wmd-bold-button").append($("").addClass("icon-bold")); + $("#wmd-italic-button").append($("").addClass("icon-italic")); + $("#wmd-link-button").append($("").addClass("icon-globe")); + $("#wmd-quote-button").append($("").addClass("icon-indent-left")); + $("#wmd-code-button").append($("").addClass("icon-code")); + $("#wmd-image-button").append($("").addClass("icon-picture")); + $("#wmd-olist-button").append($("").addClass("icon-numbered-list")); + $("#wmd-ulist-button").append($("").addClass("icon-list")); + $("#wmd-heading-button").append($("").addClass("icon-text-height")); + $("#wmd-hr-button").append($("").addClass("icon-hr")); + $("#wmd-undo-button").append($("").addClass("icon-undo")); + $("#wmd-redo-button").append($("").addClass("icon-share-alt")); + }; + + // onReady event callbacks + var readyCallbacks = []; + core.onReady = function(callback) { + readyCallbacks.push(callback); + runReadyCallbacks(); + }; + var ready = false; + core.setReady = function() { + ready = true; + runReadyCallbacks(); + }; + function runReadyCallbacks() { + if(ready === true) { + _.each(readyCallbacks, function(callback) { + callback(); + }); + readyCallbacks = []; + } + } + + core.onReady(extensionMgr.onReady); + core.onReady(function() { + + // Load theme list + _.each(THEME_LIST, function(name, value) { + $("#input-settings-theme").append($('')); + }); + + // listen to online/offline events + $(window).on('offline', core.setOffline); + $(window).on('online', setOnline); + if(navigator.onLine === false) { + core.setOffline(); + } + + // Detect user activity + $(document).mousemove(setUserActive).keypress(setUserActive); + + // Avoid dropdown to close when clicking on submenu + $(".dropdown-submenu > a").click(function(e) { + e.stopPropagation(); + }); + + // Click events on "insert link" and "insert image" dialog buttons + $(".action-insert-link").click(function(e) { + var value = utils.getInputTextValue($("#input-insert-link"), e); + if(value !== undefined) { + insertLinkCallback(value); + } + }); + $(".action-insert-image").click(function(e) { + var value = utils.getInputTextValue($("#input-insert-image"), e); + if(value !== undefined) { + insertLinkCallback(value); + } + }); + $(".action-close-insert-link").click(function(e) { + insertLinkCallback(null); + }); + + // Settings loading/saving + $(".action-load-settings").click(function() { + loadSettings(); + }); + $(".action-apply-settings").click(function(e) { + saveSettings(e); + if(!e.isPropagationStopped()) { + window.location.reload(); + } + }); + + $(".action-default-settings").click(function() { + localStorage.removeItem("settings"); + localStorage.removeItem("theme"); + window.location.reload(); + }); + + $(".action-app-reset").click(function() { + localStorage.clear(); + window.location.reload(); + }); + + // UI layout + $("#menu-bar, .ui-layout-center, .ui-layout-east, .ui-layout-south").removeClass("hide"); + core.createLayout(); + + // Editor's textarea + $("#wmd-input, #md-section-helper").css({ + // Apply editor font size + "font-size": settings.editorFontSize + "px", + "line-height": Math.round(settings.editorFontSize * (20 / 14)) + "px" + }); + + // Manage tab key + $("#wmd-input").keydown(function(e) { + if(e.keyCode === 9) { + var value = $(this).val(); + var start = this.selectionStart; + var end = this.selectionEnd; + // IE8 does not support selection attributes + if(start === undefined || end === undefined) { + return; + } + $(this).val(value.substring(0, start) + "\t" + value.substring(end)); + this.selectionStart = this.selectionEnd = start + 1; + e.preventDefault(); + } + }); + + // Tooltips + $(".tooltip-lazy-rendering").tooltip({ + container: '#modal-settings', + placement: 'right', + title: 'Disable preview rendering while typing in order to offload CPU. Refresh preview after 500 ms of inactivity.' + }); + $(".tooltip-default-content").tooltip({ + html: true, + container: '#modal-settings', + placement: 'right', + title: 'Thanks for supporting StackEdit by adding a backlink in your documents!' + }); + $(".tooltip-template").tooltip({ + html: true, + container: '#modal-settings', + placement: 'right', + trigger: 'manual', + title: [ + 'Available variables:
', + '
    ', + '
  • documentTitle: document title
  • ', + '
  • documentMarkdown: document in Markdown format
  • ', + '
  • documentHTML: document in HTML format
  • ', + '
  • publishAttributes: attributes of the publish location (undefined when using "Save")
  • ', + '
', + 'Examples:
', + _.escape('<%= documentTitle %>'), + '
', + _.escape('
<%- documentHTML %>
'), + '
', + _.escape('<% if(publishAttributes.provider == "github") print(documentMarkdown); %>'), + '

', + 'More info', + ].join("") + }).click(function(e) { + $(this).tooltip('show'); + e.stopPropagation(); + }); + + $(document).click(function(e) { + $(".tooltip-template").tooltip('hide'); + }); + + // Reset inputs + $(".action-reset-input").click(function() { + utils.resetModalInputs(); + }); + + // Do periodic tasks + intervalId = window.setInterval(function() { + utils.updateCurrentTime(); + checkWindowUnique(); + if(isUserActive() === true || viewerMode === true) { + _.each(periodicCallbacks, function(callback) { + callback(); + }); + checkOnline(); + } + }, 1000); + }); + + return core; }); - diff --git a/js/download-provider.js b/js/download-provider.js index 20ee65b3..c3973a36 100644 --- a/js/download-provider.js +++ b/js/download-provider.js @@ -3,46 +3,48 @@ define([ "core", "async-runner" ], function($, core, asyncRunner) { - - var PROVIDER_DOWNLOAD = "download"; - - var downloadProvider = { - providerId: PROVIDER_DOWNLOAD, - sharingAttributes: ["url"] - }; - - downloadProvider.importPublic = function(importParameters, callback) { - var task = asyncRunner.createTask(); - var title = undefined; - var content = undefined; - task.onRun(function() { - var url = importParameters.url; - var slashUrl = url.lastIndexOf("/"); - if(slashUrl === -1) { - task.error(new Error("Invalid URL parameter.")); - return; - } - title = url.substring(slashUrl + 1); - $.ajax({ - url : DOWNLOAD_PROXY_URL + "download?url=" + url, - type: "GET", - dataType : "text", - timeout : AJAX_TIMEOUT - }).done(function(result, textStatus, jqXHR) { - content = result; - task.chain(); - }).fail(function(jqXHR) { - task.error(new Error("Unable to access URL " + url)); - }); - }); - task.onSuccess(function() { - callback(undefined, title, content); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - return downloadProvider; + var PROVIDER_DOWNLOAD = "download"; + + var downloadProvider = { + providerId: PROVIDER_DOWNLOAD, + sharingAttributes: [ + "url" + ] + }; + + downloadProvider.importPublic = function(importParameters, callback) { + var task = asyncRunner.createTask(); + var title = undefined; + var content = undefined; + task.onRun(function() { + var url = importParameters.url; + var slashUrl = url.lastIndexOf("/"); + if(slashUrl === -1) { + task.error(new Error("Invalid URL parameter.")); + return; + } + title = url.substring(slashUrl + 1); + $.ajax({ + url: DOWNLOAD_PROXY_URL + "download?url=" + url, + type: "GET", + dataType: "text", + timeout: AJAX_TIMEOUT + }).done(function(result, textStatus, jqXHR) { + content = result; + task.chain(); + }).fail(function(jqXHR) { + task.error(new Error("Unable to access URL " + url)); + }); + }); + task.onSuccess(function() { + callback(undefined, title, content); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; + + return downloadProvider; }); \ No newline at end of file diff --git a/js/dropbox-helper.js b/js/dropbox-helper.js index a522eabd..55f49f8b 100644 --- a/js/dropbox-helper.js +++ b/js/dropbox-helper.js @@ -6,323 +6,330 @@ define([ "async-runner" ], function($, _, core, extensionMgr, asyncRunner) { - var client = undefined; - var authenticated = false; + var client = undefined; + var authenticated = false; - var dropboxHelper = {}; + var dropboxHelper = {}; - // Try to connect dropbox by downloading client.js - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - client = undefined; - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - if (client !== undefined) { - task.chain(); - return; - } - $.ajax({ - url : "lib/dropbox.min.js", - dataType : "script", timeout : AJAX_TIMEOUT - }).done(function() { - client = new Dropbox.Client({ - key: DROPBOX_APP_KEY, - secret: DROPBOX_APP_SECRET - }); - client.authDriver(new Dropbox.Drivers.Popup({ - receiverUrl: BASE_URL + "dropbox-oauth-receiver.html", - rememberUser: true - })); - task.chain(); - }).fail(function(jqXHR) { - var error = { - status: jqXHR.status, - responseText: jqXHR.statusText - }; - handleError(error, task); - }); - }); - } + // Try to connect dropbox by downloading client.js + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + client = undefined; + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + if(client !== undefined) { + task.chain(); + return; + } + $.ajax({ + url: "lib/dropbox.min.js", + dataType: "script", + timeout: AJAX_TIMEOUT + }).done(function() { + client = new Dropbox.Client({ + key: DROPBOX_APP_KEY, + secret: DROPBOX_APP_SECRET + }); + client.authDriver(new Dropbox.Drivers.Popup({ + receiverUrl: BASE_URL + "dropbox-oauth-receiver.html", + rememberUser: true + })); + task.chain(); + }).fail(function(jqXHR) { + var error = { + status: jqXHR.status, + responseText: jqXHR.statusText + }; + handleError(error, task); + }); + }); + } - // Try to authenticate with Oauth - function authenticate(task) { - task.onRun(function() { - if (authenticated === true) { - task.chain(); - return; - } - var immediate = true; - function localAuthenticate() { - if (immediate === false) { - extensionMgr.onMessage("Please make sure the Dropbox authorization popup is not blocked by your browser."); - // If not immediate we add time for user to enter his credentials - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - } - client.reset(); - client.authenticate({interactive: !immediate}, function(error, client) { - // Success - if (client.authState === Dropbox.Client.DONE) { - authenticated = true; - task.chain(); - return; - } - // If immediate did not work retry without immediate flag - if (immediate === true) { - immediate = false; - task.chain(localAuthenticate); - return; - } - // Error - task.error(new Error("Access to Dropbox account is not authorized.")); - }); - } - task.chain(localAuthenticate); - }); - } + // Try to authenticate with Oauth + function authenticate(task) { + task.onRun(function() { + if(authenticated === true) { + task.chain(); + return; + } + var immediate = true; + function localAuthenticate() { + if(immediate === false) { + extensionMgr.onMessage("Please make sure the Dropbox authorization popup is not blocked by your browser."); + // If not immediate we add time for user to enter his + // credentials + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + } + client.reset(); + client.authenticate({ + interactive: !immediate + }, function(error, client) { + // Success + if(client.authState === Dropbox.Client.DONE) { + authenticated = true; + task.chain(); + return; + } + // If immediate did not work retry without immediate flag + if(immediate === true) { + immediate = false; + task.chain(localAuthenticate); + return; + } + // Error + task.error(new Error("Access to Dropbox account is not authorized.")); + }); + } + task.chain(localAuthenticate); + }); + } - dropboxHelper.upload = function(path, content, callback) { - var result = undefined; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - client.writeFile(path, content, function(error, stat) { - if (!error) { - result = stat; - task.chain(); - return; - } - // Handle error - if(error.status === 400) { - error = 'Could not upload document into path "' + path + '".'; - } - handleError(error, task); - }); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + dropboxHelper.upload = function(path, content, callback) { + var result = undefined; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + client.writeFile(path, content, function(error, stat) { + if(!error) { + result = stat; + task.chain(); + return; + } + // Handle error + if(error.status === 400) { + error = 'Could not upload document into path "' + path + '".'; + } + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - dropboxHelper.checkChanges = function(lastChangeId, callback) { - var changes = []; - var newChangeId = lastChangeId || 0; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - function retrievePageOfChanges() { - client.pullChanges(newChangeId, function(error, pullChanges) { - if (error) { - handleError(error, task); - return; - } - // Retrieve success - newChangeId = pullChanges.cursor(); - if(pullChanges.changes !== undefined) { - changes = changes.concat(pullChanges.changes); - } - if (pullChanges.shouldPullAgain) { - task.chain(retrievePageOfChanges); - } else { - task.chain(); - } - }); - } - task.chain(retrievePageOfChanges); - }); - task.onSuccess(function() { - callback(undefined, changes, newChangeId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + dropboxHelper.checkChanges = function(lastChangeId, callback) { + var changes = []; + var newChangeId = lastChangeId || 0; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + function retrievePageOfChanges() { + client.pullChanges(newChangeId, function(error, pullChanges) { + if(error) { + handleError(error, task); + return; + } + // Retrieve success + newChangeId = pullChanges.cursor(); + if(pullChanges.changes !== undefined) { + changes = changes.concat(pullChanges.changes); + } + if(pullChanges.shouldPullAgain) { + task.chain(retrievePageOfChanges); + } + else { + task.chain(); + } + }); + } + task.chain(retrievePageOfChanges); + }); + task.onSuccess(function() { + callback(undefined, changes, newChangeId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - dropboxHelper.downloadMetadata = function(paths, callback) { - var result = []; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - function recursiveDownloadMetadata() { - if(paths.length === 0) { - task.chain(); - return; - } - var path = paths[0]; - client.stat(path, function(error, stat) { - if(stat) { - result.push(stat); - paths.shift(); - task.chain(recursiveDownloadMetadata); - return; - } - handleError(error, task); - }); - } - task.chain(recursiveDownloadMetadata); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + dropboxHelper.downloadMetadata = function(paths, callback) { + var result = []; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + function recursiveDownloadMetadata() { + if(paths.length === 0) { + task.chain(); + return; + } + var path = paths[0]; + client.stat(path, function(error, stat) { + if(stat) { + result.push(stat); + paths.shift(); + task.chain(recursiveDownloadMetadata); + return; + } + handleError(error, task); + }); + } + task.chain(recursiveDownloadMetadata); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - dropboxHelper.downloadContent = function(objects, callback) { - var result = []; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - function recursiveDownloadContent() { - if(objects.length === 0) { - task.chain(); - return; - } - var object = objects[0]; - result.push(object); - var file = undefined; - // object may be a file - if(object.isFile === true) { - file = object; - } - // object may be a change - else if(object.wasRemoved !== undefined) { - file = object.stat; - } - if(!file) { - objects.shift(); - task.chain(recursiveDownloadContent); - return; - } - client.readFile(file.path, function(error, data) { - if(data) { - file.content = data; - objects.shift(); - task.chain(recursiveDownloadContent); - return; - } - handleError(error, task); - }); - } - task.chain(recursiveDownloadContent); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = true; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = error; - } - else { - errorMsg = "Dropbox error (" - + error.status + ": " + error.responseText + ")."; + dropboxHelper.downloadContent = function(objects, callback) { + var result = []; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + function recursiveDownloadContent() { + if(objects.length === 0) { + task.chain(); + return; + } + var object = objects[0]; + result.push(object); + var file = undefined; + // object may be a file + if(object.isFile === true) { + file = object; + } + // object may be a change + else if(object.wasRemoved !== undefined) { + file = object.stat; + } + if(!file) { + objects.shift(); + task.chain(recursiveDownloadContent); + return; + } + client.readFile(file.path, function(error, data) { + if(data) { + file.content = data; + objects.shift(); + task.chain(recursiveDownloadContent); + return; + } + handleError(error, task); + }); + } + task.chain(recursiveDownloadContent); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - if (error.status === 401 || error.status === 403) { - authenticated = false; - errorMsg = "Access to Dropbox account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; - } else if(error.status === 400 && error.responseText - .indexOf("oauth_nonce") !== -1) { - // A bug I guess... - _.each(_.keys(localStorage), function(key) { - // We have to remove the Oauth cache from the localStorage - if(key.indexOf("dropbox-auth") === 0) { - localStorage.removeItem(key); - } - }); - authenticated = false; - task.retry(new Error(errorMsg), 1); - return; - } else if (error.status <= 0) { - client = undefined; - authenticated = false; - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + function handleError(error, task) { + var errorMsg = true; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Dropbox error (" + error.status + ": " + error.responseText + ")."; - var pickerLoaded = false; - function loadPicker(task) { - task.onRun(function() { - if (pickerLoaded === true) { - task.chain(); - return; - } - $.ajax({ - url : "https://www.dropbox.com/static/api/1/dropbox.js", - dataType : "script", timeout : AJAX_TIMEOUT - }).done(function() { - pickerLoaded = true; - task.chain(); - }).fail(function(jqXHR) { - var error = { - status: jqXHR.status, - responseText: jqXHR.statusText - }; - handleError(error, task); - }); - }); - } - - dropboxHelper.picker = function(callback) { - var paths = []; - var task = asyncRunner.createTask(); - // Add some time for user to choose his files - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - connect(task); - loadPicker(task); - task.onRun(function() { - var options = {}; - options.multiselect = true; - options.linkType = "direct"; - options.success = function(files) { - for(var i=0; i:"\|?\*]+$/)) { - extensionMgr.onError('"' + path + '" contains invalid characters.'); - return undefined; - } - if(path.indexOf("/") !== 0) { - return "/" + path; - } - return path; - } - - function createSyncIndex(path) { - return "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase()); - } - - function createSyncAttributes(path, versionTag, content) { - var syncAttributes = {}; - syncAttributes.provider = dropboxProvider; - syncAttributes.path = path; - syncAttributes.version = versionTag; - syncAttributes.contentCRC = utils.crc32(content); - syncAttributes.syncIndex = createSyncIndex(path); - utils.storeAttributes(syncAttributes); - return syncAttributes; - } - - function importFilesFromPaths(paths) { - dropboxHelper.downloadMetadata(paths, function(error, result) { - if(error) { - return; - } - dropboxHelper.downloadContent(result, function(error, result) { - if(error) { - return; - } - var fileDescList = []; - _.each(result, function(file) { - var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content); - var syncLocations = {}; - syncLocations[syncAttributes.syncIndex] = syncAttributes; - var fileDesc = fileMgr.createFile(file.name, file.content, syncLocations); - fileMgr.selectFile(fileDesc); - fileDescList.push(fileDesc); - }); - extensionMgr.onSyncImportSuccess(fileDescList, dropboxProvider); - }); - }); - } - dropboxProvider.importFiles = function() { - dropboxHelper.picker(function(error, paths) { - if(error || paths.length === 0) { - return; - } - var importPaths = []; - _.each(paths, function(path) { - var syncIndex = createSyncIndex(path); - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - if(fileDesc !== undefined) { - extensionMgr.onError('"' + fileDesc.title + '" was already imported'); - return; - } - importPaths.push(path); - }); - importFilesFromPaths(importPaths); - }); - }; - - function exportFileToPath(path, title, content, callback) { - path = checkPath(path); - if(path === undefined) { - callback(true); - return; - } - // Check that file is not synchronized with an other one - var syncIndex = createSyncIndex(path); - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - if(fileDesc !== undefined) { - var existingTitle = fileDesc.title; - extensionMgr.onError('File path is already synchronized with "' + existingTitle + '"'); - callback(true); - return; - } - dropboxHelper.upload(path, content, function(error, result) { - if (error) { - callback(error); - return; - } - var syncAttributes = createSyncAttributes(result.path, result.versionTag, content); - callback(undefined, syncAttributes); - }); - } - - dropboxProvider.exportFile = function(event, title, content, callback) { - var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event); - exportFileToPath(path, title, content, callback); - }; + var PROVIDER_DROPBOX = "dropbox"; - dropboxProvider.exportManual = function(event, title, content, callback) { - var path = utils.getInputTextValue("#input-sync-manual-dropbox-path", event); - exportFileToPath(path, title, content, callback); - }; - - dropboxProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) { - var syncContentCRC = syncAttributes.contentCRC; - // Skip if CRC has not changed - if(uploadContentCRC == syncContentCRC) { - callback(undefined, false); - return; - } - dropboxHelper.upload(syncAttributes.path, uploadContent, function(error, result) { - if(error) { - callback(error, true); - return; - } - syncAttributes.version = result.versionTag; - syncAttributes.contentCRC = uploadContentCRC; - callback(undefined, true); - }); - }; - - dropboxProvider.syncDown = function(callback) { - var lastChangeId = localStorage[PROVIDER_DROPBOX + ".lastChangeId"]; - dropboxHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) { - if (error) { - callback(error); - return; - } - var interestingChanges = []; - _.each(changes, function(change) { - var syncIndex = createSyncIndex(change.path); - var syncAttributes = fileMgr.getSyncAttributes(syncIndex); - if(syncAttributes === undefined) { - return; - } - // Store syncAttributes to avoid 2 times searching - change.syncAttributes = syncAttributes; - // Delete - if(change.wasRemoved === true) { - interestingChanges.push(change); - return; - } - // Modify - if(syncAttributes.version != change.stat.versionTag) { - interestingChanges.push(change); - } - }); - dropboxHelper.downloadContent(interestingChanges, function(error, changes) { - if (error) { - callback(error); - return; - } - _.each(changes, function(change) { - var syncAttributes = change.syncAttributes; - var syncIndex = syncAttributes.syncIndex; - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - // No file corresponding (file may have been deleted locally) - if(fileDesc === undefined) { - return; - } - var localTitle = fileDesc.title; - // File deleted - if (change.wasRemoved === true) { - extensionMgr.onError('"' + localTitle + '" has been removed from Dropbox.'); - fileMgr.removeSync(syncAttributes); - return; - } - var localContent = fileDesc.getContent(); - var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent); - var file = change.stat; + var dropboxProvider = { + providerId: PROVIDER_DROPBOX, + providerName: "Dropbox", + defaultPublishFormat: "template" + }; + + function checkPath(path) { + if(path === undefined) { + return undefined; + } + if(!path.match(/^[^\\<>:"\|?\*]+$/)) { + extensionMgr.onError('"' + path + '" contains invalid characters.'); + return undefined; + } + if(path.indexOf("/") !== 0) { + return "/" + path; + } + return path; + } + + function createSyncIndex(path) { + return "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase()); + } + + function createSyncAttributes(path, versionTag, content) { + var syncAttributes = {}; + syncAttributes.provider = dropboxProvider; + syncAttributes.path = path; + syncAttributes.version = versionTag; + syncAttributes.contentCRC = utils.crc32(content); + syncAttributes.syncIndex = createSyncIndex(path); + utils.storeAttributes(syncAttributes); + return syncAttributes; + } + + function importFilesFromPaths(paths) { + dropboxHelper.downloadMetadata(paths, function(error, result) { + if(error) { + return; + } + dropboxHelper.downloadContent(result, function(error, result) { + if(error) { + return; + } + var fileDescList = []; + _.each(result, function(file) { + var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content); + var syncLocations = {}; + syncLocations[syncAttributes.syncIndex] = syncAttributes; + var fileDesc = fileMgr.createFile(file.name, file.content, syncLocations); + fileMgr.selectFile(fileDesc); + fileDescList.push(fileDesc); + }); + extensionMgr.onSyncImportSuccess(fileDescList, dropboxProvider); + }); + }); + } + + dropboxProvider.importFiles = function() { + dropboxHelper.picker(function(error, paths) { + if(error || paths.length === 0) { + return; + } + var importPaths = []; + _.each(paths, function(path) { + var syncIndex = createSyncIndex(path); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + extensionMgr.onError('"' + fileDesc.title + '" was already imported'); + return; + } + importPaths.push(path); + }); + importFilesFromPaths(importPaths); + }); + }; + + function exportFileToPath(path, title, content, callback) { + path = checkPath(path); + if(path === undefined) { + callback(true); + return; + } + // Check that file is not synchronized with an other one + var syncIndex = createSyncIndex(path); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + var existingTitle = fileDesc.title; + extensionMgr.onError('File path is already synchronized with "' + existingTitle + '"'); + callback(true); + return; + } + dropboxHelper.upload(path, content, function(error, result) { + if(error) { + callback(error); + return; + } + var syncAttributes = createSyncAttributes(result.path, result.versionTag, content); + callback(undefined, syncAttributes); + }); + } + + dropboxProvider.exportFile = function(event, title, content, callback) { + var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event); + exportFileToPath(path, title, content, callback); + }; + + dropboxProvider.exportManual = function(event, title, content, callback) { + var path = utils.getInputTextValue("#input-sync-manual-dropbox-path", event); + exportFileToPath(path, title, content, callback); + }; + + dropboxProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) { + var syncContentCRC = syncAttributes.contentCRC; + // Skip if CRC has not changed + if(uploadContentCRC == syncContentCRC) { + callback(undefined, false); + return; + } + dropboxHelper.upload(syncAttributes.path, uploadContent, function(error, result) { + if(error) { + callback(error, true); + return; + } + syncAttributes.version = result.versionTag; + syncAttributes.contentCRC = uploadContentCRC; + callback(undefined, true); + }); + }; + + dropboxProvider.syncDown = function(callback) { + var lastChangeId = localStorage[PROVIDER_DROPBOX + ".lastChangeId"]; + dropboxHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) { + if(error) { + callback(error); + return; + } + var interestingChanges = []; + _.each(changes, function(change) { + var syncIndex = createSyncIndex(change.path); + var syncAttributes = fileMgr.getSyncAttributes(syncIndex); + if(syncAttributes === undefined) { + return; + } + // Store syncAttributes to avoid 2 times searching + change.syncAttributes = syncAttributes; + // Delete + if(change.wasRemoved === true) { + interestingChanges.push(change); + return; + } + // Modify + if(syncAttributes.version != change.stat.versionTag) { + interestingChanges.push(change); + } + }); + dropboxHelper.downloadContent(interestingChanges, function(error, changes) { + if(error) { + callback(error); + return; + } + _.each(changes, function(change) { + var syncAttributes = change.syncAttributes; + var syncIndex = syncAttributes.syncIndex; + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + // No file corresponding (file may have been deleted + // locally) + if(fileDesc === undefined) { + return; + } + var localTitle = fileDesc.title; + // File deleted + if(change.wasRemoved === true) { + extensionMgr.onError('"' + localTitle + '" has been removed from Dropbox.'); + fileMgr.removeSync(syncAttributes); + return; + } + var localContent = fileDesc.content; + var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent); + var file = change.stat; var remoteContentCRC = utils.crc32(file.content); var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; - var fileContentChanged = localContent != file.content; - // Conflict detection - if (fileContentChanged === true && localContentChanged === true && remoteContentChanged === true) { - fileMgr.createFile(localTitle + " (backup)", localContent); - extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); - } - // If file content changed - if(fileContentChanged && remoteContentChanged === true) { - fileDesc.setContent(file.content); - extensionMgr.onMessage('"' + localTitle + '" has been updated from Dropbox.'); - if(fileMgr.isCurrentFile(fileDesc)) { - fileMgr.selectFile(); // Refresh editor - } - } - // Update syncAttributes - syncAttributes.version = file.versionTag; - syncAttributes.contentCRC = remoteContentCRC; - utils.storeAttributes(syncAttributes); - }); - localStorage[PROVIDER_DROPBOX + ".lastChangeId"] = newChangeId; - callback(); - }); - }); - }; - - dropboxProvider.publish = function(publishAttributes, title, content, callback) { - var path = checkPath(publishAttributes.path); - if(path === undefined) { - callback(true); - return; - } - dropboxHelper.upload(path, content, callback); - }; + var fileContentChanged = localContent != file.content; + // Conflict detection + if(fileContentChanged === true && localContentChanged === true && remoteContentChanged === true) { + fileMgr.createFile(localTitle + " (backup)", localContent); + extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); + } + // If file content changed + if(fileContentChanged && remoteContentChanged === true) { + fileDesc.content = file.content; + extensionMgr.onMessage('"' + localTitle + '" has been updated from Dropbox.'); + if(fileMgr.isCurrentFile(fileDesc)) { + fileMgr.selectFile(); // Refresh editor + } + } + // Update syncAttributes + syncAttributes.version = file.versionTag; + syncAttributes.contentCRC = remoteContentCRC; + utils.storeAttributes(syncAttributes); + }); + localStorage[PROVIDER_DROPBOX + ".lastChangeId"] = newChangeId; + callback(); + }); + }); + }; - dropboxProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.path = utils.getInputTextValue("#input-publish-dropbox-path", event); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + dropboxProvider.publish = function(publishAttributes, title, content, callback) { + var path = checkPath(publishAttributes.path); + if(path === undefined) { + callback(true); + return; + } + dropboxHelper.upload(path, content, callback); + }; - return dropboxProvider; + dropboxProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.path = utils.getInputTextValue("#input-publish-dropbox-path", event); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + return dropboxProvider; }); \ No newline at end of file diff --git a/js/extension-manager.js b/js/extension-manager.js index bcd22f82..42ebc592 100644 --- a/js/extension-manager.js +++ b/js/extension-manager.js @@ -1,4 +1,4 @@ -define( [ +define([ "jquery", "underscore", "utils", @@ -20,167 +20,162 @@ define( [ "extensions/scroll-link", "lib/bootstrap" ], function($, _, utils, settings) { - - var extensionMgr = {}; - - // Create a list of extensions - var extensionList = _.chain( - arguments - ).map(function(argument) { - return _.isObject(argument) && argument.extensionId && argument; - }).compact().value(); - // Return every named callbacks implemented in extensions - function getExtensionCallbackList(hookName) { - return _.chain( - extensionList - ).map(function(extension) { - return extension.config.enabled && extension[hookName]; - }).compact().value(); - } - - // Return a function that calls every callbacks from extensions - function createHook(hookName) { - var callbackList = getExtensionCallbackList(hookName); - return function() { - logger.debug(hookName, arguments); - var callbackArguments = arguments; - _.each(callbackList, function(callback) { - callback.apply(null, callbackArguments); - }); - }; - } - - // Add a Hook to the extensionMgr - function addHook(hookName) { - extensionMgr[hookName] = createHook(hookName); - } - - // Set extension config - extensionSettings = settings.extensionSettings || {}; - _.each(extensionList, function(extension) { - extension.config = _.extend({}, extension.defaultConfig, extensionSettings[extension.extensionId]); - extension.config.enabled = !extension.optional || extension.config.enabled === undefined || extension.config.enabled === true; - }); - - // Load/Save extension config from/to settings - extensionMgr["onLoadSettings"] = function() { - logger.debug("onLoadSettings"); - _.each(extensionList, function(extension) { - utils.setInputChecked("#input-enable-extension-" + extension.extensionId, extension.config.enabled); - var onLoadSettingsCallback = extension.onLoadSettings; - onLoadSettingsCallback && onLoadSettingsCallback(); - }); - }; - extensionMgr["onSaveSettings"] = function(newExtensionSettings, event) { - logger.debug("onSaveSettings"); - _.each(extensionList, function(extension) { - var newExtensionConfig = extension.defaultConfig || {}; - newExtensionConfig.enabled = utils.getInputChecked("#input-enable-extension-" + extension.extensionId); - var onSaveSettingsCallback = extension.onSaveSettings; - onSaveSettingsCallback && onSaveSettingsCallback(newExtensionConfig, event); - newExtensionSettings[extension.extensionId] = newExtensionConfig; - }); - }; - - addHook("onReady"); - addHook("onMessage"); - addHook("onError"); - addHook("onOfflineChanged"); - addHook("onAsyncRunning"); - - // To access modules that are loaded after extensions - addHook("onFileMgrCreated"); - addHook("onSynchronizerCreated"); - addHook("onPublisherCreated"); - - // Operations on files - addHook("onFileCreated"); - addHook("onFileDeleted"); - addHook("onFileSelected"); - addHook("onContentChanged"); - addHook("onTitleChanged"); - - // Sync events - addHook("onSyncRunning"); - addHook("onSyncSuccess"); - addHook("onSyncImportSuccess"); - addHook("onSyncExportSuccess"); - addHook("onSyncRemoved"); - - // Publish events - addHook("onPublishRunning"); - addHook("onPublishSuccess"); - addHook("onNewPublishSuccess"); - addHook("onPublishRemoved"); - - // Operations on Layout - addHook("onLayoutConfigure"); - addHook("onLayoutCreated"); - - // Operations on PageDown - addHook("onEditorConfigure"); - - var onPreviewFinished = createHook("onPreviewFinished"); - var onAsyncPreviewCallbackList = getExtensionCallbackList("onAsyncPreview"); - extensionMgr["onAsyncPreview"] = function() { - logger.debug("onAsyncPreview"); - // Call onPreviewFinished callbacks when all async preview are finished - var counter = 0; - function tryFinished() { - if(counter === onAsyncPreviewCallbackList.length) { - onPreviewFinished(); - } - } - _.each(onAsyncPreviewCallbackList, function(asyncPreviewCallback) { - asyncPreviewCallback(function() { - counter++; - tryFinished(); - }); - }); - tryFinished(); - }; - - var accordionTmpl = [ + var extensionMgr = {}; + + // Create a list of extensions + var extensionList = _.chain(arguments).map(function(argument) { + return _.isObject(argument) && argument.extensionId && argument; + }).compact().value(); + + // Return every named callbacks implemented in extensions + function getExtensionCallbackList(hookName) { + return _.chain(extensionList).map(function(extension) { + return extension.config.enabled && extension[hookName]; + }).compact().value(); + } + + // Return a function that calls every callbacks from extensions + function createHook(hookName) { + var callbackList = getExtensionCallbackList(hookName); + return function() { + logger.debug(hookName, arguments); + var callbackArguments = arguments; + _.each(callbackList, function(callback) { + callback.apply(null, callbackArguments); + }); + }; + } + + // Add a Hook to the extensionMgr + function addHook(hookName) { + extensionMgr[hookName] = createHook(hookName); + } + + // Set extension config + extensionSettings = settings.extensionSettings || {}; + _.each(extensionList, function(extension) { + extension.config = _.extend({}, extension.defaultConfig, extensionSettings[extension.extensionId]); + extension.config.enabled = !extension.optional || extension.config.enabled === undefined || extension.config.enabled === true; + }); + + // Load/Save extension config from/to settings + extensionMgr["onLoadSettings"] = function() { + logger.debug("onLoadSettings"); + _.each(extensionList, function(extension) { + utils.setInputChecked("#input-enable-extension-" + extension.extensionId, extension.config.enabled); + var onLoadSettingsCallback = extension.onLoadSettings; + onLoadSettingsCallback && onLoadSettingsCallback(); + }); + }; + extensionMgr["onSaveSettings"] = function(newExtensionSettings, event) { + logger.debug("onSaveSettings"); + _.each(extensionList, function(extension) { + var newExtensionConfig = _.extend({}, extension.defaultConfig); + newExtensionConfig.enabled = utils.getInputChecked("#input-enable-extension-" + extension.extensionId); + var onSaveSettingsCallback = extension.onSaveSettings; + onSaveSettingsCallback && onSaveSettingsCallback(newExtensionConfig, event); + newExtensionSettings[extension.extensionId] = newExtensionConfig; + }); + }; + + addHook("onReady"); + addHook("onMessage"); + addHook("onError"); + addHook("onOfflineChanged"); + addHook("onAsyncRunning"); + + // To access modules that are loaded after extensions + addHook("onFileMgrCreated"); + addHook("onSynchronizerCreated"); + addHook("onPublisherCreated"); + + // Operations on files + addHook("onFileCreated"); + addHook("onFileDeleted"); + addHook("onFileSelected"); + addHook("onContentChanged"); + addHook("onTitleChanged"); + + // Sync events + addHook("onSyncRunning"); + addHook("onSyncSuccess"); + addHook("onSyncImportSuccess"); + addHook("onSyncExportSuccess"); + addHook("onSyncRemoved"); + + // Publish events + addHook("onPublishRunning"); + addHook("onPublishSuccess"); + addHook("onNewPublishSuccess"); + addHook("onPublishRemoved"); + + // Operations on Layout + addHook("onLayoutConfigure"); + addHook("onLayoutCreated"); + + // Operations on PageDown + addHook("onEditorConfigure"); + + var onPreviewFinished = createHook("onPreviewFinished"); + var onAsyncPreviewCallbackList = getExtensionCallbackList("onAsyncPreview"); + extensionMgr["onAsyncPreview"] = function() { + logger.debug("onAsyncPreview"); + // Call onPreviewFinished callbacks when all async preview are finished + var counter = 0; + function tryFinished() { + if(counter === onAsyncPreviewCallbackList.length) { + onPreviewFinished(); + } + } + _.each(onAsyncPreviewCallbackList, function(asyncPreviewCallback) { + asyncPreviewCallback(function() { + counter++; + tryFinished(); + }); + }); + tryFinished(); + }; + + var accordionTmpl = [ '
', - '
', - '', - '', - '<%= extensionName %>', - '', - '
', - '
', - '
<%= settingsBloc %>
', - '
', - '
'].join(""); - - function createSettings(extension) { - $("#accordion-extensions").append($(_.template(accordionTmpl, { - extensionId: extension.extensionId, - extensionName: extension.extensionName, - optional: extension.optional, - settingsBloc: extension.settingsBloc - }))); - } + '
', + ' ', + ' ', + ' <%= extensionName %>', + ' ', + '
', + '
', + '
<%= settingsBloc %>
', + '
', + '' + ].join(""); - $(function() { - // Create accordion in settings dialog - _.chain( - extensionList - ).sortBy(function(extension) { - return extension.extensionName.toLowerCase(); - }).each(createSettings); - - // Create extension buttons - logger.debug("onCreateButton"); - var onCreateButtonCallbackList = getExtensionCallbackList("onCreateButton"); - _.each(onCreateButtonCallbackList, function(callback) { - $("#extension-buttons").append($('
').append(callback())); - }); + function createSettings(extension) { + $("#accordion-extensions").append($(_.template(accordionTmpl, { + extensionId: extension.extensionId, + extensionName: extension.extensionName, + optional: extension.optional, + settingsBloc: extension.settingsBloc + }))); + } - }); - - return extensionMgr; + $(function() { + // Create accordion in settings dialog + _.chain(extensionList).sortBy(function(extension) { + return extension.extensionName.toLowerCase(); + }).each(createSettings); + + // Create extension buttons + logger.debug("onCreateButton"); + var onCreateButtonCallbackList = getExtensionCallbackList("onCreateButton"); + _.each(onCreateButtonCallbackList, function(callback) { + $("#extension-buttons").append($('
').append(callback())); + }); + + }); + + return extensionMgr; }); \ No newline at end of file diff --git a/js/extensions/button-publish.js b/js/extensions/button-publish.js index fe2f92d8..64810558 100644 --- a/js/extensions/button-publish.js +++ b/js/extensions/button-publish.js @@ -2,78 +2,79 @@ define([ "jquery", "underscore" ], function($, _) { - - var buttonPublish = { - extensionId: "buttonPublish", - extensionName: 'Button "Publish"', - settingsBloc: '

Adds a "Publish document" button in the navigation bar.

' - }; - - var button = undefined; - var currentFileDesc = undefined; - var publishRunning = false; - var hasPublications = false; - var isOffline = false; - // Enable/disable the button - function updateButtonState() { - if(button === undefined) { - return; - } - if(publishRunning === true || hasPublications === false || isOffline === true) { - button.addClass("disabled"); - } - else { - button.removeClass("disabled"); - } - }; - - var publisher = undefined; - buttonPublish.onPublisherCreated = function(publisherParameter) { - publisher = publisherParameter; - }; - - buttonPublish.onCreateButton = function() { - button = $([ - ''].join("") - ).click(function() { - if(!$(this).hasClass("disabled")) { - publisher.publish(); - } - }); - return button; - }; - - buttonPublish.onPublishRunning = function(isRunning) { - publishRunning = isRunning; - updateButtonState(); - }; - - buttonPublish.onOfflineChanged = function(isOfflineParameter) { - isOffline = isOfflineParameter; - updateButtonState(); - }; - - // Check that current file has publications - var checkPublication = function() { - if(_.size(currentFileDesc.publishLocations) === 0) { - hasPublications = false; - } - else { - hasPublications = true; - } - updateButtonState(); - }; - - buttonPublish.onFileSelected = function(fileDesc) { - currentFileDesc = fileDesc; - checkPublication(); - }; - - buttonPublish.onPublishRemoved = checkPublication; - buttonPublish.onNewPublishSuccess = checkPublication; - - return buttonPublish; - + + var buttonPublish = { + extensionId: "buttonPublish", + extensionName: 'Button "Publish"', + settingsBloc: '

Adds a "Publish document" button in the navigation bar.

' + }; + + var button = undefined; + var currentFileDesc = undefined; + var publishRunning = false; + var hasPublications = false; + var isOffline = false; + // Enable/disable the button + function updateButtonState() { + if(button === undefined) { + return; + } + if(publishRunning === true || hasPublications === false || isOffline === true) { + button.addClass("disabled"); + } + else { + button.removeClass("disabled"); + } + } + ; + + var publisher = undefined; + buttonPublish.onPublisherCreated = function(publisherParameter) { + publisher = publisherParameter; + }; + + buttonPublish.onCreateButton = function() { + button = $([ + '' + ].join("")).click(function() { + if(!$(this).hasClass("disabled")) { + publisher.publish(); + } + }); + return button; + }; + + buttonPublish.onPublishRunning = function(isRunning) { + publishRunning = isRunning; + updateButtonState(); + }; + + buttonPublish.onOfflineChanged = function(isOfflineParameter) { + isOffline = isOfflineParameter; + updateButtonState(); + }; + + // Check that current file has publications + var checkPublication = function() { + if(_.size(currentFileDesc.publishLocations) === 0) { + hasPublications = false; + } + else { + hasPublications = true; + } + updateButtonState(); + }; + + buttonPublish.onFileSelected = function(fileDesc) { + currentFileDesc = fileDesc; + checkPublication(); + }; + + buttonPublish.onPublishRemoved = checkPublication; + buttonPublish.onNewPublishSuccess = checkPublication; + + return buttonPublish; + }); \ No newline at end of file diff --git a/js/extensions/button-share.js b/js/extensions/button-share.js index 55b21b73..5963f786 100644 --- a/js/extensions/button-share.js +++ b/js/extensions/button-share.js @@ -2,71 +2,73 @@ define([ "jquery", "underscore" ], function($, _) { - - var buttonShare = { - extensionId: "buttonShare", - extensionName: 'Button "Share"', + + var buttonShare = { + extensionId: "buttonShare", + extensionName: 'Button "Share"', optional: true, - settingsBloc: '

Adds a "Share document" button in the navigation bar.

' - }; - - buttonShare.onCreateButton = function() { - return $([ - '', - ''].join("") - ); - }; - - var fileDesc = undefined; - var lineTemplate = [ + settingsBloc: '

Adds a "Share document" button in the navigation bar.

' + }; + + buttonShare.onCreateButton = function() { + return $([ + '', + '' + ].join("")); + }; + + var fileDesc = undefined; + var lineTemplate = [ '
', - '', - '', - '
'].join(""); - var refreshDocumentSharing = function(fileDescParameter) { - if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { - return; - } - - var linkList = $("#link-container .link-list").empty(); - $("#link-container .no-link").show(); - - var attributesList = _.values(fileDesc.publishLocations); - _.each(attributesList, function(attributes) { - if(attributes.sharingLink) { - var lineElement = $(_.template(lineTemplate, { - link: attributes.sharingLink - })); - lineElement.click(function(event) { - event.stopPropagation(); - }); - linkList.append(lineElement); - $("#link-container .no-link").hide(); - } - }); - }; - - buttonShare.onFileSelected = function(fileDescParameter) { - fileDesc = fileDescParameter; - refreshDocumentSharing(fileDescParameter); - }; - - buttonShare.onNewPublishSuccess = refreshDocumentSharing; - buttonShare.onPublishRemoved = refreshDocumentSharing; - - return buttonShare; - + ' ', + ' ', + '
' + ].join(""); + var refreshDocumentSharing = function(fileDescParameter) { + if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { + return; + } + + var linkList = $("#link-container .link-list").empty(); + $("#link-container .no-link").show(); + + var attributesList = _.values(fileDesc.publishLocations); + _.each(attributesList, function(attributes) { + if(attributes.sharingLink) { + var lineElement = $(_.template(lineTemplate, { + link: attributes.sharingLink + })); + lineElement.click(function(event) { + event.stopPropagation(); + }); + linkList.append(lineElement); + $("#link-container .no-link").hide(); + } + }); + }; + + buttonShare.onFileSelected = function(fileDescParameter) { + fileDesc = fileDescParameter; + refreshDocumentSharing(fileDescParameter); + }; + + buttonShare.onNewPublishSuccess = refreshDocumentSharing; + buttonShare.onPublishRemoved = refreshDocumentSharing; + + return buttonShare; + }); \ No newline at end of file diff --git a/js/extensions/button-stat.js b/js/extensions/button-stat.js index 0a0872cd..4ec914df 100644 --- a/js/extensions/button-stat.js +++ b/js/extensions/button-stat.js @@ -1,72 +1,78 @@ define([ "jquery", - "underscore" -], function($, _) { - - var buttonStat = { - extensionId: "buttonStat", - extensionName: 'Button "Statistics"', + "underscore", + "utils" +], function($, _, utils) { + + var buttonStat = { + extensionId: "buttonStat", + extensionName: 'Button "Statistics"', optional: true, - settingsBloc: '

Adds a "Document statistics" button in the navigation bar.

' - }; - - buttonStat.onCreateButton = function() { - return $([ - '', - ''].join("") - ); - }; - - var fileDesc = undefined; - var lineTemplate = [ - '
', - '', - '', - '
'].join(""); - var refreshDocumentSharing = function(fileDescParameter) { - if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { - return; - } - - var linkList = $("#link-container .link-list").empty(); - $("#link-container .no-link").show(); - - var attributesList = _.values(fileDesc.publishLocations); - _.each(attributesList, function(attributes) { - if(attributes.sharingLink) { - var lineElement = $(_.template(lineTemplate, { - link: attributes.sharingLink - })); - lineElement.click(function(event) { - event.stopPropagation(); - }); - linkList.append(lineElement); - $("#link-container .no-link").hide(); - } - }); - }; - - buttonStat.onFileSelected = function(fileDescParameter) { - fileDesc = fileDescParameter; - refreshDocumentSharing(fileDescParameter); - }; - - buttonStat.onNewPublishSuccess = refreshDocumentSharing; - buttonStat.onPublishRemoved = refreshDocumentSharing; - - return buttonStat; - + defaultConfig: { + name1: "Words", + value1: "\\S+", + name2: "Characters", + value2: "\\S", + name3: "Paragraphs", + value3: ".+", + }, + settingsBloc: [ + '

Adds a "Document statistics" button in the navigation bar.

', + '

', + ' ', + ' ', + ' ', + ' ', + '

', + '

', + ' ', + ' ', + ' ', + ' ', + '

', + '

', + ' ', + ' ', + ' ', + ' ', + '

'].join("") + }; + + buttonStat.onLoadSettings = function() { + _.each(buttonStat.defaultConfig, function(value, key) { + utils.setInputValue("#input-stat-" + key, buttonStat.config[key]); + }); + }; + + buttonStat.onSaveSettings = function(newConfig, event) { + _.each(buttonStat.defaultConfig, function(value, key) { + newConfig[key] = utils.getInputTextValue("#input-stat-" + key, event); + }); + }; + + buttonStat.onCreateButton = function() { + return $([ + '', + '' + ].join("")); + }; + + buttonStat.onPreviewFinished = function() { + var text = $("#wmd-preview").text(); + $("#span-stat-value1").text(text.match(new RegExp(buttonStat.config.value1, "g")).length); + $("#span-stat-value2").text(text.match(new RegExp(buttonStat.config.value2, "g")).length); + $("#span-stat-value3").text(text.match(new RegExp(buttonStat.config.value3, "g")).length); + }; + + return buttonStat; + }); \ No newline at end of file diff --git a/js/extensions/button-sync.js b/js/extensions/button-sync.js index 3d9d3dbe..07938733 100644 --- a/js/extensions/button-sync.js +++ b/js/extensions/button-sync.js @@ -2,77 +2,77 @@ define([ "jquery", "underscore" ], function($, _) { - - var buttonSync = { - extensionId: "buttonSync", - extensionName: 'Button "Synchronize"', - settingsBloc: '

Adds a "Synchronize documents" button in the navigation bar.

' - }; - - var button = undefined; - var syncRunning = false; - var uploadPending = false; - var isOffline = false; - // Enable/disable the button - var updateButtonState = function() { - if(button === undefined) { - return; - } - if(syncRunning === true || uploadPending === false || isOffline) { - button.addClass("disabled"); - } - else { - button.removeClass("disabled"); - } - }; - - var synchronizer = undefined; - buttonSync.onSynchronizerCreated = function(synchronizerParameter) { - synchronizer = synchronizerParameter; - }; - - buttonSync.onCreateButton = function() { - button = $([ - ''].join("") - ).click(function() { - if(!$(this).hasClass("disabled")) { - synchronizer.forceSync(); - } - }); - return button; - }; - - buttonSync.onReady = updateButtonState; - - buttonSync.onSyncRunning = function(isRunning) { - syncRunning = isRunning; - uploadPending = true; - updateButtonState(); - }; - - buttonSync.onSyncSuccess = function() { - uploadPending = false; - updateButtonState(); - }; - - buttonSync.onOfflineChanged = function(isOfflineParameter) { - isOffline = isOfflineParameter; - updateButtonState(); - }; - - // Check that a file has synchronized locations - var checkSynchronization = function(fileDesc) { - if(_.size(fileDesc.syncLocations) !== 0) { - uploadPending = true; - updateButtonState(); - } - }; - - buttonSync.onContentChanged = checkSynchronization; - buttonSync.onTitleChanged = checkSynchronization; - - return buttonSync; - + + var buttonSync = { + extensionId: "buttonSync", + extensionName: 'Button "Synchronize"', + settingsBloc: '

Adds a "Synchronize documents" button in the navigation bar.

' + }; + + var button = undefined; + var syncRunning = false; + var uploadPending = false; + var isOffline = false; + // Enable/disable the button + var updateButtonState = function() { + if(button === undefined) { + return; + } + if(syncRunning === true || uploadPending === false || isOffline) { + button.addClass("disabled"); + } + else { + button.removeClass("disabled"); + } + }; + + var synchronizer = undefined; + buttonSync.onSynchronizerCreated = function(synchronizerParameter) { + synchronizer = synchronizerParameter; + }; + + buttonSync.onCreateButton = function() { + button = $([ + '' + ].join("")).click(function() { + if(!$(this).hasClass("disabled")) { + synchronizer.forceSync(); + } + }); + return button; + }; + + buttonSync.onReady = updateButtonState; + + buttonSync.onSyncRunning = function(isRunning) { + syncRunning = isRunning; + uploadPending = true; + updateButtonState(); + }; + + buttonSync.onSyncSuccess = function() { + uploadPending = false; + updateButtonState(); + }; + + buttonSync.onOfflineChanged = function(isOfflineParameter) { + isOffline = isOfflineParameter; + updateButtonState(); + }; + + // Check that a file has synchronized locations + var checkSynchronization = function(fileDesc) { + if(_.size(fileDesc.syncLocations) !== 0) { + uploadPending = true; + updateButtonState(); + } + }; + + buttonSync.onContentChanged = checkSynchronization; + buttonSync.onTitleChanged = checkSynchronization; + + return buttonSync; + }); \ No newline at end of file diff --git a/js/extensions/document-selector.js b/js/extensions/document-selector.js index 727cbab5..0ac99ea0 100644 --- a/js/extensions/document-selector.js +++ b/js/extensions/document-selector.js @@ -3,70 +3,99 @@ define([ "underscore", "file-system" ], function($, _, fileSystem) { - - var documentSelector = { - extensionId: "documentSelector", - extensionName: "Document selector", - settingsBloc: '

Builds the "Open document" dropdown menu.

' - }; - - var fileMgr = undefined; - documentSelector.onFileMgrCreated = function(fileMgrParameter) { - fileMgr = fileMgrParameter; - }; - - var liMap = undefined; - var buildSelector = function() { - - function composeTitle(fileDesc) { - var result = []; - var syncAttributesList = _.values(fileDesc.syncLocations); - var publishAttributesList = _.values(fileDesc.publishLocations); - var attributesList = syncAttributesList.concat(publishAttributesList); - _.chain(attributesList).sortBy(function(attributes) { - return attributes.provider.providerId; - }).each(function(attributes) { - result.push(''); - }); - result.push(" "); - result.push(fileDesc.title); - return result.join(""); - } - liMap = {}; - $("#file-selector li:not(.stick)").empty(); - _.chain( - fileSystem - ).sortBy(function(fileDesc) { - return fileDesc.title.toLowerCase(); - }).each(function(fileDesc) { - var a = $('').html(composeTitle(fileDesc)).click(function() { - if(!liMap[fileDesc.fileIndex].is(".disabled")) { - fileMgr.selectFile(fileDesc); - } - }); - var li = $("
  • ").append(a); - liMap[fileDesc.fileIndex] = li; - $("#file-selector").append(li); - }); - }; - - documentSelector.onFileSelected = function(fileDesc) { - if(liMap === undefined) { - buildSelector(); - } - $("#file-selector li:not(.stick)").removeClass("disabled"); - liMap[fileDesc.fileIndex].addClass("disabled"); - }; - - documentSelector.onFileCreated = buildSelector; - documentSelector.onFileDeleted = buildSelector; - documentSelector.onTitleChanged = buildSelector; - documentSelector.onSyncExportSuccess = buildSelector; - documentSelector.onSyncRemoved = buildSelector; - documentSelector.onNewPublishSuccess = buildSelector; - documentSelector.onPublishRemoved = buildSelector; - - return documentSelector; - + var documentSelector = { + extensionId: "documentSelector", + extensionName: "Document selector", + settingsBloc: '

    Builds the "Open document" dropdown menu.

    ' + }; + + var fileMgr = undefined; + documentSelector.onFileMgrCreated = function(fileMgrParameter) { + fileMgr = fileMgrParameter; + }; + + var liMap = undefined; + var buildSelector = function() { + + function composeTitle(fileDesc) { + var result = []; + var syncAttributesList = _.values(fileDesc.syncLocations); + var publishAttributesList = _.values(fileDesc.publishLocations); + var attributesList = syncAttributesList.concat(publishAttributesList); + _.chain(attributesList).sortBy(function(attributes) { + return attributes.provider.providerId; + }).each(function(attributes) { + result.push(''); + }); + result.push(" "); + result.push(fileDesc.title); + return result.join(""); + } + + liMap = {}; + $("#file-selector li:not(.stick)").empty(); + _.chain(fileSystem).sortBy(function(fileDesc) { + return fileDesc.title.toLowerCase(); + }).each(function(fileDesc) { + var a = $('
    ').html(composeTitle(fileDesc)).click(function() { + if(!liMap[fileDesc.fileIndex].is(".disabled")) { + fileMgr.selectFile(fileDesc); + } + }); + var li = $("
  • ").append(a); + liMap[fileDesc.fileIndex] = li; + $("#file-selector").append(li); + }); + }; + + documentSelector.onFileSelected = function(fileDesc) { + if(liMap === undefined) { + buildSelector(); + } + $("#file-selector li:not(.stick)").removeClass("disabled"); + liMap[fileDesc.fileIndex].addClass("disabled"); + }; + + documentSelector.onFileCreated = buildSelector; + documentSelector.onFileDeleted = buildSelector; + documentSelector.onTitleChanged = buildSelector; + documentSelector.onSyncExportSuccess = buildSelector; + documentSelector.onSyncRemoved = buildSelector; + documentSelector.onNewPublishSuccess = buildSelector; + documentSelector.onPublishRemoved = buildSelector; + + // Filter for search input in file selector + function filterFileSelector(filter) { + var liList = $("#file-selector li:not(.stick)"); + liList.show(); + if(filter) { + var words = filter.toLowerCase().split(/\s+/); + liList.each(function() { + var fileTitle = $(this).text().toLowerCase(); + if(_.some(words, function(word) { + return fileTitle.indexOf(word) === -1; + })) { + $(this).hide(); + } + }); + } + } + + documentSelector.onReady = function() { + $(".action-open-file").click(function() { + filterFileSelector(); + _.defer(function() { + $("#file-search").val("").focus(); + }); + }); + $("#file-search").keyup(function() { + filterFileSelector($(this).val()); + }).click(function(event) { + event.stopPropagation(); + }); + }; + + return documentSelector; + }); \ No newline at end of file diff --git a/js/extensions/document-title.js b/js/extensions/document-title.js index 56987ab1..80932d8b 100644 --- a/js/extensions/document-title.js +++ b/js/extensions/document-title.js @@ -2,62 +2,62 @@ define([ "jquery", "underscore" ], function($, _) { - - var documentTitle = { - extensionId: "documentTitle", - extensionName: "Document title", - settingsBloc: '

    Responsible for showing the document title in the navigation bar.

    ' - }; - - var layout = undefined; - documentTitle.onLayoutCreated = function(layoutParameter) { - layout = layoutParameter; - }; - - var fileDesc = undefined; - var updateTitle = function(fileDescParameter) { - if(fileDescParameter !== fileDesc) { - return; - } - - function composeTitle(fileDesc) { - var result = []; - var syncAttributesList = _.values(fileDesc.syncLocations); - var publishAttributesList = _.values(fileDesc.publishLocations); - var attributesList = syncAttributesList.concat(publishAttributesList); - _.chain(attributesList).sortBy(function(attributes) { - return attributes.provider.providerId; - }).each(function(attributes) { - result.push(''); - }); - result.push(" "); - result.push(fileDesc.title); - return result.join(""); - } - var title = fileDesc.title; - document.title = "StackEdit - " + title; - $("#file-title").html(composeTitle(fileDesc)); - $(".file-title").text(title); - $("#file-title-input").val(title); - - if(layout !== undefined) { - // Use defer to make sure UI has been updated - _.defer(layout.resizeAll); - } - }; - - documentTitle.onFileSelected = function(fileDescParameter) { - fileDesc = fileDescParameter; - updateTitle(fileDescParameter); - }; - - documentTitle.onTitleChanged = updateTitle; - documentTitle.onSyncExportSuccess = updateTitle; - documentTitle.onSyncRemoved = updateTitle; - documentTitle.onNewPublishSuccess = updateTitle; - documentTitle.onPublishRemoved = updateTitle; - - return documentTitle; - + var documentTitle = { + extensionId: "documentTitle", + extensionName: "Document title", + settingsBloc: '

    Responsible for showing the document title in the navigation bar.

    ' + }; + + var layout = undefined; + documentTitle.onLayoutCreated = function(layoutParameter) { + layout = layoutParameter; + }; + + var fileDesc = undefined; + var updateTitle = function(fileDescParameter) { + if(fileDescParameter !== fileDesc) { + return; + } + + function composeTitle(fileDesc) { + var result = []; + var syncAttributesList = _.values(fileDesc.syncLocations); + var publishAttributesList = _.values(fileDesc.publishLocations); + var attributesList = syncAttributesList.concat(publishAttributesList); + _.chain(attributesList).sortBy(function(attributes) { + return attributes.provider.providerId; + }).each(function(attributes) { + result.push(''); + }); + result.push(" "); + result.push(fileDesc.title); + return result.join(""); + } + + var title = fileDesc.title; + document.title = "StackEdit - " + title; + $("#file-title").html(composeTitle(fileDesc)); + $(".file-title").text(title); + $("#file-title-input").val(title); + + if(layout !== undefined) { + // Use defer to make sure UI has been updated + _.defer(layout.resizeAll); + } + }; + + documentTitle.onFileSelected = function(fileDescParameter) { + fileDesc = fileDescParameter; + updateTitle(fileDescParameter); + }; + + documentTitle.onTitleChanged = updateTitle; + documentTitle.onSyncExportSuccess = updateTitle; + documentTitle.onSyncRemoved = updateTitle; + documentTitle.onNewPublishSuccess = updateTitle; + documentTitle.onPublishRemoved = updateTitle; + + return documentTitle; + }); \ No newline at end of file diff --git a/js/extensions/email-converter.js b/js/extensions/email-converter.js index f4223425..ab516316 100644 --- a/js/extensions/email-converter.js +++ b/js/extensions/email-converter.js @@ -1,20 +1,19 @@ define(function() { - - var emailConverter = { - extensionId: "emailConverter", - extensionName: "Email Converter", + + var emailConverter = { + extensionId: "emailConverter", + extensionName: "Email Converter", optional: true, - settingsBloc: '

    Converts email adresses in the form <email@example.com> into a clickable links.

    ' - }; - - emailConverter.onEditorConfigure = function(editor) { - editor.getConverter().hooks.chain("postConversion", function(text) { - return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) { - return '
    ' + email + ''; - }); - }); - }; - - return emailConverter; + settingsBloc: '

    Converts email adresses in the form <email@example.com> into a clickable links.

    ' + }; + + emailConverter.onEditorConfigure = function(editor) { + editor.getConverter().hooks.chain("postConversion", function(text) { + return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) { + return '' + email + ''; + }); + }); + }; + + return emailConverter; }); - diff --git a/js/extensions/manage-publication.js b/js/extensions/manage-publication.js index fe68bca2..08dee00d 100644 --- a/js/extensions/manage-publication.js +++ b/js/extensions/manage-publication.js @@ -2,65 +2,67 @@ define([ "jquery", "underscore" ], function($, _) { - - var managePublication = { - extensionId: "managePublication", - extensionName: "Manage publication", - settingsBloc: '

    Populates the "Manage publication" dialog box.

    ' - }; - - var fileMgr = undefined; - managePublication.onFileMgrCreated = function(fileMgrParameter) { - fileMgr = fileMgrParameter; - }; - - var fileDesc = undefined; - var lineTemplate = [ + + var managePublication = { + extensionId: "managePublication", + extensionName: "Manage publication", + settingsBloc: '

    Populates the "Manage publication" dialog box.

    ' + }; + + var fileMgr = undefined; + managePublication.onFileMgrCreated = function(fileMgrParameter) { + fileMgr = fileMgrParameter; + }; + + var fileDesc = undefined; + var lineTemplate = [ '
    ', - '', - '', - '', - '', - '
    '].join(""); - var removeButtonTemplate = ''; - var refreshDialog = function(fileDescParameter) { - if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { - return; - } - - var publishAttributesList = _.values(fileDesc.publishLocations); - $(".msg-no-publish, .msg-publish-list").addClass("hide"); - var publishList = $("#manage-publish-list").empty(); - if (publishAttributesList.length > 0) { - $(".msg-publish-list").removeClass("hide"); - } else { - $(".msg-no-publish").removeClass("hide"); - } - _.each(publishAttributesList, function(publishAttributes) { - formattedAttributes = _.omit(publishAttributes, "provider", "publishIndex", "sharingLink"); - if(formattedAttributes.password) { - formattedAttributes.password = "********"; - } - var publishDesc = JSON.stringify(formattedAttributes).replace(/{|}|"/g, "").replace(/,/g, ", "); - var lineElement = $(_.template(lineTemplate, { - provider: publishAttributes.provider, - publishDesc: publishDesc - })); - lineElement.append($(removeButtonTemplate).click(function() { - fileMgr.removePublish(publishAttributes); - })); - publishList.append(lineElement); - }); - }; - - managePublication.onFileSelected = function(fileDescParameter) { - fileDesc = fileDescParameter; - refreshDialog(fileDescParameter); - }; - - managePublication.onNewPublishSuccess = refreshDialog; - managePublication.onPublishRemoved = refreshDialog; - - return managePublication; - + ' ', + ' ', + ' ', + ' ', + '
  • ' + ].join(""); + var removeButtonTemplate = ''; + var refreshDialog = function(fileDescParameter) { + if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { + return; + } + + var publishAttributesList = _.values(fileDesc.publishLocations); + $(".msg-no-publish, .msg-publish-list").addClass("hide"); + var publishList = $("#manage-publish-list").empty(); + if(publishAttributesList.length > 0) { + $(".msg-publish-list").removeClass("hide"); + } + else { + $(".msg-no-publish").removeClass("hide"); + } + _.each(publishAttributesList, function(publishAttributes) { + formattedAttributes = _.omit(publishAttributes, "provider", "publishIndex", "sharingLink"); + if(formattedAttributes.password) { + formattedAttributes.password = "********"; + } + var publishDesc = JSON.stringify(formattedAttributes).replace(/{|}|"/g, "").replace(/,/g, ", "); + var lineElement = $(_.template(lineTemplate, { + provider: publishAttributes.provider, + publishDesc: publishDesc + })); + lineElement.append($(removeButtonTemplate).click(function() { + fileMgr.removePublish(publishAttributes); + })); + publishList.append(lineElement); + }); + }; + + managePublication.onFileSelected = function(fileDescParameter) { + fileDesc = fileDescParameter; + refreshDialog(fileDescParameter); + }; + + managePublication.onNewPublishSuccess = refreshDialog; + managePublication.onPublishRemoved = refreshDialog; + + return managePublication; + }); \ No newline at end of file diff --git a/js/extensions/manage-synchronization.js b/js/extensions/manage-synchronization.js index 79e9bbba..84578673 100644 --- a/js/extensions/manage-synchronization.js +++ b/js/extensions/manage-synchronization.js @@ -2,61 +2,63 @@ define([ "jquery", "underscore" ], function($, _) { - - var manageSynchronization = { - extensionId: "manageSynchronization", - extensionName: "Manage synchronization", - settingsBloc: '

    Populates the "Manage synchronization" dialog box.

    ' - }; - - var fileMgr = undefined; - manageSynchronization.onFileMgrCreated = function(fileMgrParameter) { - fileMgr = fileMgrParameter; - }; - - var fileDesc = undefined; - var lineTemplate = [ + + var manageSynchronization = { + extensionId: "manageSynchronization", + extensionName: "Manage synchronization", + settingsBloc: '

    Populates the "Manage synchronization" dialog box.

    ' + }; + + var fileMgr = undefined; + manageSynchronization.onFileMgrCreated = function(fileMgrParameter) { + fileMgr = fileMgrParameter; + }; + + var fileDesc = undefined; + var lineTemplate = [ '
    ', - '', - '', - '', - '', - '
    '].join(""); - var removeButtonTemplate = ''; - var refreshDialog = function(fileDescParameter) { - if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { - return; - } - - var syncAttributesList = _.values(fileDesc.syncLocations); - $(".msg-no-sync, .msg-sync-list").addClass("hide"); - var syncList = $("#manage-sync-list").empty(); - if (syncAttributesList.length > 0) { - $(".msg-sync-list").removeClass("hide"); - } else { - $(".msg-no-sync").removeClass("hide"); - } - _.each(syncAttributesList, function(syncAttributes) { - var syncDesc = syncAttributes.id || syncAttributes.path; - var lineElement = $(_.template(lineTemplate, { - provider: syncAttributes.provider, - syncDesc: syncDesc - })); - lineElement.append($(removeButtonTemplate).click(function() { - fileMgr.removeSync(syncAttributes); - })); - syncList.append(lineElement); - }); - }; - - manageSynchronization.onFileSelected = function(fileDescParameter) { - fileDesc = fileDescParameter; - refreshDialog(fileDescParameter); - }; - - manageSynchronization.onSyncExportSuccess = refreshDialog; - manageSynchronization.onSyncRemoved = refreshDialog; - - return manageSynchronization; - + ' ', + ' ', + ' ', + ' ', + '' + ].join(""); + var removeButtonTemplate = ''; + var refreshDialog = function(fileDescParameter) { + if(fileDescParameter !== undefined && fileDescParameter !== fileDesc) { + return; + } + + var syncAttributesList = _.values(fileDesc.syncLocations); + $(".msg-no-sync, .msg-sync-list").addClass("hide"); + var syncList = $("#manage-sync-list").empty(); + if(syncAttributesList.length > 0) { + $(".msg-sync-list").removeClass("hide"); + } + else { + $(".msg-no-sync").removeClass("hide"); + } + _.each(syncAttributesList, function(syncAttributes) { + var syncDesc = syncAttributes.id || syncAttributes.path; + var lineElement = $(_.template(lineTemplate, { + provider: syncAttributes.provider, + syncDesc: syncDesc + })); + lineElement.append($(removeButtonTemplate).click(function() { + fileMgr.removeSync(syncAttributes); + })); + syncList.append(lineElement); + }); + }; + + manageSynchronization.onFileSelected = function(fileDescParameter) { + fileDesc = fileDescParameter; + refreshDialog(fileDescParameter); + }; + + manageSynchronization.onSyncExportSuccess = refreshDialog; + manageSynchronization.onSyncRemoved = refreshDialog; + + return manageSynchronization; + }); \ No newline at end of file diff --git a/js/extensions/markdown-extra.js b/js/extensions/markdown-extra.js index a567b480..e909d21e 100644 --- a/js/extensions/markdown-extra.js +++ b/js/extensions/markdown-extra.js @@ -2,44 +2,44 @@ define([ "utils", "lib/Markdown.Extra" ], function(utils) { - + var markdownExtra = { extensionId: "markdownExtra", extensionName: "Markdown Extra", optional: true, defaultConfig: { - prettify: true - }, + prettify: true + }, settingsBloc: [ - '

    Adds extra features to the original Markdown syntax.

    ', - '
    ', - '
    ', - '', - '
    ', - '', - '
    ', - '
    ', - '
    ' - ].join("") + '

    Adds extra features to the original Markdown syntax.

    ', + '
    ', + '
    ', + ' ', + '
    ', + ' ', + '
    ', + '
    ', + '
    ' + ].join("") }; - + markdownExtra.onLoadSettings = function() { - utils.setInputChecked("#input-markdownextra-prettify", markdownExtra.config.prettify); + utils.setInputChecked("#input-markdownextra-prettify", markdownExtra.config.prettify); }; - + markdownExtra.onSaveSettings = function(newConfig, event) { - newConfig.prettify = utils.getInputChecked("#input-markdownextra-prettify"); + newConfig.prettify = utils.getInputChecked("#input-markdownextra-prettify"); }; - + markdownExtra.onEditorConfigure = function(editor) { - var converter = editor.getConverter(); - var options = {}; - if(markdownExtra.config.prettify === true) { - options.highlighter = "prettify"; - editor.hooks.chain("onPreviewRefresh", prettyPrint); - } - Markdown.Extra.init(converter, options); - }; - + var converter = editor.getConverter(); + var options = {}; + if(markdownExtra.config.prettify === true) { + options.highlighter = "prettify"; + editor.hooks.chain("onPreviewRefresh", prettyPrint); + } + Markdown.Extra.init(converter, options); + }; + return markdownExtra; }); \ No newline at end of file diff --git a/js/extensions/notifications.js b/js/extensions/notifications.js index 5dedce51..ca2c16a5 100644 --- a/js/extensions/notifications.js +++ b/js/extensions/notifications.js @@ -4,117 +4,117 @@ define([ "utils", "jgrowl" ], function($, _, utils, jGrowl) { - - var notifications = { - extensionId: "notifications", - extensionName: "Notifications", - defaultConfig: { - timeout: 5000 - }, + + var notifications = { + extensionId: "notifications", + extensionName: "Notifications", + defaultConfig: { + timeout: 5000 + }, settingsBloc: [ - '

    Shows notification messages in the bottom-right corner of the screen.

    ', - '
    ', - '
    ', - '', - '
    ', - '', - 'ms', - '
    ', - '
    ', - '
    ' - ].join("") - }; - - notifications.onLoadSettings = function() { - utils.setInputValue("#input-notifications-timeout", notifications.config.timeout); + '

    Shows notification messages in the bottom-right corner of the screen.

    ', + '
    ', + '
    ', + ' ', + '
    ', + ' ', + ' ms', + '
    ', + '
    ', + '
    ' + ].join("") }; - + + notifications.onLoadSettings = function() { + utils.setInputValue("#input-notifications-timeout", notifications.config.timeout); + }; + notifications.onSaveSettings = function(newConfig, event) { - newConfig.timeout = utils.getInputIntValue("#input-notifications-timeout", event, 1, 60000); + newConfig.timeout = utils.getInputIntValue("#input-notifications-timeout", event, 1, 60000); }; - - notifications.onReady = function() { - // jGrowl configuration - jGrowl.defaults.life = notifications.config.timeout; - jGrowl.defaults.closer = false; - jGrowl.defaults.closeTemplate = ''; - jGrowl.defaults.position = 'bottom-right'; - }; - - function showMessage(msg, iconClass, options) { - if(!msg) { - return; - } - var endOfMsg = msg.indexOf("|"); - if(endOfMsg !== -1) { - msg = msg.substring(0, endOfMsg); - if(!msg) { - return; - } - } - options = options || {}; - iconClass = iconClass || "icon-info-sign"; - jGrowl(" " + _.escape(msg), options); - } - - notifications.onMessage = function(message) { - logger.log(message); - showMessage(message); - }; - - notifications.onError = function(error) { - logger.error(error); - if(_.isString(error)) { - showMessage(error, "icon-warning-sign"); - } - else if(_.isObject(error)) { - showMessage(error.message, "icon-warning-sign"); - } - }; - - notifications.onOfflineChanged = function(isOffline) { - if(isOffline === true) { - showMessage("You are offline.", "icon-exclamation-sign msg-offline", { - sticky : true, - close : function() { - showMessage("You are back online!", "icon-signal"); - } - }); - } else { - $(".msg-offline").parents(".jGrowl-notification").trigger( - 'jGrowl.beforeClose'); - } - }; - - notifications.onSyncImportSuccess = function(fileDescList, provider) { - if(!fileDescList) { - return; - } - var titles = _.map(fileDescList, function(fileDesc) { - return fileDesc.title; - }).join(", "); - showMessage(titles + ' imported successfully from ' + provider.providerName + '.'); - }; - - notifications.onSyncExportSuccess = function(fileDesc, syncAttributes) { - showMessage('"' + fileDesc.title + '" will now be synchronized on ' + syncAttributes.provider.providerName + '.'); - }; - - notifications.onSyncRemoved = function(fileDesc, syncAttributes) { - showMessage(syncAttributes.provider.providerName + " synchronized location has been removed."); - }; - - notifications.onPublishSuccess = function(fileDesc) { - showMessage('"' + fileDesc.title + '" successfully published.'); - }; - - notifications.onNewPublishSuccess = function(fileDesc, publishIndex, publishAttributes) { - showMessage('"' + fileDesc.title + '" is now published on ' + publishAttributes.provider.providerName + '.'); - }; - - notifications.onPublishRemoved = function(fileDesc, publishAttributes) { - showMessage(publishAttributes.provider.providerName + " publish location has been removed."); - }; - - return notifications; + + notifications.onReady = function() { + // jGrowl configuration + jGrowl.defaults.life = notifications.config.timeout; + jGrowl.defaults.closer = false; + jGrowl.defaults.closeTemplate = ''; + jGrowl.defaults.position = 'bottom-right'; + }; + + function showMessage(msg, iconClass, options) { + if(!msg) { + return; + } + var endOfMsg = msg.indexOf("|"); + if(endOfMsg !== -1) { + msg = msg.substring(0, endOfMsg); + if(!msg) { + return; + } + } + options = options || {}; + iconClass = iconClass || "icon-info-sign"; + jGrowl(" " + _.escape(msg), options); + } + + notifications.onMessage = function(message) { + logger.log(message); + showMessage(message); + }; + + notifications.onError = function(error) { + logger.error(error); + if(_.isString(error)) { + showMessage(error, "icon-warning-sign"); + } + else if(_.isObject(error)) { + showMessage(error.message, "icon-warning-sign"); + } + }; + + notifications.onOfflineChanged = function(isOffline) { + if(isOffline === true) { + showMessage("You are offline.", "icon-exclamation-sign msg-offline", { + sticky: true, + close: function() { + showMessage("You are back online!", "icon-signal"); + } + }); + } + else { + $(".msg-offline").parents(".jGrowl-notification").trigger('jGrowl.beforeClose'); + } + }; + + notifications.onSyncImportSuccess = function(fileDescList, provider) { + if(!fileDescList) { + return; + } + var titles = _.map(fileDescList, function(fileDesc) { + return fileDesc.title; + }).join(", "); + showMessage(titles + ' imported successfully from ' + provider.providerName + '.'); + }; + + notifications.onSyncExportSuccess = function(fileDesc, syncAttributes) { + showMessage('"' + fileDesc.title + '" will now be synchronized on ' + syncAttributes.provider.providerName + '.'); + }; + + notifications.onSyncRemoved = function(fileDesc, syncAttributes) { + showMessage(syncAttributes.provider.providerName + " synchronized location has been removed."); + }; + + notifications.onPublishSuccess = function(fileDesc) { + showMessage('"' + fileDesc.title + '" successfully published.'); + }; + + notifications.onNewPublishSuccess = function(fileDesc, publishIndex, publishAttributes) { + showMessage('"' + fileDesc.title + '" is now published on ' + publishAttributes.provider.providerName + '.'); + }; + + notifications.onPublishRemoved = function(fileDesc, publishAttributes) { + showMessage(publishAttributes.provider.providerName + " publish location has been removed."); + }; + + return notifications; }); \ No newline at end of file diff --git a/js/extensions/scroll-link.js b/js/extensions/scroll-link.js index 644c3975..09e62912 100644 --- a/js/extensions/scroll-link.js +++ b/js/extensions/scroll-link.js @@ -1,199 +1,202 @@ define([ "jquery", "underscore", - "lib/css_browser_selector" + "lib/css_browser_selector" ], function($, _) { - - var scrollLink = { - extensionId: "scrollLink", - extensionName: "Scroll Link", - optional: true, - settingsBloc: [ - '

    Binds together editor and preview scrollbars.

    ', - '
    NOTE: ', - 'The mapping between Markdown and HTML is based on the position of the title elements (h1, h2, ...) in the page. ', - 'Therefore, if your document does not contain any title, the mapping will be linear and consequently less accurate.', - '' - ].join("") - }; - - var mdSectionList = []; - var htmlSectionList = []; - function pxToFloat(px) { - return parseFloat(px.substring(0, px.length-2)); - } - var buildSections = _.debounce(function() { - - // Try to find Markdown sections by looking for titles - var editorElt = $("#wmd-input"); - mdSectionList = []; - // This textarea is used to measure sections height - var textareaElt = $("#md-section-helper"); - // It has to be the same width than wmd-input - textareaElt.width(editorElt.width()); - // Consider wmd-input top padding - var padding = pxToFloat(editorElt.css('padding-top')); - var offset = 0, mdSectionOffset = 0; - function addMdSection(sectionText) { - var sectionHeight = padding; - if(sectionText !== undefined) { - textareaElt.val(sectionText); - sectionHeight += textareaElt.prop('scrollHeight'); - } - var newSectionOffset = mdSectionOffset + sectionHeight; - mdSectionList.push({ - startOffset: mdSectionOffset, - endOffset: newSectionOffset, - height: sectionHeight - }); - mdSectionOffset = newSectionOffset; - padding = 0; - } - // Create MD sections by finding title patterns (excluding gfm blocs) - var text = editorElt.val() + "\n\n"; - text.replace(/^```.*\n[\s\S]*?\n```|(^.+[ \t]*\n=+[ \t]*\n+|^.+[ \t]*\n-+[ \t]*\n+|^\#{1,6}[ \t]*.+?[ \t]*\#*\n+)/gm, - function(match, title, matchOffset) { - if(title) { - // We just found a title which means end of the previous section - // Exclude last \n of the section - var sectionText = undefined; - if(matchOffset > offset) { - sectionText = text.substring(offset, matchOffset-1); - } - addMdSection(sectionText); - offset = matchOffset; - } - return ""; - } - ); - // Last section - // Consider wmd-input bottom padding and exclude \n\n previously added - padding += pxToFloat(editorElt.css('padding-bottom')); - addMdSection(text.substring(offset, text.length-2)); - - // Try to find corresponding sections in the preview - var previewElt = $("#wmd-preview"); - htmlSectionList = []; - var htmlSectionOffset = 0; - var previewScrollTop = previewElt.scrollTop(); - // Each title element is a section separator - previewElt.children("h1,h2,h3,h4,h5,h6").each(function() { - // Consider div scroll position and header element top margin - var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top')); - htmlSectionList.push({ - startOffset: htmlSectionOffset, - endOffset: newSectionOffset, - height: newSectionOffset - htmlSectionOffset - }); - htmlSectionOffset = newSectionOffset; - }); - // Last section - var scrollHeight = previewElt.prop('scrollHeight'); - htmlSectionList.push({ - startOffset: htmlSectionOffset, - endOffset: scrollHeight, - height: scrollHeight - htmlSectionOffset - }); - - // apply Scroll Link - lastEditorScrollTop = -9; - skipScrollLink = false; - isScrollPreview = false; - runScrollLink(); - }, 500); - - // -9 is less than -5 - var lastEditorScrollTop = -9; - var lastPreviewScrollTop = -9; - var skipScrollLink = false; - var isScrollPreview = false; - var runScrollLink = _.debounce(function() { - if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { - return; - } - var editorElt = $("#wmd-input"); - var editorScrollTop = editorElt.scrollTop(); - var previewElt = $("#wmd-preview"); - var previewScrollTop = previewElt.scrollTop(); - function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) { - // Find the section corresponding to the offset - var sectionIndex = undefined; - var srcSection = _.find(srcSectionList, function(section, index) { - sectionIndex = index; - return srcScrollTop < section.endOffset; - }); - if(srcSection === undefined) { - // Something wrong in the algorithm... - return -9; - } - var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height; - var destSection = destSectionList[sectionIndex]; - var destScrollTop = destSection.startOffset + destSection.height * posInSection; - destScrollTop = _.min([destScrollTop, destElt.prop('scrollHeight') - destElt.outerHeight()]); - if(Math.abs(destScrollTop - lastDestScrollTop) < 5) { - // Skip the animation in case it's not necessary - return; - } - destElt.animate({scrollTop: destScrollTop}, 600, function() { - callback(destScrollTop); - }); - } - // Perform the animation if diff > 5px - if(isScrollPreview === false && Math.abs(editorScrollTop - lastEditorScrollTop) > 5) { - // Animate the preview - lastEditorScrollTop = editorScrollTop; - animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) { - lastPreviewScrollTop = destScrollTop; - }); - } - else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) { - // Animate the editor - lastPreviewScrollTop = previewScrollTop; - animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) { - lastEditorScrollTop = destScrollTop; - }); - } - }, 600); - scrollLink.onLayoutConfigure = function(layoutConfig) { - layoutConfig.onresize = buildSections; - }; - - scrollLink.onLayoutCreated = function() { - $("#wmd-preview").scroll(function() { - isScrollPreview = true; - runScrollLink(); - }); - $("#wmd-input").scroll(function() { - isScrollPreview = false; - runScrollLink(); - }); - }; - - scrollLink.onEditorConfigure = function(editor) { - skipScrollLink = true; - lastPreviewScrollTop = 0; - editor.hooks.chain("onPreviewRefresh", function() { - skipScrollLink = true; - }); - }; - - scrollLink.onPreviewFinished = function() { - // MathJax may have change the scrolling position. Restore it. - if(lastPreviewScrollTop >= 0) { - $("#wmd-preview").scrollTop(lastPreviewScrollTop); - } - _.defer(function() { - // Modify scroll position of the preview not the editor - lastEditorScrollTop = -9; - buildSections(); - // Preview may change if images are loading - $("#wmd-preview img").load(function() { - lastEditorScrollTop = -9; - buildSections(); - }); - }); - }; - - return scrollLink; + var scrollLink = { + extensionId: "scrollLink", + extensionName: "Scroll Link", + optional: true, + settingsBloc: [ + '

    Binds together editor and preview scrollbars.

    ', + '
    NOTE: ', + ' The mapping between Markdown and HTML is based on the position of the title elements (h1, h2, ...) in the page. ', + ' Therefore, if your document does not contain any title, the mapping will be linear and consequently less accurate.', + '' + ].join("") + }; + + var mdSectionList = []; + var htmlSectionList = []; + function pxToFloat(px) { + return parseFloat(px.substring(0, px.length - 2)); + } + var buildSections = _.debounce(function() { + + // Try to find Markdown sections by looking for titles + var editorElt = $("#wmd-input"); + mdSectionList = []; + // This textarea is used to measure sections height + var textareaElt = $("#md-section-helper"); + // It has to be the same width than wmd-input + textareaElt.width(editorElt.width()); + // Consider wmd-input top padding + var padding = pxToFloat(editorElt.css('padding-top')); + var offset = 0, mdSectionOffset = 0; + function addMdSection(sectionText) { + var sectionHeight = padding; + if(sectionText !== undefined) { + textareaElt.val(sectionText); + sectionHeight += textareaElt.prop('scrollHeight'); + } + var newSectionOffset = mdSectionOffset + sectionHeight; + mdSectionList.push({ + startOffset: mdSectionOffset, + endOffset: newSectionOffset, + height: sectionHeight + }); + mdSectionOffset = newSectionOffset; + padding = 0; + } + // Create MD sections by finding title patterns (excluding gfm blocs) + var text = editorElt.val() + "\n\n"; + text.replace(/^```.*\n[\s\S]*?\n```|(^.+[ \t]*\n=+[ \t]*\n+|^.+[ \t]*\n-+[ \t]*\n+|^\#{1,6}[ \t]*.+?[ \t]*\#*\n+)/gm, function(match, title, matchOffset) { + if(title) { + // We just found a title which means end of the previous section + // Exclude last \n of the section + var sectionText = undefined; + if(matchOffset > offset) { + sectionText = text.substring(offset, matchOffset - 1); + } + addMdSection(sectionText); + offset = matchOffset; + } + return ""; + }); + // Last section + // Consider wmd-input bottom padding and exclude \n\n previously added + padding += pxToFloat(editorElt.css('padding-bottom')); + addMdSection(text.substring(offset, text.length - 2)); + + // Try to find corresponding sections in the preview + var previewElt = $("#wmd-preview"); + htmlSectionList = []; + var htmlSectionOffset = 0; + var previewScrollTop = previewElt.scrollTop(); + // Each title element is a section separator + previewElt.children("h1,h2,h3,h4,h5,h6").each(function() { + // Consider div scroll position and header element top margin + var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top')); + htmlSectionList.push({ + startOffset: htmlSectionOffset, + endOffset: newSectionOffset, + height: newSectionOffset - htmlSectionOffset + }); + htmlSectionOffset = newSectionOffset; + }); + // Last section + var scrollHeight = previewElt.prop('scrollHeight'); + htmlSectionList.push({ + startOffset: htmlSectionOffset, + endOffset: scrollHeight, + height: scrollHeight - htmlSectionOffset + }); + + // apply Scroll Link + lastEditorScrollTop = -9; + skipScrollLink = false; + isScrollPreview = false; + runScrollLink(); + }, 500); + + // -9 is less than -5 + var lastEditorScrollTop = -9; + var lastPreviewScrollTop = -9; + var skipScrollLink = false; + var isScrollPreview = false; + var runScrollLink = _.debounce(function() { + if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { + return; + } + var editorElt = $("#wmd-input"); + var editorScrollTop = editorElt.scrollTop(); + var previewElt = $("#wmd-preview"); + var previewScrollTop = previewElt.scrollTop(); + function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) { + // Find the section corresponding to the offset + var sectionIndex = undefined; + var srcSection = _.find(srcSectionList, function(section, index) { + sectionIndex = index; + return srcScrollTop < section.endOffset; + }); + if(srcSection === undefined) { + // Something wrong in the algorithm... + return -9; + } + var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height; + var destSection = destSectionList[sectionIndex]; + var destScrollTop = destSection.startOffset + destSection.height * posInSection; + destScrollTop = _.min([ + destScrollTop, + destElt.prop('scrollHeight') - destElt.outerHeight() + ]); + if(Math.abs(destScrollTop - lastDestScrollTop) < 5) { + // Skip the animation in case it's not necessary + return; + } + destElt.animate({ + scrollTop: destScrollTop + }, 600, function() { + callback(destScrollTop); + }); + } + // Perform the animation if diff > 5px + if(isScrollPreview === false && Math.abs(editorScrollTop - lastEditorScrollTop) > 5) { + // Animate the preview + lastEditorScrollTop = editorScrollTop; + animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) { + lastPreviewScrollTop = destScrollTop; + }); + } + else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) { + // Animate the editor + lastPreviewScrollTop = previewScrollTop; + animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) { + lastEditorScrollTop = destScrollTop; + }); + } + }, 600); + + scrollLink.onLayoutConfigure = function(layoutConfig) { + layoutConfig.onresize = buildSections; + }; + + scrollLink.onLayoutCreated = function() { + $("#wmd-preview").scroll(function() { + isScrollPreview = true; + runScrollLink(); + }); + $("#wmd-input").scroll(function() { + isScrollPreview = false; + runScrollLink(); + }); + }; + + scrollLink.onEditorConfigure = function(editor) { + skipScrollLink = true; + lastPreviewScrollTop = 0; + editor.hooks.chain("onPreviewRefresh", function() { + skipScrollLink = true; + }); + }; + + scrollLink.onPreviewFinished = function() { + // MathJax may have change the scrolling position. Restore it. + if(lastPreviewScrollTop >= 0) { + $("#wmd-preview").scrollTop(lastPreviewScrollTop); + } + _.defer(function() { + // Modify scroll position of the preview not the editor + lastEditorScrollTop = -9; + buildSections(); + // Preview may change if images are loading + $("#wmd-preview img").load(function() { + lastEditorScrollTop = -9; + buildSections(); + }); + }); + }; + + return scrollLink; }); \ No newline at end of file diff --git a/js/extensions/toc.js b/js/extensions/toc.js index dbac74a0..c4973e33 100644 --- a/js/extensions/toc.js +++ b/js/extensions/toc.js @@ -3,116 +3,106 @@ define([ "underscore", "utils" ], function($, _, utils) { - - var toc = { - extensionId: "toc", - extensionName: "Table of content", + + var toc = { + extensionId: "toc", + extensionName: "Table of content", optional: true, - settingsBloc: '

    Generates a table of content when a [TOC] marker is found.

    ' - }; - - // TOC element description - function TocElement(tagName, anchor, text) { - this.tagName = tagName; - this.anchor = anchor; - this.text = text; - this.children = []; - } - TocElement.prototype.childrenToString = function() { - if(this.children.length === 0) { - return ""; - } - var result = "
      "; - _.each(this.children, function(child) { - result += child.toString(); - }); - result += "
    "; - return result; - }; - TocElement.prototype.toString = function() { - var result = "
  • "; - if(this.anchor && this.text) { - result += '' + this.text + ''; - } - result += this.childrenToString() + "
  • "; - return result; - }; - - // Transform flat list of TocElement into a tree - function groupTags(array, level) { - level = level || 1; - var tagName = "H" + level; - var result = []; - - var currentElement = undefined; - function pushCurrentElement() { - if(currentElement !== undefined) { - if(currentElement.children.length > 0) { - currentElement.children = groupTags(currentElement.children, level + 1); - } - result.push(currentElement); - } - } - - _.each(array, function(element, index) { - if(element.tagName != tagName) { - if(currentElement === undefined) { - currentElement = new TocElement(); - } - currentElement.children.push(element); - } - else { - pushCurrentElement(); - currentElement = element; - } - }); - pushCurrentElement(); - return result; - } - - // Build the TOC - function buildToc() { - var anchorList = {}; - function createAnchor(element) { - var id = element.prop("id") || utils.slugify(element.text()); - var anchor = id; - var index = 0; - while(_.has(anchorList, anchor)) { - anchor = id + "-" + (++index); - } - anchorList[anchor] = true; - // Update the id of the element - element.prop("id", anchor); - return anchor; - } - - var elementList = []; - $("#wmd-preview > h1," + - "#wmd-preview > h2," + - "#wmd-preview > h3," + - "#wmd-preview > h4," + - "#wmd-preview > h5," + - "#wmd-preview > h6").each(function() { - elementList.push(new TocElement( - $(this).prop("tagName"), - createAnchor($(this)), - $(this).text() - )); - }); - elementList = groupTags(elementList); - return '
      ' + elementList.toString() + '
    '; - } - - toc.onEditorConfigure = function(editor) { - // Run TOC generation when conversion is finished directly on HTML - editor.hooks.chain("onPreviewRefresh", function() { - var toc = buildToc(); - var html = $("#wmd-preview").html(); - html = html.replace(/

    \[TOC\]<\/p>/g, toc); - $("#wmd-preview").html(html); - }); - }; - - return toc; + settingsBloc: '

    Generates a table of content when a [TOC] marker is found.

    ' + }; + + // TOC element description + function TocElement(tagName, anchor, text) { + this.tagName = tagName; + this.anchor = anchor; + this.text = text; + this.children = []; + } + TocElement.prototype.childrenToString = function() { + if(this.children.length === 0) { + return ""; + } + var result = "
      "; + _.each(this.children, function(child) { + result += child.toString(); + }); + result += "
    "; + return result; + }; + TocElement.prototype.toString = function() { + var result = "
  • "; + if(this.anchor && this.text) { + result += '' + this.text + ''; + } + result += this.childrenToString() + "
  • "; + return result; + }; + + // Transform flat list of TocElement into a tree + function groupTags(array, level) { + level = level || 1; + var tagName = "H" + level; + var result = []; + + var currentElement = undefined; + function pushCurrentElement() { + if(currentElement !== undefined) { + if(currentElement.children.length > 0) { + currentElement.children = groupTags(currentElement.children, level + 1); + } + result.push(currentElement); + } + } + + _.each(array, function(element, index) { + if(element.tagName != tagName) { + if(currentElement === undefined) { + currentElement = new TocElement(); + } + currentElement.children.push(element); + } + else { + pushCurrentElement(); + currentElement = element; + } + }); + pushCurrentElement(); + return result; + } + + // Build the TOC + function buildToc() { + var anchorList = {}; + function createAnchor(element) { + var id = element.prop("id") || utils.slugify(element.text()); + var anchor = id; + var index = 0; + while (_.has(anchorList, anchor)) { + anchor = id + "-" + (++index); + } + anchorList[anchor] = true; + // Update the id of the element + element.prop("id", anchor); + return anchor; + } + + var elementList = []; + $("#wmd-preview > h1," + "#wmd-preview > h2," + "#wmd-preview > h3," + "#wmd-preview > h4," + "#wmd-preview > h5," + "#wmd-preview > h6").each(function() { + elementList.push(new TocElement($(this).prop("tagName"), createAnchor($(this)), $(this).text())); + }); + elementList = groupTags(elementList); + return '
      ' + elementList.toString() + '
    '; + } + + toc.onEditorConfigure = function(editor) { + // Run TOC generation when conversion is finished directly on HTML + editor.hooks.chain("onPreviewRefresh", function() { + var toc = buildToc(); + var html = $("#wmd-preview").html(); + html = html.replace(/

    \[TOC\]<\/p>/g, toc); + $("#wmd-preview").html(html); + }); + }; + + return toc; }); - diff --git a/js/extensions/working-indicator.js b/js/extensions/working-indicator.js index 74b472e8..31acb065 100644 --- a/js/extensions/working-indicator.js +++ b/js/extensions/working-indicator.js @@ -1,24 +1,25 @@ define([ - "jquery", - "underscore" + "jquery", + "underscore" ], function($, _) { - - var workingIndicator = { - extensionId: "workingIndicator", - extensionName: "Working indicator", - settingsBloc: '

    Displays an animated image when a network operation is running.

    ' - }; - - workingIndicator.onAsyncRunning = function(isRunning) { - if (isRunning === false) { - $(".working-indicator").removeClass("show"); - $("body").removeClass("working"); - } else { - $(".working-indicator").addClass("show"); - $("body").addClass("working"); - } - }; - - return workingIndicator; - + + var workingIndicator = { + extensionId: "workingIndicator", + extensionName: "Working indicator", + settingsBloc: '

    Displays an animated image when a network operation is running.

    ' + }; + + workingIndicator.onAsyncRunning = function(isRunning) { + if(isRunning === false) { + $(".working-indicator").removeClass("show"); + $("body").removeClass("working"); + } + else { + $(".working-indicator").addClass("show"); + $("body").addClass("working"); + } + }; + + return workingIndicator; + }); \ No newline at end of file diff --git a/js/file-manager.js b/js/file-manager.js index 1714ca83..3f1a6216 100644 --- a/js/file-manager.js +++ b/js/file-manager.js @@ -8,347 +8,316 @@ define([ "file-system", "lib/text!../WELCOME.md" ], function($, _, core, utils, settings, extensionMgr, fileSystem, welcomeContent) { - - var fileMgr = {}; - - // Defines a file descriptor in the file system (fileDesc objects) - function FileDescriptor(fileIndex, title, syncLocations, publishLocations) { - this.fileIndex = fileIndex; - this.title = title; - this.syncLocations = syncLocations || {}; - this.publishLocations = publishLocations || {}; - } - FileDescriptor.prototype.getContent = function() { - return localStorage[this.fileIndex + ".content"]; - }; - FileDescriptor.prototype.setContent = function(content) { - localStorage[this.fileIndex + ".content"] = content; - extensionMgr.onContentChanged(this); - }; - FileDescriptor.prototype.setTitle = function(title) { - this.title = title; - localStorage[this.fileIndex + ".title"] = title; - extensionMgr.onTitleChanged(this); - }; - // Load file descriptors from localStorage - _.chain( - localStorage["file.list"].split(";") - ).compact().each(function(fileIndex) { - fileSystem[fileIndex] = new FileDescriptor(fileIndex, localStorage[fileIndex + ".title"]); - }); - - // Defines the current file - var currentFile = undefined; - fileMgr.getCurrentFile = function() { - return currentFile; - }; - fileMgr.isCurrentFile = function(fileDesc) { - return fileDesc === currentFile; - }; - fileMgr.setCurrentFile = function(fileDesc) { - currentFile = fileDesc; - if(fileDesc === undefined) { - localStorage.removeItem("file.current"); - } - else if(fileDesc.fileIndex != TEMPORARY_FILE_INDEX) { - localStorage["file.current"] = fileDesc.fileIndex; - } - }; - - // Caution: this function recreates the editor (reset undo operations) - fileMgr.selectFile = function(fileDesc) { - fileDesc = fileDesc || fileMgr.getCurrentFile(); - - if(fileDesc === undefined) { - var fileSystemSize = _.size(fileSystem); - // If fileSystem empty create one file - if (fileSystemSize === 0) { - fileDesc = fileMgr.createFile(WELCOME_DOCUMENT_TITLE, welcomeContent); - } - else { - var fileIndex = localStorage["file.current"]; - // If no file is selected take the last created - if(fileIndex === undefined) { - fileIndex = _.keys(fileSystem)[fileSystemSize - 1]; - } - fileDesc = fileSystem[fileIndex]; - } - } - - if(fileMgr.isCurrentFile(fileDesc) === false) { - fileMgr.setCurrentFile(fileDesc); - - // Notify extensions - extensionMgr.onFileSelected(fileDesc); + var fileMgr = {}; - // Hide the viewer pencil button - if(fileDesc.fileIndex == TEMPORARY_FILE_INDEX) { - $(".action-edit-document").removeClass("hide"); - } - else { - $(".action-edit-document").addClass("hide"); - } - } - - // Recreate the editor - $("#wmd-input").val(fileDesc.getContent()); - core.createEditor(function() { - // Callback to save content when textarea changes - fileMgr.saveFile(); - }); - }; - - fileMgr.createFile = function(title, content, syncLocations, isTemporary) { - content = content !== undefined ? content : settings.defaultContent; - if (!title) { - // Create a file title - title = DEFAULT_FILE_TITLE; - var indicator = 2; - while(_.some(fileSystem, function(fileDesc) { - return fileDesc.title == title; - })) { - title = DEFAULT_FILE_TITLE + indicator++; - } - } - - // Generate a unique fileIndex - var fileIndex = TEMPORARY_FILE_INDEX; - if(!isTemporary) { - do { - fileIndex = "file." + utils.randomString(); - } while(_.has(fileSystem, fileIndex)); - } - - // syncIndex associations - syncLocations = syncLocations || {}; - var sync = _.reduce(syncLocations, function(sync, syncAttributes, syncIndex) { - return sync + syncIndex + ";"; - }, ";"); - - localStorage[fileIndex + ".title"] = title; - localStorage[fileIndex + ".content"] = content; - localStorage[fileIndex + ".sync"] = sync; - localStorage[fileIndex + ".publish"] = ";"; - - // Create the file descriptor - var fileDesc = new FileDescriptor(fileIndex, title, syncLocations); - - // Add the index to the file list - if(!isTemporary) { - localStorage["file.list"] += fileIndex + ";"; - fileSystem[fileIndex] = fileDesc; - extensionMgr.onFileCreated(fileDesc); - } - return fileDesc; - }; + // Defines a file descriptor in the file system (fileDesc objects) + function FileDescriptor(fileIndex, title, syncLocations, publishLocations) { + this.fileIndex = fileIndex; + this._title = title; + this.__defineGetter__("title", function() { + return this._title; + }); + this.__defineSetter__("title", function(title) { + this._title = title; + localStorage[this.fileIndex + ".title"] = title; + extensionMgr.onTitleChanged(this); + }); + this.__defineGetter__("content", function() { + return localStorage[this.fileIndex + ".content"]; + }); + this.__defineSetter__("content", function(content) { + localStorage[this.fileIndex + ".content"] = content; + extensionMgr.onContentChanged(this); + }); + this.syncLocations = syncLocations || {}; + this.publishLocations = publishLocations || {}; + } - fileMgr.deleteFile = function(fileDesc) { - fileDesc = fileDesc || fileMgr.getCurrentFile(); - if(fileMgr.isCurrentFile(fileDesc) === true) { - // Unset the current fileDesc - fileMgr.setCurrentFile(); - // Refresh the editor with an other file - fileMgr.selectFile(); - } + // Load file descriptors from localStorage + _.chain(localStorage["file.list"].split(";")).compact().each(function(fileIndex) { + fileSystem[fileIndex] = new FileDescriptor(fileIndex, localStorage[fileIndex + ".title"]); + }); - // Remove synchronized locations - _.each(fileDesc.syncLocations, function(syncAttributes) { - fileMgr.removeSync(syncAttributes, true); - }); - - // Remove publish locations - _.each(fileDesc.publishLocations, function(publishAttributes) { - fileMgr.removePublish(publishAttributes, true); - }); + // Defines the current file + var currentFile = undefined; + fileMgr.getCurrentFile = function() { + return currentFile; + }; + fileMgr.isCurrentFile = function(fileDesc) { + return fileDesc === currentFile; + }; + fileMgr.setCurrentFile = function(fileDesc) { + currentFile = fileDesc; + if(fileDesc === undefined) { + localStorage.removeItem("file.current"); + } + else if(fileDesc.fileIndex != TEMPORARY_FILE_INDEX) { + localStorage["file.current"] = fileDesc.fileIndex; + } + }; - // Remove the index from the file list - var fileIndex = fileDesc.fileIndex; - localStorage["file.list"] = localStorage["file.list"].replace(";" - + fileIndex + ";", ";"); - - localStorage.removeItem(fileIndex + ".title"); - localStorage.removeItem(fileIndex + ".content"); - localStorage.removeItem(fileIndex + ".sync"); - localStorage.removeItem(fileIndex + ".publish"); - - fileSystem.removeItem(fileIndex); - extensionMgr.onFileDeleted(fileDesc); - }; + // Caution: this function recreates the editor (reset undo operations) + fileMgr.selectFile = function(fileDesc) { + fileDesc = fileDesc || fileMgr.getCurrentFile(); - // Save current file in localStorage - fileMgr.saveFile = function() { - var content = $("#wmd-input").val(); - var fileDesc = fileMgr.getCurrentFile(); - fileDesc.setContent(content); - }; + if(fileDesc === undefined) { + var fileSystemSize = _.size(fileSystem); + // If fileSystem empty create one file + if(fileSystemSize === 0) { + fileDesc = fileMgr.createFile(WELCOME_DOCUMENT_TITLE, welcomeContent); + } + else { + var fileIndex = localStorage["file.current"]; + // If no file is selected take the last created + if(fileIndex === undefined) { + fileIndex = _.keys(fileSystem)[fileSystemSize - 1]; + } + fileDesc = fileSystem[fileIndex]; + } + } - // Add a synchronized location to a file - fileMgr.addSync = function(fileDesc, syncAttributes) { - localStorage[fileDesc.fileIndex + ".sync"] += syncAttributes.syncIndex + ";"; - fileDesc.syncLocations[syncAttributes.syncIndex] = syncAttributes; - // addSync is only used for export, not for import - extensionMgr.onSyncExportSuccess(fileDesc, syncAttributes); - }; - - // Remove a synchronized location - fileMgr.removeSync = function(syncAttributes, skipExtensions) { - var fileDesc = fileMgr.getFileFromSyncIndex(syncAttributes.syncIndex); - if(fileDesc !== undefined) { - localStorage[fileDesc.fileIndex + ".sync"] = localStorage[fileDesc.fileIndex + ".sync"].replace(";" - + syncAttributes.syncIndex + ";", ";"); - } - // Remove sync attributes - localStorage.removeItem(syncAttributes.syncIndex); - fileDesc.syncLocations.removeItem(syncAttributes.syncIndex); - if(!skipExtensions) { - extensionMgr.onSyncRemoved(fileDesc, syncAttributes); - } - }; - - // Get the file descriptor associated to a syncIndex - fileMgr.getFileFromSyncIndex = function(syncIndex) { - return _.find(fileSystem, function(fileDesc) { - return _.has(fileDesc.syncLocations, syncIndex); - }); - }; - - // Get syncAttributes from syncIndex - fileMgr.getSyncAttributes = function(syncIndex) { - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - return fileDesc && fileDesc.syncLocations[syncIndex]; - }; - - // Returns true if provider has locations to synchronize - fileMgr.hasSync = function(provider) { - return _.some(fileSystem, function(fileDesc) { - return _.some(fileDesc.syncLocations, function(syncAttributes) { - return syncAttributes.provider === provider; - }); - }); - }; + if(fileMgr.isCurrentFile(fileDesc) === false) { + fileMgr.setCurrentFile(fileDesc); - // Add a publishIndex (publish location) to a file - fileMgr.addPublish = function(fileDesc, publishAttributes) { - localStorage[fileDesc.fileIndex + ".publish"] += publishAttributes.publishIndex + ";"; - fileDesc.publishLocations[publishAttributes.publishIndex] = publishAttributes; - extensionMgr.onNewPublishSuccess(fileDesc, publishAttributes); - }; - - // Remove a publishIndex (publish location) - fileMgr.removePublish = function(publishAttributes, skipExtensions) { - var fileDesc = fileMgr.getFileFromPublishIndex(publishAttributes.publishIndex); - if(fileDesc !== undefined) { - localStorage[fileDesc.fileIndex + ".publish"] = localStorage[fileDesc.fileIndex + ".publish"].replace(";" - + publishAttributes.publishIndex + ";", ";"); - } - // Remove publish attributes - localStorage.removeItem(publishAttributes.publishIndex); - fileDesc.publishLocations.removeItem(publishAttributes.publishIndex); - if(!skipExtensions) { - extensionMgr.onPublishRemoved(fileDesc, publishAttributes); - } - }; - - // Get the file descriptor associated to a publishIndex - fileMgr.getFileFromPublishIndex = function(publishIndex) { - return _.find(fileSystem, function(fileDesc) { - return _.has(fileDesc.publishLocations, publishIndex); - }); - }; - - // Filter for search input in file selector - function filterFileSelector(filter) { - var liList = $("#file-selector li:not(.stick)"); - liList.show(); - if(filter) { - var words = filter.toLowerCase().split(/\s+/); - liList.each(function() { - var fileTitle = $(this).text().toLowerCase(); - if(_.some(words, function(word) { - return fileTitle.indexOf(word) === -1; - })) { - $(this).hide(); - } - }); - } - } - - core.onReady(function() { - - fileMgr.selectFile(); + // Notify extensions + extensionMgr.onFileSelected(fileDesc); - $(".action-create-file").click(function() { - var fileDesc = fileMgr.createFile(); - fileMgr.selectFile(fileDesc); - var wmdInput = $("#wmd-input").focus().get(0); - if(wmdInput.setSelectionRange) { - wmdInput.setSelectionRange(0, 0); - } - $("#file-title").click(); - }); - $(".action-remove-file").click(function() { - fileMgr.deleteFile(); - }); - $("#file-title").click(function() { - if(viewerMode === true) { - return; - } - $(this).hide(); - var fileTitleInput = $("#file-title-input").show(); - _.defer(function() { - fileTitleInput.focus().get(0).select(); - }); - }); - function applyTitle(input) { - input.hide(); - $("#file-title").show(); - var title = $.trim(input.val()); - var fileDesc = fileMgr.getCurrentFile(); - if (title && title != fileDesc.title) { - fileDesc.setTitle(title); - } - input.val(fileDesc.title); - $("#wmd-input").focus(); - } - $("#file-title-input").blur(function() { - applyTitle($(this)); - }).keyup(function(e) { - if (e.keyCode == 13) { - applyTitle($(this)); - } - if (e.keyCode == 27) { - $(this).val(""); - applyTitle($(this)); - } - }); - $(".action-open-file").click(function() { - filterFileSelector(); - _.defer(function() { - $("#file-search").val("").focus(); - }); - }); - $("#file-search").keyup(function() { - filterFileSelector($(this).val()); - }).click(function(event) { - event.stopPropagation(); - }); - $(".action-open-stackedit").click(function() { - window.location.href = "."; - }); - $(".action-edit-document").click(function() { - var content = $("#wmd-input").val(); - var title = fileMgr.getCurrentFile().title; - var fileDesc = fileMgr.createFile(title, content); - fileMgr.selectFile(fileDesc); - window.location.href = "."; - }); - $(".action-welcome-file").click(function() { - var fileDesc = fileMgr.createFile(WELCOME_DOCUMENT_TITLE, welcomeContent); - fileMgr.selectFile(fileDesc); - }); - }); + // Hide the viewer pencil button + if(fileDesc.fileIndex == TEMPORARY_FILE_INDEX) { + $(".action-edit-document").removeClass("hide"); + } + else { + $(".action-edit-document").addClass("hide"); + } + } - extensionMgr.onFileMgrCreated(fileMgr); - return fileMgr; + // Recreate the editor + $("#wmd-input").val(fileDesc.content); + core.createEditor(function() { + // Callback to save content when textarea changes + fileMgr.saveFile(); + }); + }; + + fileMgr.createFile = function(title, content, syncLocations, isTemporary) { + content = content !== undefined ? content : settings.defaultContent; + if(!title) { + // Create a file title + title = DEFAULT_FILE_TITLE; + var indicator = 2; + while (_.some(fileSystem, function(fileDesc) { + return fileDesc.title == title; + })) { + title = DEFAULT_FILE_TITLE + indicator++; + } + } + + // Generate a unique fileIndex + var fileIndex = TEMPORARY_FILE_INDEX; + if(!isTemporary) { + do { + fileIndex = "file." + utils.randomString(); + } while (_.has(fileSystem, fileIndex)); + } + + // syncIndex associations + syncLocations = syncLocations || {}; + var sync = _.reduce(syncLocations, function(sync, syncAttributes, syncIndex) { + return sync + syncIndex + ";"; + }, ";"); + + localStorage[fileIndex + ".title"] = title; + localStorage[fileIndex + ".content"] = content; + localStorage[fileIndex + ".sync"] = sync; + localStorage[fileIndex + ".publish"] = ";"; + + // Create the file descriptor + var fileDesc = new FileDescriptor(fileIndex, title, syncLocations); + + // Add the index to the file list + if(!isTemporary) { + localStorage["file.list"] += fileIndex + ";"; + fileSystem[fileIndex] = fileDesc; + extensionMgr.onFileCreated(fileDesc); + } + return fileDesc; + }; + + fileMgr.deleteFile = function(fileDesc) { + fileDesc = fileDesc || fileMgr.getCurrentFile(); + if(fileMgr.isCurrentFile(fileDesc) === true) { + // Unset the current fileDesc + fileMgr.setCurrentFile(); + // Refresh the editor with an other file + fileMgr.selectFile(); + } + + // Remove synchronized locations + _.each(fileDesc.syncLocations, function(syncAttributes) { + fileMgr.removeSync(syncAttributes, true); + }); + + // Remove publish locations + _.each(fileDesc.publishLocations, function(publishAttributes) { + fileMgr.removePublish(publishAttributes, true); + }); + + // Remove the index from the file list + var fileIndex = fileDesc.fileIndex; + localStorage["file.list"] = localStorage["file.list"].replace(";" + fileIndex + ";", ";"); + + localStorage.removeItem(fileIndex + ".title"); + localStorage.removeItem(fileIndex + ".content"); + localStorage.removeItem(fileIndex + ".sync"); + localStorage.removeItem(fileIndex + ".publish"); + + fileSystem.removeItem(fileIndex); + extensionMgr.onFileDeleted(fileDesc); + }; + + // Save current file in localStorage + fileMgr.saveFile = function() { + var fileDesc = fileMgr.getCurrentFile(); + fileDesc.content = $("#wmd-input").val(); + }; + + // Add a synchronized location to a file + fileMgr.addSync = function(fileDesc, syncAttributes) { + localStorage[fileDesc.fileIndex + ".sync"] += syncAttributes.syncIndex + ";"; + fileDesc.syncLocations[syncAttributes.syncIndex] = syncAttributes; + // addSync is only used for export, not for import + extensionMgr.onSyncExportSuccess(fileDesc, syncAttributes); + }; + + // Remove a synchronized location + fileMgr.removeSync = function(syncAttributes, skipExtensions) { + var fileDesc = fileMgr.getFileFromSyncIndex(syncAttributes.syncIndex); + if(fileDesc !== undefined) { + localStorage[fileDesc.fileIndex + ".sync"] = localStorage[fileDesc.fileIndex + ".sync"].replace(";" + syncAttributes.syncIndex + ";", ";"); + } + // Remove sync attributes + localStorage.removeItem(syncAttributes.syncIndex); + fileDesc.syncLocations.removeItem(syncAttributes.syncIndex); + if(!skipExtensions) { + extensionMgr.onSyncRemoved(fileDesc, syncAttributes); + } + }; + + // Get the file descriptor associated to a syncIndex + fileMgr.getFileFromSyncIndex = function(syncIndex) { + return _.find(fileSystem, function(fileDesc) { + return _.has(fileDesc.syncLocations, syncIndex); + }); + }; + + // Get syncAttributes from syncIndex + fileMgr.getSyncAttributes = function(syncIndex) { + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + return fileDesc && fileDesc.syncLocations[syncIndex]; + }; + + // Returns true if provider has locations to synchronize + fileMgr.hasSync = function(provider) { + return _.some(fileSystem, function(fileDesc) { + return _.some(fileDesc.syncLocations, function(syncAttributes) { + return syncAttributes.provider === provider; + }); + }); + }; + + // Add a publishIndex (publish location) to a file + fileMgr.addPublish = function(fileDesc, publishAttributes) { + localStorage[fileDesc.fileIndex + ".publish"] += publishAttributes.publishIndex + ";"; + fileDesc.publishLocations[publishAttributes.publishIndex] = publishAttributes; + extensionMgr.onNewPublishSuccess(fileDesc, publishAttributes); + }; + + // Remove a publishIndex (publish location) + fileMgr.removePublish = function(publishAttributes, skipExtensions) { + var fileDesc = fileMgr.getFileFromPublishIndex(publishAttributes.publishIndex); + if(fileDesc !== undefined) { + localStorage[fileDesc.fileIndex + ".publish"] = localStorage[fileDesc.fileIndex + ".publish"].replace(";" + publishAttributes.publishIndex + ";", ";"); + } + // Remove publish attributes + localStorage.removeItem(publishAttributes.publishIndex); + fileDesc.publishLocations.removeItem(publishAttributes.publishIndex); + if(!skipExtensions) { + extensionMgr.onPublishRemoved(fileDesc, publishAttributes); + } + }; + + // Get the file descriptor associated to a publishIndex + fileMgr.getFileFromPublishIndex = function(publishIndex) { + return _.find(fileSystem, function(fileDesc) { + return _.has(fileDesc.publishLocations, publishIndex); + }); + }; + + core.onReady(function() { + + fileMgr.selectFile(); + + $(".action-create-file").click(function() { + var fileDesc = fileMgr.createFile(); + fileMgr.selectFile(fileDesc); + var wmdInput = $("#wmd-input").focus().get(0); + if(wmdInput.setSelectionRange) { + wmdInput.setSelectionRange(0, 0); + } + $("#file-title").click(); + }); + $(".action-remove-file").click(function() { + fileMgr.deleteFile(); + }); + $("#file-title").click(function() { + if(viewerMode === true) { + return; + } + $(this).hide(); + var fileTitleInput = $("#file-title-input").show(); + _.defer(function() { + fileTitleInput.focus().get(0).select(); + }); + }); + function applyTitle(input) { + input.hide(); + $("#file-title").show(); + var title = $.trim(input.val()); + var fileDesc = fileMgr.getCurrentFile(); + if(title && title != fileDesc.title) { + fileDesc.title = title; + } + input.val(fileDesc.title); + $("#wmd-input").focus(); + } + $("#file-title-input").blur(function() { + applyTitle($(this)); + }).keyup(function(e) { + if(e.keyCode == 13) { + applyTitle($(this)); + } + if(e.keyCode == 27) { + $(this).val(""); + applyTitle($(this)); + } + }); + $(".action-open-stackedit").click(function() { + window.location.href = "."; + }); + $(".action-edit-document").click(function() { + var content = $("#wmd-input").val(); + var title = fileMgr.getCurrentFile().title; + var fileDesc = fileMgr.createFile(title, content); + fileMgr.selectFile(fileDesc); + window.location.href = "."; + }); + $(".action-welcome-file").click(function() { + var fileDesc = fileMgr.createFile(WELCOME_DOCUMENT_TITLE, welcomeContent); + fileMgr.selectFile(fileDesc); + }); + }); + + extensionMgr.onFileMgrCreated(fileMgr); + return fileMgr; }); diff --git a/js/gdrive-provider.js b/js/gdrive-provider.js index a80da023..c8eeb275 100644 --- a/js/gdrive-provider.js +++ b/js/gdrive-provider.js @@ -6,280 +6,275 @@ define([ "file-manager", "google-helper" ], function(_, core, utils, extensionMgr, fileMgr, googleHelper) { - - var PROVIDER_GDRIVE = "gdrive"; - - var gdriveProvider = { - providerId: PROVIDER_GDRIVE, - providerName: "Google Drive", - defaultPublishFormat: "template", - exportPreferencesInputIds: ["gdrive-parentid"] - }; - - function createSyncIndex(id) { - return "sync." + PROVIDER_GDRIVE + "." + id; - } - - function createSyncAttributes(id, etag, content, title) { - var syncAttributes = {}; - syncAttributes.provider = gdriveProvider; - syncAttributes.id = id; - syncAttributes.etag = etag; - syncAttributes.contentCRC = utils.crc32(content); - syncAttributes.titleCRC = utils.crc32(title); - syncAttributes.syncIndex = createSyncIndex(id); - utils.storeAttributes(syncAttributes); - return syncAttributes; - } - - function importFilesFromIds(ids) { - googleHelper.downloadMetadata(ids, function(error, result) { - if(error) { - return; - } - googleHelper.downloadContent(result, function(error, result) { - if(error) { - return; - } - var fileDescList = []; - _.each(result, function(file) { - var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); - var syncLocations = {}; - syncLocations[syncAttributes.syncIndex] = syncAttributes; - var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations); - fileMgr.selectFile(fileDesc); - fileDescList.push(fileDesc); - }); - extensionMgr.onSyncImportSuccess(fileDescList, gdriveProvider); - }); - }); - }; - gdriveProvider.importFiles = function() { - googleHelper.picker(function(error, ids) { - if(error || ids.length === 0) { - return; - } - var importIds = []; - _.each(ids, function(id) { - var syncIndex = createSyncIndex(id); - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - if(fileDesc !== undefined) { - extensionMgr.onError('"' + fileDesc.title + '" was already imported'); - return; - } - importIds.push(id); - }); - importFilesFromIds(importIds); - }); - }; - - gdriveProvider.exportFile = function(event, title, content, callback) { - var parentId = utils.getInputTextValue("#input-sync-export-gdrive-parentid"); - googleHelper.upload(undefined, parentId, title, content, undefined, function(error, result) { - if (error) { - callback(error); - return; - } - var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); - callback(undefined, syncAttributes); - }); - }; + var PROVIDER_GDRIVE = "gdrive"; - gdriveProvider.exportManual = function(event, title, content, callback) { - var id = utils.getInputTextValue("#input-sync-manual-gdrive-id", event); - if(!id) { - return; - } - // Check that file is not synchronized with another an existing document - var syncIndex = createSyncIndex(id); - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - if(fileDesc !== undefined) { - extensionMgr.onError('File ID is already synchronized with "' + fileDesc.title + '"'); - callback(true); - return; - } - googleHelper.upload(id, undefined, title, content, undefined, function(error, result) { - if (error) { - callback(error); - return; - } - var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); - callback(undefined, syncAttributes); - }); - }; - - gdriveProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) { - var syncContentCRC = syncAttributes.contentCRC; - var syncTitleCRC = syncAttributes.titleCRC; - // Skip if CRC has not changed - if(uploadContentCRC == syncContentCRC && uploadTitleCRC == syncTitleCRC) { - callback(undefined, false); - return; - } - googleHelper.upload(syncAttributes.id, undefined, uploadTitle, uploadContent, syncAttributes.etag, function(error, result) { - if(error) { - callback(error, true); - return; - } - syncAttributes.etag = result.etag; - syncAttributes.contentCRC = uploadContentCRC; - syncAttributes.titleCRC = uploadTitleCRC; - callback(undefined, true); - }); - }; - - gdriveProvider.syncDown = function(callback) { - var lastChangeId = parseInt(localStorage[PROVIDER_GDRIVE + ".lastChangeId"]); - googleHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) { - if (error) { - callback(error); - return; - } - var interestingChanges = []; - _.each(changes, function(change) { - var syncIndex = createSyncIndex(change.fileId); - var syncAttributes = fileMgr.getSyncAttributes(syncIndex); - if(syncAttributes === undefined) { - return; - } - // Store syncAttributes to avoid 2 times searching - change.syncAttributes = syncAttributes; - // Delete - if(change.deleted === true) { - interestingChanges.push(change); - return; - } - // Modify - if(syncAttributes.etag != change.file.etag) { - interestingChanges.push(change); - } - }); - googleHelper.downloadContent(interestingChanges, function(error, changes) { - if (error) { - callback(error); - return; - } - _.each(changes, function(change) { - var syncAttributes = change.syncAttributes; - var syncIndex = syncAttributes.syncIndex; - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - // No file corresponding (file may have been deleted locally) - if(fileDesc === undefined) { - return; - } - var localTitle = fileDesc.title; - // File deleted - if (change.deleted === true) { - extensionMgr.onError('"' + localTitle + '" has been removed from Google Drive.'); - fileMgr.removeSync(syncAttributes); - return; - } - var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle); - var localContent = fileDesc.getContent(); - var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent); - var file = change.file; + var gdriveProvider = { + providerId: PROVIDER_GDRIVE, + providerName: "Google Drive", + defaultPublishFormat: "template", + exportPreferencesInputIds: [ + "gdrive-parentid" + ] + }; + + function createSyncIndex(id) { + return "sync." + PROVIDER_GDRIVE + "." + id; + } + + function createSyncAttributes(id, etag, content, title) { + var syncAttributes = {}; + syncAttributes.provider = gdriveProvider; + syncAttributes.id = id; + syncAttributes.etag = etag; + syncAttributes.contentCRC = utils.crc32(content); + syncAttributes.titleCRC = utils.crc32(title); + syncAttributes.syncIndex = createSyncIndex(id); + utils.storeAttributes(syncAttributes); + return syncAttributes; + } + + function importFilesFromIds(ids) { + googleHelper.downloadMetadata(ids, function(error, result) { + if(error) { + return; + } + googleHelper.downloadContent(result, function(error, result) { + if(error) { + return; + } + var fileDescList = []; + _.each(result, function(file) { + var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); + var syncLocations = {}; + syncLocations[syncAttributes.syncIndex] = syncAttributes; + var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations); + fileMgr.selectFile(fileDesc); + fileDescList.push(fileDesc); + }); + extensionMgr.onSyncImportSuccess(fileDescList, gdriveProvider); + }); + }); + } + ; + + gdriveProvider.importFiles = function() { + googleHelper.picker(function(error, ids) { + if(error || ids.length === 0) { + return; + } + var importIds = []; + _.each(ids, function(id) { + var syncIndex = createSyncIndex(id); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + extensionMgr.onError('"' + fileDesc.title + '" was already imported'); + return; + } + importIds.push(id); + }); + importFilesFromIds(importIds); + }); + }; + + gdriveProvider.exportFile = function(event, title, content, callback) { + var parentId = utils.getInputTextValue("#input-sync-export-gdrive-parentid"); + googleHelper.upload(undefined, parentId, title, content, undefined, function(error, result) { + if(error) { + callback(error); + return; + } + var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); + callback(undefined, syncAttributes); + }); + }; + + gdriveProvider.exportManual = function(event, title, content, callback) { + var id = utils.getInputTextValue("#input-sync-manual-gdrive-id", event); + if(!id) { + return; + } + // Check that file is not synchronized with another an existing document + var syncIndex = createSyncIndex(id); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + extensionMgr.onError('File ID is already synchronized with "' + fileDesc.title + '"'); + callback(true); + return; + } + googleHelper.upload(id, undefined, title, content, undefined, function(error, result) { + if(error) { + callback(error); + return; + } + var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); + callback(undefined, syncAttributes); + }); + }; + + gdriveProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) { + var syncContentCRC = syncAttributes.contentCRC; + var syncTitleCRC = syncAttributes.titleCRC; + // Skip if CRC has not changed + if(uploadContentCRC == syncContentCRC && uploadTitleCRC == syncTitleCRC) { + callback(undefined, false); + return; + } + googleHelper.upload(syncAttributes.id, undefined, uploadTitle, uploadContent, syncAttributes.etag, function(error, result) { + if(error) { + callback(error, true); + return; + } + syncAttributes.etag = result.etag; + syncAttributes.contentCRC = uploadContentCRC; + syncAttributes.titleCRC = uploadTitleCRC; + callback(undefined, true); + }); + }; + + gdriveProvider.syncDown = function(callback) { + var lastChangeId = parseInt(localStorage[PROVIDER_GDRIVE + ".lastChangeId"]); + googleHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) { + if(error) { + callback(error); + return; + } + var interestingChanges = []; + _.each(changes, function(change) { + var syncIndex = createSyncIndex(change.fileId); + var syncAttributes = fileMgr.getSyncAttributes(syncIndex); + if(syncAttributes === undefined) { + return; + } + // Store syncAttributes to avoid 2 times searching + change.syncAttributes = syncAttributes; + // Delete + if(change.deleted === true) { + interestingChanges.push(change); + return; + } + // Modify + if(syncAttributes.etag != change.file.etag) { + interestingChanges.push(change); + } + }); + googleHelper.downloadContent(interestingChanges, function(error, changes) { + if(error) { + callback(error); + return; + } + _.each(changes, function(change) { + var syncAttributes = change.syncAttributes; + var syncIndex = syncAttributes.syncIndex; + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + // No file corresponding (file may have been deleted + // locally) + if(fileDesc === undefined) { + return; + } + var localTitle = fileDesc.title; + // File deleted + if(change.deleted === true) { + extensionMgr.onError('"' + localTitle + '" has been removed from Google Drive.'); + fileMgr.removeSync(syncAttributes); + return; + } + var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle); + var localContent = fileDesc.content; + var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent); + var file = change.file; var remoteTitleCRC = utils.crc32(file.title); var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC; var fileTitleChanged = localTitle != file.title; var remoteContentCRC = utils.crc32(file.content); var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; - var fileContentChanged = localContent != file.content; - // Conflict detection - if ((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) - || (fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) { - fileMgr.createFile(localTitle + " (backup)", localContent); - extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); - } - // If file title changed - if(fileTitleChanged && remoteTitleChanged === true) { - fileDesc.setTitle(file.title); - extensionMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.'); - } - // If file content changed - if(fileContentChanged && remoteContentChanged === true) { - fileDesc.setContent(file.content); - extensionMgr.onMessage('"' + file.title + '" has been updated from Google Drive.'); - if(fileMgr.isCurrentFile(fileDesc)) { - fileMgr.selectFile(); // Refresh editor - } - } - // Update syncAttributes - syncAttributes.etag = file.etag; - syncAttributes.contentCRC = remoteContentCRC; - syncAttributes.titleCRC = remoteTitleCRC; - utils.storeAttributes(syncAttributes); - }); - localStorage[PROVIDER_GDRIVE + ".lastChangeId"] = newChangeId; - callback(); - }); - }); - }; - - gdriveProvider.publish = function(publishAttributes, title, content, callback) { - googleHelper.upload( - publishAttributes.id, - undefined, - publishAttributes.fileName || title, - content, - undefined, - function(error, result) { - if(error) { - callback(error); - return; - } - publishAttributes.id = result.id; - callback(); - } - ); - }; + var fileContentChanged = localContent != file.content; + // Conflict detection + if((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) || (fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) { + fileMgr.createFile(localTitle + " (backup)", localContent); + extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); + } + // If file title changed + if(fileTitleChanged && remoteTitleChanged === true) { + fileDesc.title = file.title; + extensionMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.'); + } + // If file content changed + if(fileContentChanged && remoteContentChanged === true) { + fileDesc.content = file.content; + extensionMgr.onMessage('"' + file.title + '" has been updated from Google Drive.'); + if(fileMgr.isCurrentFile(fileDesc)) { + fileMgr.selectFile(); // Refresh editor + } + } + // Update syncAttributes + syncAttributes.etag = file.etag; + syncAttributes.contentCRC = remoteContentCRC; + syncAttributes.titleCRC = remoteTitleCRC; + utils.storeAttributes(syncAttributes); + }); + localStorage[PROVIDER_GDRIVE + ".lastChangeId"] = newChangeId; + callback(); + }); + }); + }; - gdriveProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.id = utils.getInputTextValue("#input-publish-gdrive-fileid"); - publishAttributes.fileName = utils.getInputTextValue("#input-publish-gdrive-filename"); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; - - core.onReady(function() { - var state = localStorage[PROVIDER_GDRIVE + ".state"]; - if(state === undefined) { - return; - } - localStorage.removeItem(PROVIDER_GDRIVE + ".state"); - state = JSON.parse(state); - if (state.action == "create") { - googleHelper.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE, - "", undefined, function(error, file) { - if(error) { - return; - } - var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); - var syncLocations = {}; - syncLocations[syncAttributes.syncIndex] = syncAttributes; - var fileDesc = fileMgr.createFile(file.title, file.content, syncAttributes); - fileMgr.selectFile(fileDesc); - extensionMgr.onMessage('"' + file.title + '" created successfully on Google Drive.'); - }); - } - else if (state.action == "open") { - var importIds = []; - _.each(state.ids, function(id) { - var syncIndex = createSyncIndex(id); - var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); - if(fileDesc !== undefined) { - fileMgr.selectFile(fileDesc); - } - else { - importIds.push(id); - } - }); - importFilesFromIds(importIds); - } - }); + gdriveProvider.publish = function(publishAttributes, title, content, callback) { + googleHelper.upload(publishAttributes.id, undefined, publishAttributes.fileName || title, content, undefined, function(error, result) { + if(error) { + callback(error); + return; + } + publishAttributes.id = result.id; + callback(); + }); + }; - return gdriveProvider; + gdriveProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.id = utils.getInputTextValue("#input-publish-gdrive-fileid"); + publishAttributes.fileName = utils.getInputTextValue("#input-publish-gdrive-filename"); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + core.onReady(function() { + var state = localStorage[PROVIDER_GDRIVE + ".state"]; + if(state === undefined) { + return; + } + localStorage.removeItem(PROVIDER_GDRIVE + ".state"); + state = JSON.parse(state); + if(state.action == "create") { + googleHelper.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE, "", undefined, function(error, file) { + if(error) { + return; + } + var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); + var syncLocations = {}; + syncLocations[syncAttributes.syncIndex] = syncAttributes; + var fileDesc = fileMgr.createFile(file.title, file.content, syncAttributes); + fileMgr.selectFile(fileDesc); + extensionMgr.onMessage('"' + file.title + '" created successfully on Google Drive.'); + }); + } + else if(state.action == "open") { + var importIds = []; + _.each(state.ids, function(id) { + var syncIndex = createSyncIndex(id); + var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); + if(fileDesc !== undefined) { + fileMgr.selectFile(fileDesc); + } + else { + importIds.push(id); + } + }); + importFilesFromIds(importIds); + } + }); + + return gdriveProvider; }); \ No newline at end of file diff --git a/js/gist-provider.js b/js/gist-provider.js index 6355d460..9e19a7a7 100644 --- a/js/gist-provider.js +++ b/js/gist-provider.js @@ -2,42 +2,43 @@ define([ "utils", "github-helper" ], function(utils, githubHelper) { - - var PROVIDER_GIST = "gist"; - - var gistProvider = { - providerId: PROVIDER_GIST, - providerName: "Gist", - sharingAttributes: ["gistId", "filename"] - }; - - gistProvider.publish = function(publishAttributes, title, content, callback) { - githubHelper.uploadGist(publishAttributes.gistId, publishAttributes.filename, publishAttributes.isPublic, - title, content, function(error, gistId) { - if(error) { - callback(error); - return; - } - publishAttributes.gistId = gistId; - callback(); - } - ); - }; - gistProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.gistId = utils.getInputTextValue("#input-publish-gist-id"); - publishAttributes.filename = utils.getInputTextValue("#input-publish-filename", event); - publishAttributes.isPublic = utils.getInputChecked("#input-publish-gist-public"); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; - - gistProvider.importPublic = function(importParameters, callback) { - githubHelper.downloadGist(importParameters.gistId, importParameters.filename, callback); - }; + var PROVIDER_GIST = "gist"; - return gistProvider; + var gistProvider = { + providerId: PROVIDER_GIST, + providerName: "Gist", + sharingAttributes: [ + "gistId", + "filename" + ] + }; + + gistProvider.publish = function(publishAttributes, title, content, callback) { + githubHelper.uploadGist(publishAttributes.gistId, publishAttributes.filename, publishAttributes.isPublic, title, content, function(error, gistId) { + if(error) { + callback(error); + return; + } + publishAttributes.gistId = gistId; + callback(); + }); + }; + + gistProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.gistId = utils.getInputTextValue("#input-publish-gist-id"); + publishAttributes.filename = utils.getInputTextValue("#input-publish-filename", event); + publishAttributes.isPublic = utils.getInputChecked("#input-publish-gist-public"); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + gistProvider.importPublic = function(importParameters, callback) { + githubHelper.downloadGist(importParameters.gistId, importParameters.filename, callback); + }; + + return gistProvider; }); \ No newline at end of file diff --git a/js/github-helper.js b/js/github-helper.js index ec49c7d0..642e9c89 100644 --- a/js/github-helper.js +++ b/js/github-helper.js @@ -6,247 +6,249 @@ define([ "async-runner" ], function($, core, utils, extensionMgr, asyncRunner) { - var connected = undefined; - var github = undefined; + var connected = undefined; + var github = undefined; - var githubHelper = {}; + var githubHelper = {}; - // Try to connect github by downloading js file - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - connected = false; - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - if (connected === true) { - task.chain(); - return; - } - $.ajax({ - url : "lib/github.js", - dataType : "script", timeout : AJAX_TIMEOUT - }).done(function() { - connected = true; - task.chain(); - }).fail(function(jqXHR) { - var error = { - error: jqXHR.status, - message: jqXHR.statusText - }; - handleError(error, task); - }); - }); - } + // Try to connect github by downloading js file + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + connected = false; + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + if(connected === true) { + task.chain(); + return; + } + $.ajax({ + url: "lib/github.js", + dataType: "script", + timeout: AJAX_TIMEOUT + }).done(function() { + connected = true; + task.chain(); + }).fail(function(jqXHR) { + var error = { + error: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + } - // Try to authenticate with Oauth - function authenticate(task) { - var authWindow = undefined; - var intervalId = undefined; - task.onRun(function() { - if (github !== undefined) { - task.chain(); - return; - } - var token = localStorage["githubToken"]; - if(token !== undefined) { - github = new Github({ - token: token, - auth: "oauth" - }); - task.chain(); - return; - } - extensionMgr.onMessage("Please make sure the Github authorization popup is not blocked by your browser."); - var errorMsg = "Failed to retrieve a token from GitHub."; - // We add time for user to enter his credentials - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - var code = undefined; - function getCode() { - localStorage.removeItem("githubCode"); - authWindow = utils.popupWindow( - 'github-oauth-client.html?client_id=' + GITHUB_CLIENT_ID, - 'stackedit-github-oauth', 960, 600); - authWindow.focus(); - intervalId = setInterval(function() { - if(authWindow.closed === true) { - clearInterval(intervalId); - authWindow = undefined; - intervalId = undefined; - code = localStorage["githubCode"]; - if(code === undefined) { - task.error(new Error(errorMsg)); - return; - } - localStorage.removeItem("githubCode"); - task.chain(getToken); - } - }, 500); - } - function getToken() { - $.getJSON(GATEKEEPER_URL + "authenticate/" + code, function(data) { - if(data.token !== undefined) { - token = data.token; - localStorage["githubToken"] = token; - github = new Github({ - token: token, - auth: "oauth" - }); - task.chain(); - } - else { - task.error(new Error(errorMsg)); - } - }); - } - task.chain(getCode); - }); - task.onError(function() { - if(intervalId !== undefined) { - clearInterval(intervalId); - } - if(authWindow !== undefined) { - authWindow.close(); - } - }); - } + // Try to authenticate with Oauth + function authenticate(task) { + var authWindow = undefined; + var intervalId = undefined; + task.onRun(function() { + if(github !== undefined) { + task.chain(); + return; + } + var token = localStorage["githubToken"]; + if(token !== undefined) { + github = new Github({ + token: token, + auth: "oauth" + }); + task.chain(); + return; + } + extensionMgr.onMessage("Please make sure the Github authorization popup is not blocked by your browser."); + var errorMsg = "Failed to retrieve a token from GitHub."; + // We add time for user to enter his credentials + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + var code = undefined; + function getCode() { + localStorage.removeItem("githubCode"); + authWindow = utils.popupWindow('github-oauth-client.html?client_id=' + GITHUB_CLIENT_ID, 'stackedit-github-oauth', 960, 600); + authWindow.focus(); + intervalId = setInterval(function() { + if(authWindow.closed === true) { + clearInterval(intervalId); + authWindow = undefined; + intervalId = undefined; + code = localStorage["githubCode"]; + if(code === undefined) { + task.error(new Error(errorMsg)); + return; + } + localStorage.removeItem("githubCode"); + task.chain(getToken); + } + }, 500); + } + function getToken() { + $.getJSON(GATEKEEPER_URL + "authenticate/" + code, function(data) { + if(data.token !== undefined) { + token = data.token; + localStorage["githubToken"] = token; + github = new Github({ + token: token, + auth: "oauth" + }); + task.chain(); + } + else { + task.error(new Error(errorMsg)); + } + }); + } + task.chain(getCode); + }); + task.onError(function() { + if(intervalId !== undefined) { + clearInterval(intervalId); + } + if(authWindow !== undefined) { + authWindow.close(); + } + }); + } - githubHelper.upload = function(reponame, branch, path, content, commitMsg, callback) { - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var userLogin = undefined; - function getUserLogin() { - var user = github.getUser(); - user.show(undefined, function(err, result) { - if(err) { - handleError(err, task); - return; - } - userLogin = result.login; - task.chain(write); - }); - } - function write() { - var repo = github.getRepo(userLogin, reponame); - repo.write(branch, path, content, commitMsg, function(err) { - if(err) { - handleError(err, task); - return; - } - task.chain(); - }); - } - task.chain(getUserLogin); - }); - task.onSuccess(function() { - callback(); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - githubHelper.uploadGist = function(gistId, filename, isPublic, title, content, callback) { - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var gist = github.getGist(gistId); - var files = {}; - files[filename] = {content: content}; - githubFunction = gist.update; - if(gistId === undefined) { - githubFunction = gist.create; - } - githubFunction({ - description: title, - "public": isPublic, - files: files - }, function(err, gist) { - if(err) { - // Handle error - if(err.error === 404 && gistId !== undefined) { - err = 'Gist ' + gistId + ' not found on GitHub.|removePublish'; - } - handleError(err, task); - return; - } - gistId = gist.id; - task.chain(); - }); - }); - task.onSuccess(function() { - callback(undefined, gistId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - githubHelper.downloadGist = function(gistId, filename, callback) { - var task = asyncRunner.createTask(); - connect(task); - // No need for authentication - var title = undefined; - var content = undefined; - task.onRun(function() { - var github = new Github({}); - var gist = github.getGist(gistId); - gist.read(function(err, gist) { - if(err) { - // Handle error - task.error(new Error('Error trying to access Gist ' + gistId + '.')); - return; - } - title = gist.description; - var file = gist.files[filename]; - if(file === undefined) { - task.error(new Error('Gist ' + gistId + ' does not contain "' + filename + '".')); - return; - } - content = file.content; - task.chain(); - }); - }); - task.onSuccess(function() { - callback(undefined, title, content); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = undefined; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = error; - } - else { - errorMsg = "Could not publish on GitHub."; - if (error.error === 401 || error.error === 403) { - github = undefined; - localStorage.removeItem("githubToken"); - errorMsg = "Access to GitHub account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; - } else if (error.error <= 0) { - connected = false; - github = undefined; - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + githubHelper.upload = function(reponame, branch, path, content, commitMsg, callback) { + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var userLogin = undefined; + function getUserLogin() { + var user = github.getUser(); + user.show(undefined, function(err, result) { + if(err) { + handleError(err, task); + return; + } + userLogin = result.login; + task.chain(write); + }); + } + function write() { + var repo = github.getRepo(userLogin, reponame); + repo.write(branch, path, content, commitMsg, function(err) { + if(err) { + handleError(err, task); + return; + } + task.chain(); + }); + } + task.chain(getUserLogin); + }); + task.onSuccess(function() { + callback(); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - return githubHelper; + githubHelper.uploadGist = function(gistId, filename, isPublic, title, content, callback) { + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var gist = github.getGist(gistId); + var files = {}; + files[filename] = { + content: content + }; + githubFunction = gist.update; + if(gistId === undefined) { + githubFunction = gist.create; + } + githubFunction({ + description: title, + "public": isPublic, + files: files + }, function(err, gist) { + if(err) { + // Handle error + if(err.error === 404 && gistId !== undefined) { + err = 'Gist ' + gistId + ' not found on GitHub.|removePublish'; + } + handleError(err, task); + return; + } + gistId = gist.id; + task.chain(); + }); + }); + task.onSuccess(function() { + callback(undefined, gistId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; + + githubHelper.downloadGist = function(gistId, filename, callback) { + var task = asyncRunner.createTask(); + connect(task); + // No need for authentication + var title = undefined; + var content = undefined; + task.onRun(function() { + var github = new Github({}); + var gist = github.getGist(gistId); + gist.read(function(err, gist) { + if(err) { + // Handle error + task.error(new Error('Error trying to access Gist ' + gistId + '.')); + return; + } + title = gist.description; + var file = gist.files[filename]; + if(file === undefined) { + task.error(new Error('Gist ' + gistId + ' does not contain "' + filename + '".')); + return; + } + content = file.content; + task.chain(); + }); + }); + task.onSuccess(function() { + callback(undefined, title, content); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; + + function handleError(error, task) { + var errorMsg = undefined; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Could not publish on GitHub."; + if(error.error === 401 || error.error === 403) { + github = undefined; + localStorage.removeItem("githubToken"); + errorMsg = "Access to GitHub account is not authorized."; + task.retry(new Error(errorMsg), 1); + return; + } + else if(error.error <= 0) { + connected = false; + github = undefined; + core.setOffline(); + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } + + return githubHelper; }); diff --git a/js/github-provider.js b/js/github-provider.js index 32d7c7fb..30efe559 100644 --- a/js/github-provider.js +++ b/js/github-provider.js @@ -3,31 +3,33 @@ define([ "settings", "github-helper" ], function(utils, settings, githubHelper) { - - var PROVIDER_GITHUB = "github"; - - var githubProvider = { - providerId: PROVIDER_GITHUB, - providerName: "GitHub", - publishPreferencesInputIds: ["github-reponame", "github-branch"] - }; - - githubProvider.publish = function(publishAttributes, title, content, callback) { - var commitMsg = settings.commitMsg; - githubHelper.upload(publishAttributes.repository, publishAttributes.branch, - publishAttributes.path, content, commitMsg, callback); - }; - githubProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.repository = utils.getInputTextValue("#input-publish-github-reponame", event); - publishAttributes.branch = utils.getInputTextValue("#input-publish-github-branch", event); - publishAttributes.path = utils.getInputTextValue("#input-publish-file-path", event); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + var PROVIDER_GITHUB = "github"; - return githubProvider; + var githubProvider = { + providerId: PROVIDER_GITHUB, + providerName: "GitHub", + publishPreferencesInputIds: [ + "github-reponame", + "github-branch" + ] + }; + + githubProvider.publish = function(publishAttributes, title, content, callback) { + var commitMsg = settings.commitMsg; + githubHelper.upload(publishAttributes.repository, publishAttributes.branch, publishAttributes.path, content, commitMsg, callback); + }; + + githubProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.repository = utils.getInputTextValue("#input-publish-github-reponame", event); + publishAttributes.branch = utils.getInputTextValue("#input-publish-github-branch", event); + publishAttributes.path = utils.getInputTextValue("#input-publish-file-path", event); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + return githubProvider; }); \ No newline at end of file diff --git a/js/google-helper.js b/js/google-helper.js index e92f3bbc..f7d95551 100644 --- a/js/google-helper.js +++ b/js/google-helper.js @@ -6,518 +6,543 @@ define([ "async-runner" ], function($, core, utils, extensionMgr, asyncRunner) { - var connected = false; - var authenticated = false; + var connected = false; + var authenticated = false; - var googleHelper = {}; + var googleHelper = {}; - // Try to connect Gdrive by downloading client.js - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - connected = false; - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - if (connected === true) { - task.chain(); - return; - } - delayedFunction = function() { - connected = true; - task.chain(); - }; - $.ajax({ - url : "https://apis.google.com/js/client.js?onload=runDelayedFunction", - dataType : "script", timeout : AJAX_TIMEOUT - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - handleError(error, task); - }); - }); - } + // Try to connect Gdrive by downloading client.js + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + connected = false; + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + if(connected === true) { + task.chain(); + return; + } + delayedFunction = function() { + connected = true; + task.chain(); + }; + $.ajax({ + url: "https://apis.google.com/js/client.js?onload=runDelayedFunction", + dataType: "script", + timeout: AJAX_TIMEOUT + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + } - // Try to authenticate with Oauth - function authenticate(task) { - task.onRun(function() { - if (authenticated === true) { - task.chain(); - return; - } - var immediate = true; - function localAuthenticate() { - if (immediate === false) { - extensionMgr.onMessage("Please make sure the Google authorization popup is not blocked by your browser."); - // If not immediate we add time for user to enter his credentials - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - } - gapi.auth.authorize({ 'client_id' : GOOGLE_CLIENT_ID, - 'scope' : GOOGLE_SCOPES, 'immediate' : immediate }, function( - authResult) { - gapi.client.load('drive', 'v2', function() { - if (!authResult || authResult.error) { - // If immediate did not work retry without immediate flag - if (connected === true && immediate === true) { - immediate = false; - task.chain(localAuthenticate); - return; - } - // Error - task.error(new Error("Access to Google account is not authorized.")); - return; - } - // Success - authenticated = true; - task.chain(); - }); - }); - } - task.chain(localAuthenticate); - }); - } + // Try to authenticate with Oauth + function authenticate(task) { + task.onRun(function() { + if(authenticated === true) { + task.chain(); + return; + } + var immediate = true; + function localAuthenticate() { + if(immediate === false) { + extensionMgr.onMessage("Please make sure the Google authorization popup is not blocked by your browser."); + // If not immediate we add time for user to enter his + // credentials + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + } + gapi.auth.authorize({ + 'client_id': GOOGLE_CLIENT_ID, + 'scope': GOOGLE_SCOPES, + 'immediate': immediate + }, function(authResult) { + gapi.client.load('drive', 'v2', function() { + if(!authResult || authResult.error) { + // If immediate did not work retry without immediate + // flag + if(connected === true && immediate === true) { + immediate = false; + task.chain(localAuthenticate); + return; + } + // Error + task.error(new Error("Access to Google account is not authorized.")); + return; + } + // Success + authenticated = true; + task.chain(); + }); + }); + } + task.chain(localAuthenticate); + }); + } - googleHelper.upload = function(fileId, parentId, title, content, etag, callback) { - var result = undefined; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var boundary = '-------314159265358979323846'; - var delimiter = "\r\n--" + boundary + "\r\n"; - var close_delim = "\r\n--" + boundary + "--"; - var contentType = 'text/x-markdown'; - var metadata = { title : title, mimeType : contentType }; - if (parentId !== undefined) { - // Specify the directory - metadata.parents = [ { kind : 'drive#fileLink', - id : parentId } ]; - } - var path = '/upload/drive/v2/files'; - var method = 'POST'; - if (fileId !== undefined) { - // If it's an update - path += "/" + fileId; - method = 'PUT'; - } - var headers = { 'Content-Type' : 'multipart/mixed; boundary="' - + boundary + '"', }; - if(etag !== undefined) { - // Sometimes we have error 412 from Google even with the correct etag - //headers["If-Match"] = etag; - } + googleHelper.upload = function(fileId, parentId, title, content, etag, callback) { + var result = undefined; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var boundary = '-------314159265358979323846'; + var delimiter = "\r\n--" + boundary + "\r\n"; + var close_delim = "\r\n--" + boundary + "--"; + var contentType = 'text/x-markdown'; + var metadata = { + title: title, + mimeType: contentType + }; + if(parentId !== undefined) { + // Specify the directory + metadata.parents = [ + { + kind: 'drive#fileLink', + id: parentId + } + ]; + } + var path = '/upload/drive/v2/files'; + var method = 'POST'; + if(fileId !== undefined) { + // If it's an update + path += "/" + fileId; + method = 'PUT'; + } + var headers = { + 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"', + }; + if(etag !== undefined) { + // Sometimes we have error 412 from Google even with the correct + // etag + // headers["If-Match"] = etag; + } - var base64Data = utils.encodeBase64(content); - var multipartRequestBody = delimiter - + 'Content-Type: application/json\r\n\r\n' - + JSON.stringify(metadata) + delimiter + 'Content-Type: ' - + contentType + '\r\n' - + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' - + base64Data + close_delim; + var base64Data = utils.encodeBase64(content); + var multipartRequestBody = delimiter + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delimiter + 'Content-Type: ' + contentType + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' + base64Data + close_delim; - var request = gapi.client - .request({ - 'path' : path, - 'method' : method, - 'params' : { 'uploadType' : 'multipart', }, - 'headers' : headers, - 'body' : multipartRequestBody, }); - request.execute(function(response) { - if (response && response.id) { - // Upload success - result = response; - task.chain(); - return; - } - var error = response.error; - // Handle error - if(error !== undefined && fileId !== undefined) { - if(error.code === 404) { - error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish'; - } - else if(error.code === 412) { - // We may have missed a file update - localStorage.removeItem("gdrive.lastChangeId"); - error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.'; - } - } - handleError(error, task); - }); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + var request = gapi.client.request({ + 'path': path, + 'method': method, + 'params': { + 'uploadType': 'multipart', + }, + 'headers': headers, + 'body': multipartRequestBody, + }); + request.execute(function(response) { + if(response && response.id) { + // Upload success + result = response; + task.chain(); + return; + } + var error = response.error; + // Handle error + if(error !== undefined && fileId !== undefined) { + if(error.code === 404) { + error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish'; + } + else if(error.code === 412) { + // We may have missed a file update + localStorage.removeItem("gdrive.lastChangeId"); + error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.'; + } + } + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - googleHelper.checkChanges = function(lastChangeId, callback) { - var changes = []; - var newChangeId = lastChangeId || 0; - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var nextPageToken = undefined; - function retrievePageOfChanges() { - var request = undefined; - if(nextPageToken === undefined) { - request = gapi.client.drive.changes - .list({ 'startChangeId' : newChangeId + 1 }); - } - else { - request = gapi.client.drive.changes - .list({ 'pageToken' : nextPageToken }); - } + googleHelper.checkChanges = function(lastChangeId, callback) { + var changes = []; + var newChangeId = lastChangeId || 0; + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var nextPageToken = undefined; + function retrievePageOfChanges() { + var request = undefined; + if(nextPageToken === undefined) { + request = gapi.client.drive.changes.list({ + 'startChangeId': newChangeId + 1 + }); + } + else { + request = gapi.client.drive.changes.list({ + 'pageToken': nextPageToken + }); + } - request.execute(function(response) { - if (!response || !response.largestChangeId) { - // Handle error - handleError(response.error, task); - return; - } - // Retrieve success - newChangeId = response.largestChangeId; - nextPageToken = response.nextPageToken; - if (response.items !== undefined) { - changes = changes.concat(response.items); - } - if (nextPageToken !== undefined) { - task.chain(retrievePageOfChanges); - } - else { - task.chain(); - } - }); - } - task.chain(retrievePageOfChanges); - }); - task.onSuccess(function() { - callback(undefined, changes, newChangeId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + request.execute(function(response) { + if(!response || !response.largestChangeId) { + // Handle error + handleError(response.error, task); + return; + } + // Retrieve success + newChangeId = response.largestChangeId; + nextPageToken = response.nextPageToken; + if(response.items !== undefined) { + changes = changes.concat(response.items); + } + if(nextPageToken !== undefined) { + task.chain(retrievePageOfChanges); + } + else { + task.chain(); + } + }); + } + task.chain(retrievePageOfChanges); + }); + task.onSuccess(function() { + callback(undefined, changes, newChangeId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - googleHelper.downloadMetadata = function(ids, callback, skipAuth) { - var result = []; - var task = asyncRunner.createTask(); - connect(task); - if(!skipAuth) { - authenticate(task); - } - task.onRun(function() { - function recursiveDownloadMetadata() { - if(ids.length === 0) { - task.chain(); - return; - } - var id = ids[0]; - var headers = {}; - var token = gapi.auth.getToken(); - if(token) { - headers.Authorization = "Bearer " + token.access_token; - } - $.ajax({ - url : "https://www.googleapis.com/drive/v2/files/" + id, - headers : headers, - data : {key: GOOGLE_API_KEY}, - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(data, textStatus, jqXHR) { - result.push(data); - ids.shift(); - task.chain(recursiveDownloadMetadata); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - // Handle error - if(error.code === 404) { - error = 'File ID "' + id + '" not found on Google Drive.'; - } - handleError(error, task); - }); - } - task.chain(recursiveDownloadMetadata); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + googleHelper.downloadMetadata = function(ids, callback, skipAuth) { + var result = []; + var task = asyncRunner.createTask(); + connect(task); + if(!skipAuth) { + authenticate(task); + } + task.onRun(function() { + function recursiveDownloadMetadata() { + if(ids.length === 0) { + task.chain(); + return; + } + var id = ids[0]; + var headers = {}; + var token = gapi.auth.getToken(); + if(token) { + headers.Authorization = "Bearer " + token.access_token; + } + $.ajax({ + url: "https://www.googleapis.com/drive/v2/files/" + id, + headers: headers, + data: { + key: GOOGLE_API_KEY + }, + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(data, textStatus, jqXHR) { + result.push(data); + ids.shift(); + task.chain(recursiveDownloadMetadata); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + if(error.code === 404) { + error = 'File ID "' + id + '" not found on Google Drive.'; + } + handleError(error, task); + }); + } + task.chain(recursiveDownloadMetadata); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - googleHelper.downloadContent = function(objects, callback, skipAuth) { - var result = []; - var task = asyncRunner.createTask(); - // Add some time for user to choose his files - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - connect(task); - if(!skipAuth) { - authenticate(task); - } - task.onRun(function() { - function recursiveDownloadContent() { - if(objects.length === 0) { - task.chain(); - return; - } - var object = objects[0]; - result.push(object); - var file = undefined; - // object may be a file - if(object.kind == "drive#file") { - file = object; - } - // object may be a change - else if(object.kind == "drive#change") { - file = object.file; - } - if(!file) { - objects.shift(); - task.chain(recursiveDownloadContent); - return; - } - var headers = {}; - var token = gapi.auth.getToken(); - if(token) { - headers.Authorization = "Bearer " + token.access_token; - } - $.ajax({ - url : file.downloadUrl, - headers : headers, - data : {key: GOOGLE_API_KEY}, - dataType : "text", - timeout : AJAX_TIMEOUT - }).done(function(data, textStatus, jqXHR) { - file.content = data; - objects.shift(); - task.chain(recursiveDownloadContent); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - // Handle error - handleError(error, task); - }); - } - task.chain(recursiveDownloadContent); - }); - task.onSuccess(function() { - callback(undefined, result); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = undefined; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = error; - } - else { - errorMsg = "Google error (" + error.code + ": " - + error.message + ")."; - if (error.code >= 500 && error.code < 600) { - // Retry as described in Google's best practices - task.retry(new Error(errorMsg)); - return; - } else if (error.code === 401 || error.code === 403) { - authenticated = false; - errorMsg = "Access to Google account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; - } else if (error.code <= 0) { - connected = false; - authenticated = false; - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + googleHelper.downloadContent = function(objects, callback, skipAuth) { + var result = []; + var task = asyncRunner.createTask(); + // Add some time for user to choose his files + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + connect(task); + if(!skipAuth) { + authenticate(task); + } + task.onRun(function() { + function recursiveDownloadContent() { + if(objects.length === 0) { + task.chain(); + return; + } + var object = objects[0]; + result.push(object); + var file = undefined; + // object may be a file + if(object.kind == "drive#file") { + file = object; + } + // object may be a change + else if(object.kind == "drive#change") { + file = object.file; + } + if(!file) { + objects.shift(); + task.chain(recursiveDownloadContent); + return; + } + var headers = {}; + var token = gapi.auth.getToken(); + if(token) { + headers.Authorization = "Bearer " + token.access_token; + } + $.ajax({ + url: file.downloadUrl, + headers: headers, + data: { + key: GOOGLE_API_KEY + }, + dataType: "text", + timeout: AJAX_TIMEOUT + }).done(function(data, textStatus, jqXHR) { + file.content = data; + objects.shift(); + task.chain(recursiveDownloadContent); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + handleError(error, task); + }); + } + task.chain(recursiveDownloadContent); + }); + task.onSuccess(function() { + callback(undefined, result); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - var pickerLoaded = false; - function loadPicker(task) { - task.onRun(function() { - if (pickerLoaded === true) { - task.chain(); - return; - } - $.ajax({ - url : "//www.google.com/jsapi", - data : {key: GOOGLE_API_KEY}, - dataType : "script", - timeout : AJAX_TIMEOUT - }).done(function() { - google.load('picker', '1', {callback: task.chain}); - pickerLoaded = true; - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - handleError(error, task); - }); - }); - } - - googleHelper.picker = function(callback) { - var ids = []; - var picker = undefined; - function hidePicker() { - if(picker !== undefined) { - picker.setVisible(false); - $(".modal-backdrop, .picker").remove(); - } - } - var task = asyncRunner.createTask(); - connect(task); - loadPicker(task); - task.onRun(function() { - var view = new google.picker.View(google.picker.ViewId.DOCS); - view.setMimeTypes("text/x-markdown,text/plain,application/octet-stream"); - var pickerBuilder = new google.picker.PickerBuilder(); - pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN); - pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED); - pickerBuilder.setAppId(GOOGLE_DRIVE_APP_ID); - var token = gapi.auth.getToken(); - if(token) { - pickerBuilder.setOAuthToken(token.access_token); - } - pickerBuilder.addView(view); - pickerBuilder.addView(new google.picker.DocsUploadView()); - pickerBuilder.setCallback(function(data) { - if (data.action == google.picker.Action.PICKED || - data.action == google.picker.Action.CANCEL) { - if(data.action == google.picker.Action.PICKED) { - for(var i=0; i").addClass("modal-backdrop").click(function() { - hidePicker(); - task.chain(); - })); - picker.setVisible(true); - }); - task.onSuccess(function() { - callback(undefined, ids); - }); - task.onError(function(error) { - hidePicker(); - callback(error); - }); - asyncRunner.addTask(task); - }; + function handleError(error, task) { + var errorMsg = undefined; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Google error (" + error.code + ": " + error.message + ")."; + if(error.code >= 500 && error.code < 600) { + // Retry as described in Google's best practices + task.retry(new Error(errorMsg)); + return; + } + else if(error.code === 401 || error.code === 403) { + authenticated = false; + errorMsg = "Access to Google account is not authorized."; + task.retry(new Error(errorMsg), 1); + return; + } + else if(error.code <= 0) { + connected = false; + authenticated = false; + core.setOffline(); + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } - googleHelper.uploadBlogger = function(blogUrl, blogId, postId, labelList, title, content, callback) { - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var headers = {}; - var token = gapi.auth.getToken(); - if(token) { - headers.Authorization = "Bearer " + token.access_token; - } - function publish() { - var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/"; - var data = { - kind: "blogger#post", - blog: { id: blogId }, - labels: labelList, - title: title, - content: content - }; - var type = "POST"; - // If it's an update - if(postId !== undefined) { - url += postId; - data.id = postId; - type = "PUT"; - } - $.ajax({ - url : url, - data: JSON.stringify(data), - headers : headers, - type: type, - contentType: "application/json", - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(post, textStatus, jqXHR) { - postId = post.id; - task.chain(); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - // Handle error - if(error.code === 404 && postId !== undefined) { - error = 'Post ' + postId + ' not found on Blogger.|removePublish'; - } - handleError(error, task); - }); - } - function getBlogId() { - if(blogId !== undefined) { - task.chain(publish); - return; - } - $.ajax({ - url : "https://www.googleapis.com/blogger/v3/blogs/byurl", - data: { url: blogUrl }, - headers : headers, - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(blog, textStatus, jqXHR) { - blogId = blog.id; - task.chain(publish); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - // Handle error - if(error.code === 404) { - error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish'; - } - handleError(error, task); - }); - } - task.chain(getBlogId); - }); - task.onSuccess(function() { - callback(undefined, blogId, postId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; + var pickerLoaded = false; + function loadPicker(task) { + task.onRun(function() { + if(pickerLoaded === true) { + task.chain(); + return; + } + $.ajax({ + url: "//www.google.com/jsapi", + data: { + key: GOOGLE_API_KEY + }, + dataType: "script", + timeout: AJAX_TIMEOUT + }).done(function() { + google.load('picker', '1', { + callback: task.chain + }); + pickerLoaded = true; + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + } - return googleHelper; + googleHelper.picker = function(callback) { + var ids = []; + var picker = undefined; + function hidePicker() { + if(picker !== undefined) { + picker.setVisible(false); + $(".modal-backdrop, .picker").remove(); + } + } + var task = asyncRunner.createTask(); + connect(task); + loadPicker(task); + task.onRun(function() { + var view = new google.picker.View(google.picker.ViewId.DOCS); + view.setMimeTypes("text/x-markdown,text/plain,application/octet-stream"); + var pickerBuilder = new google.picker.PickerBuilder(); + pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN); + pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED); + pickerBuilder.setAppId(GOOGLE_DRIVE_APP_ID); + var token = gapi.auth.getToken(); + if(token) { + pickerBuilder.setOAuthToken(token.access_token); + } + pickerBuilder.addView(view); + pickerBuilder.addView(new google.picker.DocsUploadView()); + pickerBuilder.setCallback(function(data) { + if(data.action == google.picker.Action.PICKED || data.action == google.picker.Action.CANCEL) { + if(data.action == google.picker.Action.PICKED) { + for ( var i = 0; i < data.docs.length; i++) { + ids.push(data.docs[i].id); + } + } + hidePicker(); + task.chain(); + } + }); + picker = pickerBuilder.build(); + $("body").append($("
    ").addClass("modal-backdrop").click(function() { + hidePicker(); + task.chain(); + })); + picker.setVisible(true); + }); + task.onSuccess(function() { + callback(undefined, ids); + }); + task.onError(function(error) { + hidePicker(); + callback(error); + }); + asyncRunner.addTask(task); + }; + + googleHelper.uploadBlogger = function(blogUrl, blogId, postId, labelList, title, content, callback) { + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var headers = {}; + var token = gapi.auth.getToken(); + if(token) { + headers.Authorization = "Bearer " + token.access_token; + } + function publish() { + var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/"; + var data = { + kind: "blogger#post", + blog: { + id: blogId + }, + labels: labelList, + title: title, + content: content + }; + var type = "POST"; + // If it's an update + if(postId !== undefined) { + url += postId; + data.id = postId; + type = "PUT"; + } + $.ajax({ + url: url, + data: JSON.stringify(data), + headers: headers, + type: type, + contentType: "application/json", + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(post, textStatus, jqXHR) { + postId = post.id; + task.chain(); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + if(error.code === 404 && postId !== undefined) { + error = 'Post ' + postId + ' not found on Blogger.|removePublish'; + } + handleError(error, task); + }); + } + function getBlogId() { + if(blogId !== undefined) { + task.chain(publish); + return; + } + $.ajax({ + url: "https://www.googleapis.com/blogger/v3/blogs/byurl", + data: { + url: blogUrl + }, + headers: headers, + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(blog, textStatus, jqXHR) { + blogId = blog.id; + task.chain(publish); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + if(error.code === 404) { + error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish'; + } + handleError(error, task); + }); + } + task.chain(getBlogId); + }); + task.onSuccess(function() { + callback(undefined, blogId, postId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; + + return googleHelper; }); diff --git a/js/publisher.js b/js/publisher.js index 5ca8376f..decd980b 100644 --- a/js/publisher.js +++ b/js/publisher.js @@ -18,236 +18,229 @@ define([ "wordpress-provider" ], function($, _, core, utils, settings, extensionMgr, fileSystem, fileMgr, sharing) { - var publisher = {}; - - // Create a map with providerId: providerModule - var providerMap = _.chain( - arguments - ).map(function(argument) { - return argument && argument.providerId && [argument.providerId, argument]; - }).compact().object().value(); - - // Retrieve publish locations from localStorage - _.each(fileSystem, function(fileDesc) { - _.chain( - localStorage[fileDesc.fileIndex + ".publish"].split(";") - ).compact().each(function(publishIndex) { - var publishAttributes = JSON.parse(localStorage[publishIndex]); - // Store publishIndex - publishAttributes.publishIndex = publishIndex; - // Replace provider ID by provider module in attributes - publishAttributes.provider = providerMap[publishAttributes.provider]; - fileDesc.publishLocations[publishIndex] = publishAttributes; - }); - }); + var publisher = {}; - // Apply template to the current document - publisher.applyTemplate = function(publishAttributes) { - var fileDesc = fileMgr.getCurrentFile(); - try { - return _.template(settings.template, { - documentTitle: fileDesc.title, - documentMarkdown: $("#wmd-input").val(), - documentHTML: $("#wmd-preview").html(), - publishAttributes: publishAttributes - }); - } catch(e) { - extensionMgr.onError(e); - throw e; - } - }; - - // Used to get content to publish - function getPublishContent(publishAttributes) { - if(publishAttributes.format === undefined) { - publishAttributes.format = $("input:radio[name=radio-publish-format]:checked").prop("value"); - } - if(publishAttributes.format == "markdown") { - return $("#wmd-input").val(); - } - else if(publishAttributes.format == "html") { - return $("#wmd-preview").html(); - } - else { - return publisher.applyTemplate(publishAttributes); - } - } - - // Recursive function to publish a file on multiple locations - var publishAttributesList = []; - var publishTitle = undefined; - function publishLocation(callback, errorFlag) { - - // No more publish location for this document - if (publishAttributesList.length === 0) { - callback(errorFlag); - return; - } - - // Dequeue a synchronized location - var publishAttributes = publishAttributesList.pop(); - var content = getPublishContent(publishAttributes); - - // Call the provider - publishAttributes.provider.publish(publishAttributes, publishTitle, content, function(error) { - if(error !== undefined) { - var errorMsg = error.toString(); - if(errorMsg.indexOf("|removePublish") !== -1) { - fileMgr.removePublish(publishAttributes); - } - if(errorMsg.indexOf("|stopPublish") !== -1) { - callback(error); - return; - } - } - publishLocation(callback, errorFlag || error ); - }); - } - - var publishRunning = false; - publisher.publish = function() { - // If publish is running or offline - if(publishRunning === true || core.isOffline) { - return; - } - - publishRunning = true; - extensionMgr.onPublishRunning(true); - var fileDesc = fileMgr.getCurrentFile(); - publishTitle = fileDesc.title; - publishAttributesList = _.values(fileDesc.publishLocations); - publishLocation(function(errorFlag) { - publishRunning = false; - extensionMgr.onPublishRunning(false); - if(errorFlag === undefined) { - extensionMgr.onPublishSuccess(fileDesc); - } - }); - }; - - // Generate a publishIndex associated to a file and store publishAttributes - function createPublishIndex(fileDesc, publishAttributes) { - var publishIndex = undefined; - do { - publishIndex = "publish." + utils.randomString(); - } while(_.has(localStorage, publishIndex)); - publishAttributes.publishIndex = publishIndex; - utils.storeAttributes(publishAttributes); - fileMgr.addPublish(fileDesc, publishAttributes); - } - - // Initialize the "New publication" dialog - var newLocationProvider = undefined; - function initNewLocation(provider) { - var defaultPublishFormat = provider.defaultPublishFormat || "markdown"; - newLocationProvider = provider; - $(".publish-provider-name").text(provider.providerName); - - // Show/hide controls depending on provider - $('div[class*=" modal-publish-"]').hide().filter(".modal-publish-" + provider.providerId).show(); - - // Reset fields - utils.resetModalInputs(); - $("input:radio[name=radio-publish-format][value=" + defaultPublishFormat + "]").prop("checked", true); - - // Load preferences - var serializedPreferences = localStorage[provider.providerId + ".publishPreferences"]; - if(serializedPreferences) { - var publishPreferences = JSON.parse(serializedPreferences); - _.each(provider.publishPreferencesInputIds, function(inputId) { - utils.setInputValue("#input-publish-" + inputId, publishPreferences[inputId]); - }); - utils.setInputRadio("radio-publish-format", publishPreferences.format); - } - - // Open dialog box - $("#modal-publish").modal(); - } - - // Add a new publish location to a local document - function performNewLocation(event) { - var provider = newLocationProvider; - var publishAttributes = provider.newPublishAttributes(event); - if(publishAttributes === undefined) { - return; - } - - // Perform provider's publishing - var fileDesc = fileMgr.getCurrentFile(); - var title = fileDesc.title; - var content = getPublishContent(publishAttributes); - provider.publish(publishAttributes, title, content, function(error) { - if(error === undefined) { - publishAttributes.provider = provider.providerId; - sharing.createLink(publishAttributes, function() { - createPublishIndex(fileDesc, publishAttributes); - }); - } - }); - - // Store input values as preferences for next time we open the publish dialog - var publishPreferences = {}; - _.each(provider.publishPreferencesInputIds, function(inputId) { - publishPreferences[inputId] = $("#input-publish-" + inputId).val(); - }); - publishPreferences.format = publishAttributes.format; - localStorage[provider.providerId + ".publishPreferences"] = JSON.stringify(publishPreferences); - } - - // Retrieve file's publish locations from localStorage - publisher.populatePublishLocations = function(fileDesc) { - _.chain( - localStorage[fileDesc.fileIndex + ".publish"].split(";") - ).compact().each(function(publishIndex) { - var publishAttributes = JSON.parse(localStorage[publishIndex]); - // Store publishIndex - publishAttributes.publishIndex = publishIndex; - // Replace provider ID by provider module in attributes - publishAttributes.provider = providerMap[publishAttributes.provider]; - fileDesc.publishLocations[publishIndex] = publishAttributes; - }); - }; - - core.onReady(function() { - // Add every provider - var publishMenu = $("#publish-menu"); - _.each(providerMap, function(provider) { - // Provider's publish button - publishMenu.append( - $("
  • ").append( - $(' ' + provider.providerName + '') - .click(function() { - initNewLocation(provider); - } - ) - ) - ); - // Action links (if any) - $(".action-publish-" + provider.providerId).click(function() { - initNewLocation(provider); - }); - }); - - $(".action-process-publish").click(performNewLocation); - - // Save As menu items - $(".action-download-md").click(function() { - var content = $("#wmd-input").val(); - var title = fileMgr.getCurrentFile().title; - utils.saveAs(content, title + ".md"); - }); - $(".action-download-html").click(function() { - var content = $("#wmd-preview").html(); - var title = fileMgr.getCurrentFile().title; - utils.saveAs(content, title + ".html"); - }); - $(".action-download-template").click(function() { - var content = publisher.applyTemplate(); - var title = fileMgr.getCurrentFile().title; - utils.saveAs(content, title + ".txt"); - }); - }); - - extensionMgr.onPublisherCreated(publisher); - return publisher; + // Create a map with providerId: providerModule + var providerMap = _.chain(arguments).map(function(argument) { + return argument && argument.providerId && [ + argument.providerId, + argument + ]; + }).compact().object().value(); + + // Retrieve publish locations from localStorage + _.each(fileSystem, function(fileDesc) { + _.chain(localStorage[fileDesc.fileIndex + ".publish"].split(";")).compact().each(function(publishIndex) { + var publishAttributes = JSON.parse(localStorage[publishIndex]); + // Store publishIndex + publishAttributes.publishIndex = publishIndex; + // Replace provider ID by provider module in attributes + publishAttributes.provider = providerMap[publishAttributes.provider]; + fileDesc.publishLocations[publishIndex] = publishAttributes; + }); + }); + + // Apply template to the current document + publisher.applyTemplate = function(publishAttributes) { + var fileDesc = fileMgr.getCurrentFile(); + try { + return _.template(settings.template, { + documentTitle: fileDesc.title, + documentMarkdown: $("#wmd-input").val(), + documentHTML: $("#wmd-preview").html(), + publishAttributes: publishAttributes + }); + } + catch (e) { + extensionMgr.onError(e); + throw e; + } + }; + + // Used to get content to publish + function getPublishContent(publishAttributes) { + if(publishAttributes.format === undefined) { + publishAttributes.format = $("input:radio[name=radio-publish-format]:checked").prop("value"); + } + if(publishAttributes.format == "markdown") { + return $("#wmd-input").val(); + } + else if(publishAttributes.format == "html") { + return $("#wmd-preview").html(); + } + else { + return publisher.applyTemplate(publishAttributes); + } + } + + // Recursive function to publish a file on multiple locations + var publishAttributesList = []; + var publishTitle = undefined; + function publishLocation(callback, errorFlag) { + + // No more publish location for this document + if(publishAttributesList.length === 0) { + callback(errorFlag); + return; + } + + // Dequeue a synchronized location + var publishAttributes = publishAttributesList.pop(); + var content = getPublishContent(publishAttributes); + + // Call the provider + publishAttributes.provider.publish(publishAttributes, publishTitle, content, function(error) { + if(error !== undefined) { + var errorMsg = error.toString(); + if(errorMsg.indexOf("|removePublish") !== -1) { + fileMgr.removePublish(publishAttributes); + } + if(errorMsg.indexOf("|stopPublish") !== -1) { + callback(error); + return; + } + } + publishLocation(callback, errorFlag || error); + }); + } + + var publishRunning = false; + publisher.publish = function() { + // If publish is running or offline + if(publishRunning === true || core.isOffline) { + return; + } + + publishRunning = true; + extensionMgr.onPublishRunning(true); + var fileDesc = fileMgr.getCurrentFile(); + publishTitle = fileDesc.title; + publishAttributesList = _.values(fileDesc.publishLocations); + publishLocation(function(errorFlag) { + publishRunning = false; + extensionMgr.onPublishRunning(false); + if(errorFlag === undefined) { + extensionMgr.onPublishSuccess(fileDesc); + } + }); + }; + + // Generate a publishIndex associated to a file and store publishAttributes + function createPublishIndex(fileDesc, publishAttributes) { + var publishIndex = undefined; + do { + publishIndex = "publish." + utils.randomString(); + } while (_.has(localStorage, publishIndex)); + publishAttributes.publishIndex = publishIndex; + utils.storeAttributes(publishAttributes); + fileMgr.addPublish(fileDesc, publishAttributes); + } + + // Initialize the "New publication" dialog + var newLocationProvider = undefined; + function initNewLocation(provider) { + var defaultPublishFormat = provider.defaultPublishFormat || "markdown"; + newLocationProvider = provider; + $(".publish-provider-name").text(provider.providerName); + + // Show/hide controls depending on provider + $('div[class*=" modal-publish-"]').hide().filter(".modal-publish-" + provider.providerId).show(); + + // Reset fields + utils.resetModalInputs(); + $("input:radio[name=radio-publish-format][value=" + defaultPublishFormat + "]").prop("checked", true); + + // Load preferences + var serializedPreferences = localStorage[provider.providerId + ".publishPreferences"]; + if(serializedPreferences) { + var publishPreferences = JSON.parse(serializedPreferences); + _.each(provider.publishPreferencesInputIds, function(inputId) { + utils.setInputValue("#input-publish-" + inputId, publishPreferences[inputId]); + }); + utils.setInputRadio("radio-publish-format", publishPreferences.format); + } + + // Open dialog box + $("#modal-publish").modal(); + } + + // Add a new publish location to a local document + function performNewLocation(event) { + var provider = newLocationProvider; + var publishAttributes = provider.newPublishAttributes(event); + if(publishAttributes === undefined) { + return; + } + + // Perform provider's publishing + var fileDesc = fileMgr.getCurrentFile(); + var title = fileDesc.title; + var content = getPublishContent(publishAttributes); + provider.publish(publishAttributes, title, content, function(error) { + if(error === undefined) { + publishAttributes.provider = provider.providerId; + sharing.createLink(publishAttributes, function() { + createPublishIndex(fileDesc, publishAttributes); + }); + } + }); + + // Store input values as preferences for next time we open the publish + // dialog + var publishPreferences = {}; + _.each(provider.publishPreferencesInputIds, function(inputId) { + publishPreferences[inputId] = $("#input-publish-" + inputId).val(); + }); + publishPreferences.format = publishAttributes.format; + localStorage[provider.providerId + ".publishPreferences"] = JSON.stringify(publishPreferences); + } + + // Retrieve file's publish locations from localStorage + publisher.populatePublishLocations = function(fileDesc) { + _.chain(localStorage[fileDesc.fileIndex + ".publish"].split(";")).compact().each(function(publishIndex) { + var publishAttributes = JSON.parse(localStorage[publishIndex]); + // Store publishIndex + publishAttributes.publishIndex = publishIndex; + // Replace provider ID by provider module in attributes + publishAttributes.provider = providerMap[publishAttributes.provider]; + fileDesc.publishLocations[publishIndex] = publishAttributes; + }); + }; + + core.onReady(function() { + // Add every provider + var publishMenu = $("#publish-menu"); + _.each(providerMap, function(provider) { + // Provider's publish button + publishMenu.append($("
  • ").append($(' ' + provider.providerName + '').click(function() { + initNewLocation(provider); + }))); + // Action links (if any) + $(".action-publish-" + provider.providerId).click(function() { + initNewLocation(provider); + }); + }); + + $(".action-process-publish").click(performNewLocation); + + // Save As menu items + $(".action-download-md").click(function() { + var content = $("#wmd-input").val(); + var title = fileMgr.getCurrentFile().title; + utils.saveAs(content, title + ".md"); + }); + $(".action-download-html").click(function() { + var content = $("#wmd-preview").html(); + var title = fileMgr.getCurrentFile().title; + utils.saveAs(content, title + ".html"); + }); + $(".action-download-template").click(function() { + var content = publisher.applyTemplate(); + var title = fileMgr.getCurrentFile().title; + utils.saveAs(content, title + ".txt"); + }); + }); + + extensionMgr.onPublisherCreated(publisher); + return publisher; }); \ No newline at end of file diff --git a/js/settings.js b/js/settings.js index e94df83a..2793ac13 100644 --- a/js/settings.js +++ b/js/settings.js @@ -2,28 +2,29 @@ define([ "underscore", "config" ], function(_) { - - var settings = { - layoutOrientation : "horizontal", - lazyRendering : true, - editorFontSize : 14, - defaultContent: "\n\n\n> Written with [StackEdit](http://benweet.github.io/stackedit/).", - commitMsg : "Published by http://benweet.github.io/stackedit", - template : [ + + var settings = { + layoutOrientation: "horizontal", + lazyRendering: true, + editorFontSize: 14, + defaultContent: "\n\n\n> Written with [StackEdit](http://benweet.github.io/stackedit/).", + commitMsg: "Published by http://benweet.github.io/stackedit", + template: [ '\n', - '\n', - '\n', - '<%= documentTitle %>\n', - '\n', - '<%= documentHTML %>\n', - ''].join(""), - sshProxy : SSH_PROXY_URL, - extensionSettings: {} - }; - - if (_.has(localStorage, "settings")) { - _.extend(settings, JSON.parse(localStorage.settings)); - } - - return settings; + '\n', + '\n', + '<%= documentTitle %>\n', + '\n', + '<%= documentHTML %>\n', + '' + ].join(""), + sshProxy: SSH_PROXY_URL, + extensionSettings: {} + }; + + if(_.has(localStorage, "settings")) { + _.extend(settings, JSON.parse(localStorage.settings)); + } + + return settings; }); \ No newline at end of file diff --git a/js/sharing.js b/js/sharing.js index 1397ab45..8ba55b08 100644 --- a/js/sharing.js +++ b/js/sharing.js @@ -10,125 +10,128 @@ define([ "download-provider", "gist-provider" ], function($, _, core, utils, extensionMgr, fileMgr, asyncRunner) { - - var sharing = {}; - - // Create a map with providerId: providerModule - var providerMap = _.chain( - arguments - ).map(function(argument) { - return argument && argument.providerId && [argument.providerId, argument]; - }).compact().object().value(); - // Used to populate the "Sharing" dropdown box - var lineTemplate = ['
    ', - '', - '', - '
    '].join(""); - sharing.refreshDocumentSharing = function(attributesList) { - var linkList = $("#link-container .link-list").empty(); - $("#link-container .no-link").show(); - _.each(attributesList, function(attributes) { - if(attributes.sharingLink) { - var lineElement = $(_.template(lineTemplate, { - link: attributes.sharingLink - })); - lineElement.click(function(event) { - event.stopPropagation(); - }); - linkList.append(lineElement); - $("#link-container .no-link").hide(); - } - }); - }; - - sharing.createLink = function(attributes, callback) { - var provider = providerMap[attributes.provider]; - // Don't create link if link already exists or provider is not compatible for sharing - if(attributes.sharingLink !== undefined || provider === undefined - // Or document is not published in markdown format - || attributes.format != "markdown") { - callback(); - return; - } - var task = asyncRunner.createTask(); - var shortUrl = undefined; - task.onRun(function() { - if(core.isOffline === true) { - task.chain(); - return; - } - var url = [MAIN_URL, 'viewer.html?provider=', attributes.provider]; - _.each(provider.sharingAttributes, function(attributeName) { - url.push('&'); - url.push(attributeName); - url.push('='); - url.push(encodeURIComponent(attributes[attributeName])); - }); - url = url.join(""); - $.getJSON( - "https://api-ssl.bitly.com/v3/shorten", - { - "access_token": BITLY_ACCESS_TOKEN, - "longUrl": url - }, - function(response) - { - if(response.data) { - shortUrl = response.data.url; - attributes.sharingLink = shortUrl; - } - else { - extensionMgr.onError("An error occured while creating sharing link."); - attributes.sharingLink = url; - } - task.chain(); - } - ); - }); - function onFinish() { - callback(); - } - task.onSuccess(onFinish); - task.onError(onFinish); - asyncRunner.addTask(task); - }; - - core.onReady(function() { - if(viewerMode === false) { - return; - } - // Check parameters to see if we have to download a shared document - var providerId = utils.getURLParameter("provider"); - if(providerId === undefined) { - providerId = "download"; - } - var provider = providerMap[providerId]; - if(provider === undefined) { - return; - } - var importParameters = {}; - _.each(provider.sharingAttributes, function(attributeName) { - var parameter = utils.getURLParameter(attributeName); - if(!parameter) { - importParameters = undefined; - return; - } - importParameters[attributeName] = parameter; - }); - if(importParameters === undefined) { - return; - } - $("#wmd-preview, #file-title").hide(); - provider.importPublic(importParameters, function(error, title, content) { - $("#wmd-preview, #file-title").show(); - if(error) { - return; - } - var fileDesc = fileMgr.createFile(title, content, undefined, true); - fileMgr.selectFile(fileDesc); - }); - }); - - return sharing; + var sharing = {}; + + // Create a map with providerId: providerModule + var providerMap = _.chain(arguments).map(function(argument) { + return argument && argument.providerId && [ + argument.providerId, + argument + ]; + }).compact().object().value(); + + // Used to populate the "Sharing" dropdown box + var lineTemplate = [ + '
    ', + ' ', + ' ', + '
    ' + ].join(""); + sharing.refreshDocumentSharing = function(attributesList) { + var linkList = $("#link-container .link-list").empty(); + $("#link-container .no-link").show(); + _.each(attributesList, function(attributes) { + if(attributes.sharingLink) { + var lineElement = $(_.template(lineTemplate, { + link: attributes.sharingLink + })); + lineElement.click(function(event) { + event.stopPropagation(); + }); + linkList.append(lineElement); + $("#link-container .no-link").hide(); + } + }); + }; + + sharing.createLink = function(attributes, callback) { + var provider = providerMap[attributes.provider]; + // Don't create link if link already exists or provider is not + // compatible for sharing + if(attributes.sharingLink !== undefined || provider === undefined + // Or document is not published in markdown format + || attributes.format != "markdown") { + callback(); + return; + } + var task = asyncRunner.createTask(); + var shortUrl = undefined; + task.onRun(function() { + if(core.isOffline === true) { + task.chain(); + return; + } + var url = [ + MAIN_URL, + 'viewer.html?provider=', + attributes.provider + ]; + _.each(provider.sharingAttributes, function(attributeName) { + url.push('&'); + url.push(attributeName); + url.push('='); + url.push(encodeURIComponent(attributes[attributeName])); + }); + url = url.join(""); + $.getJSON("https://api-ssl.bitly.com/v3/shorten", { + "access_token": BITLY_ACCESS_TOKEN, + "longUrl": url + }, function(response) { + if(response.data) { + shortUrl = response.data.url; + attributes.sharingLink = shortUrl; + } + else { + extensionMgr.onError("An error occured while creating sharing link."); + attributes.sharingLink = url; + } + task.chain(); + }); + }); + function onFinish() { + callback(); + } + task.onSuccess(onFinish); + task.onError(onFinish); + asyncRunner.addTask(task); + }; + + core.onReady(function() { + if(viewerMode === false) { + return; + } + // Check parameters to see if we have to download a shared document + var providerId = utils.getURLParameter("provider"); + if(providerId === undefined) { + providerId = "download"; + } + var provider = providerMap[providerId]; + if(provider === undefined) { + return; + } + var importParameters = {}; + _.each(provider.sharingAttributes, function(attributeName) { + var parameter = utils.getURLParameter(attributeName); + if(!parameter) { + importParameters = undefined; + return; + } + importParameters[attributeName] = parameter; + }); + if(importParameters === undefined) { + return; + } + $("#wmd-preview, #file-title").hide(); + provider.importPublic(importParameters, function(error, title, content) { + $("#wmd-preview, #file-title").show(); + if(error) { + return; + } + var fileDesc = fileMgr.createFile(title, content, undefined, true); + fileMgr.selectFile(fileDesc); + }); + }); + + return sharing; }); \ No newline at end of file diff --git a/js/ssh-helper.js b/js/ssh-helper.js index 52268fb0..36707dec 100644 --- a/js/ssh-helper.js +++ b/js/ssh-helper.js @@ -4,80 +4,80 @@ define([ "async-runner" ], function($, core, asyncRunner) { - var sshHelper = {}; + var sshHelper = {}; - // Only used to check the offline status - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - task.chain(); - }); - } + // Only used to check the offline status + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + task.chain(); + }); + } - sshHelper.upload = function(host, port, username, password, path, title, content, callback) { - var task = asyncRunner.createTask(); - connect(task); - task.onRun(function() { - var url = SSH_PROXY_URL + "upload"; - var data = { - host: host, - port: port, - username: username, - password: password, - path: path, - title: title, - content: content - }; - $.ajax({ - url : url, - data: data, - type: "POST", - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(response, textStatus, jqXHR) { - if(response.error === undefined) { - task.chain(); - return; - } - handleError(response.error, task); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - handleError(error, task); - }); - }); - task.onSuccess(function() { - callback(); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = undefined; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = "SSH error: " + error + "."; - } - else { - errorMsg = "Could not publish on SSH server."; - if (error.code <= 0) { - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + sshHelper.upload = function(host, port, username, password, path, title, content, callback) { + var task = asyncRunner.createTask(); + connect(task); + task.onRun(function() { + var url = SSH_PROXY_URL + "upload"; + var data = { + host: host, + port: port, + username: username, + password: password, + path: path, + title: title, + content: content + }; + $.ajax({ + url: url, + data: data, + type: "POST", + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(response, textStatus, jqXHR) { + if(response.error === undefined) { + task.chain(); + return; + } + handleError(response.error, task); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - return sshHelper; + function handleError(error, task) { + var errorMsg = undefined; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = "SSH error: " + error + "."; + } + else { + errorMsg = "Could not publish on SSH server."; + if(error.code <= 0) { + core.setOffline(); + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } + + return sshHelper; }); diff --git a/js/ssh-provider.js b/js/ssh-provider.js index c9500f7e..79e5dd1f 100644 --- a/js/ssh-provider.js +++ b/js/ssh-provider.js @@ -3,46 +3,35 @@ define([ "ssh-helper" ], function(utils, sshHelper) { - var PROVIDER_SSH = "ssh"; + var PROVIDER_SSH = "ssh"; - var sshProvider = { - providerId : PROVIDER_SSH, - providerName : "SSH server", - publishPreferencesInputIds: ["ssh-host", "ssh-port", "ssh-username", "ssh-password"] - }; + var sshProvider = { + providerId: PROVIDER_SSH, + providerName: "SSH server", + publishPreferencesInputIds: [ + "ssh-host", + "ssh-port", + "ssh-username", + "ssh-password" + ] + }; - sshProvider.publish = function(publishAttributes, title, content, callback) { - sshHelper.upload( - publishAttributes.host, - publishAttributes.port, - publishAttributes.username, - publishAttributes.password, - publishAttributes.path, - title, - content, - callback); - }; + sshProvider.publish = function(publishAttributes, title, content, callback) { + sshHelper.upload(publishAttributes.host, publishAttributes.port, publishAttributes.username, publishAttributes.password, publishAttributes.path, title, content, callback); + }; - sshProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.host = utils - .getInputTextValue( - "#input-publish-ssh-host", - event, - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); - publishAttributes.port = utils.getInputIntValue( - "#input-publish-ssh-port", undefined, 0); - publishAttributes.username = utils.getInputTextValue( - "#input-publish-ssh-username", event); - publishAttributes.password = utils.getInputTextValue( - "#input-publish-ssh-password", event); - publishAttributes.path = utils.getInputTextValue( - "#input-publish-file-path", event); - if (event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + sshProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.host = utils.getInputTextValue("#input-publish-ssh-host", event, /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); + publishAttributes.port = utils.getInputIntValue("#input-publish-ssh-port", undefined, 0); + publishAttributes.username = utils.getInputTextValue("#input-publish-ssh-username", event); + publishAttributes.password = utils.getInputTextValue("#input-publish-ssh-password", event); + publishAttributes.path = utils.getInputTextValue("#input-publish-file-path", event); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; - return sshProvider; + return sshProvider; }); \ No newline at end of file diff --git a/js/storage.js b/js/storage.js index 8f76b6dc..cb8072eb 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,131 +1,128 @@ // Setup an empty localStorage or upgrade an existing one define([ - "underscore" + "underscore" ], function(_) { - - // Create the file system if not exist - if (localStorage["file.list"] === undefined) { - localStorage["file.list"] = ";"; - } - var fileIndexList = _.compact(localStorage["file.list"].split(";")); - - // localStorage versioning - var version = localStorage["version"]; - - // Upgrade from v0 to v1 - if(version === undefined) { - - // Not used anymore - localStorage.removeItem("sync.queue"); - localStorage.removeItem("sync.current"); - localStorage.removeItem("file.counter"); - - _.each(fileIndexList, function(fileIndex) { - localStorage[fileIndex + ".publish"] = ";"; - var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";")); - _.each(syncIndexList, function(syncIndex) { - localStorage[syncIndex + ".contentCRC"] = "0"; - // We store title CRC only for Google Drive synchronization - if(localStorage[syncIndex + ".etag"] !== undefined) { - localStorage[syncIndex + ".titleCRC"] = "0"; - } - }); - }); - version = "v1"; - } - - // Upgrade from v1 to v2 - if(version == "v1") { - var gdriveLastChangeId = localStorage["sync.gdrive.lastChangeId"]; - if(gdriveLastChangeId) { - localStorage["gdrive.lastChangeId"] = gdriveLastChangeId; - localStorage.removeItem("sync.gdrive.lastChangeId"); - } - var dropboxLastChangeId = localStorage["sync.dropbox.lastChangeId"]; - if(dropboxLastChangeId) { - localStorage["dropbox.lastChangeId"] = dropboxLastChangeId; - localStorage.removeItem("sync.dropbox.lastChangeId"); - } - - var PROVIDER_GDRIVE = "gdrive"; - var PROVIDER_DROPBOX = "dropbox"; - var SYNC_PROVIDER_GDRIVE = "sync." + PROVIDER_GDRIVE + "."; - var SYNC_PROVIDER_DROPBOX = "sync." + PROVIDER_DROPBOX + "."; - _.each(fileIndexList, function(fileIndex) { - var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";")); - _.each(syncIndexList, function(syncIndex) { - var syncAttributes = {}; - if (syncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { - syncAttributes.provider = PROVIDER_GDRIVE; - syncAttributes.id = syncIndex.substring(SYNC_PROVIDER_GDRIVE.length); - syncAttributes.etag = localStorage[syncIndex + ".etag"]; - syncAttributes.contentCRC = localStorage[syncIndex + ".contentCRC"]; - syncAttributes.titleCRC = localStorage[syncIndex + ".titleCRC"]; - } - else if (syncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) { - syncAttributes.provider = PROVIDER_DROPBOX; - syncAttributes.path = decodeURIComponent(syncIndex.substring(SYNC_PROVIDER_DROPBOX.length)); - syncAttributes.version = localStorage[syncIndex + ".version"]; - syncAttributes.contentCRC = localStorage[syncIndex + ".contentCRC"]; - } - localStorage[syncIndex] = JSON.stringify(syncAttributes); - localStorage.removeItem(syncIndex + ".etag"); - localStorage.removeItem(syncIndex + ".version"); - localStorage.removeItem(syncIndex + ".contentCRC"); - localStorage.removeItem(syncIndex + ".titleCRC"); - }); - }); - version = "v2"; - } - - // Upgrade from v2 to v3 - if(version == "v2") { - _.each(fileIndexList, function(fileIndex) { - if(!_.has(localStorage, fileIndex + ".sync")) { - localStorage.removeItem(fileIndex + ".title"); - localStorage.removeItem(fileIndex + ".publish"); - localStorage.removeItem(fileIndex + ".content"); - localStorage["file.list"] = localStorage["file.list"].replace(";" - + fileIndex + ";", ";"); - } - }); - version = "v3"; - } - - // Upgrade from v3 to v4 - if(version == "v3") { - var currentFileIndex = localStorage["file.current"]; - if(currentFileIndex !== undefined && - localStorage["file.list"].indexOf(";" + currentFileIndex + ";") === -1) - { - localStorage.removeItem("file.current"); - } - version = "v4"; - } - - // Upgrade from v4 to v5 - if(version == "v4") { - // Recreate GitHub token - localStorage.removeItem("githubToken"); - version = "v5"; - } - - // Upgrade from v5 to v6 - if(version == "v5") { - _.each(fileIndexList, function(fileIndex) { - var publishIndexList = _.compact(localStorage[fileIndex + ".publish"].split(";")); - _.each(publishIndexList, function(publishIndex) { - var publishAttributes = JSON.parse(localStorage[publishIndex]); - if(publishAttributes.provider == "gdrive") { - // Change fileId to Id to be consistent with syncAttributes - publishAttributes.id = publishAttributes.fileId; - publishAttributes.fileId = undefined; - localStorage[publishIndex] = JSON.stringify(publishAttributes); - } - }); - }); - version = "v6"; - } - - localStorage["version"] = version; + + // Create the file system if not exist + if(localStorage["file.list"] === undefined) { + localStorage["file.list"] = ";"; + } + var fileIndexList = _.compact(localStorage["file.list"].split(";")); + + // localStorage versioning + var version = localStorage["version"]; + + // Upgrade from v0 to v1 + if(version === undefined) { + + // Not used anymore + localStorage.removeItem("sync.queue"); + localStorage.removeItem("sync.current"); + localStorage.removeItem("file.counter"); + + _.each(fileIndexList, function(fileIndex) { + localStorage[fileIndex + ".publish"] = ";"; + var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";")); + _.each(syncIndexList, function(syncIndex) { + localStorage[syncIndex + ".contentCRC"] = "0"; + // We store title CRC only for Google Drive synchronization + if(localStorage[syncIndex + ".etag"] !== undefined) { + localStorage[syncIndex + ".titleCRC"] = "0"; + } + }); + }); + version = "v1"; + } + + // Upgrade from v1 to v2 + if(version == "v1") { + var gdriveLastChangeId = localStorage["sync.gdrive.lastChangeId"]; + if(gdriveLastChangeId) { + localStorage["gdrive.lastChangeId"] = gdriveLastChangeId; + localStorage.removeItem("sync.gdrive.lastChangeId"); + } + var dropboxLastChangeId = localStorage["sync.dropbox.lastChangeId"]; + if(dropboxLastChangeId) { + localStorage["dropbox.lastChangeId"] = dropboxLastChangeId; + localStorage.removeItem("sync.dropbox.lastChangeId"); + } + + var PROVIDER_GDRIVE = "gdrive"; + var PROVIDER_DROPBOX = "dropbox"; + var SYNC_PROVIDER_GDRIVE = "sync." + PROVIDER_GDRIVE + "."; + var SYNC_PROVIDER_DROPBOX = "sync." + PROVIDER_DROPBOX + "."; + _.each(fileIndexList, function(fileIndex) { + var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";")); + _.each(syncIndexList, function(syncIndex) { + var syncAttributes = {}; + if(syncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { + syncAttributes.provider = PROVIDER_GDRIVE; + syncAttributes.id = syncIndex.substring(SYNC_PROVIDER_GDRIVE.length); + syncAttributes.etag = localStorage[syncIndex + ".etag"]; + syncAttributes.contentCRC = localStorage[syncIndex + ".contentCRC"]; + syncAttributes.titleCRC = localStorage[syncIndex + ".titleCRC"]; + } + else if(syncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) { + syncAttributes.provider = PROVIDER_DROPBOX; + syncAttributes.path = decodeURIComponent(syncIndex.substring(SYNC_PROVIDER_DROPBOX.length)); + syncAttributes.version = localStorage[syncIndex + ".version"]; + syncAttributes.contentCRC = localStorage[syncIndex + ".contentCRC"]; + } + localStorage[syncIndex] = JSON.stringify(syncAttributes); + localStorage.removeItem(syncIndex + ".etag"); + localStorage.removeItem(syncIndex + ".version"); + localStorage.removeItem(syncIndex + ".contentCRC"); + localStorage.removeItem(syncIndex + ".titleCRC"); + }); + }); + version = "v2"; + } + + // Upgrade from v2 to v3 + if(version == "v2") { + _.each(fileIndexList, function(fileIndex) { + if(!_.has(localStorage, fileIndex + ".sync")) { + localStorage.removeItem(fileIndex + ".title"); + localStorage.removeItem(fileIndex + ".publish"); + localStorage.removeItem(fileIndex + ".content"); + localStorage["file.list"] = localStorage["file.list"].replace(";" + fileIndex + ";", ";"); + } + }); + version = "v3"; + } + + // Upgrade from v3 to v4 + if(version == "v3") { + var currentFileIndex = localStorage["file.current"]; + if(currentFileIndex !== undefined && localStorage["file.list"].indexOf(";" + currentFileIndex + ";") === -1) { + localStorage.removeItem("file.current"); + } + version = "v4"; + } + + // Upgrade from v4 to v5 + if(version == "v4") { + // Recreate GitHub token + localStorage.removeItem("githubToken"); + version = "v5"; + } + + // Upgrade from v5 to v6 + if(version == "v5") { + _.each(fileIndexList, function(fileIndex) { + var publishIndexList = _.compact(localStorage[fileIndex + ".publish"].split(";")); + _.each(publishIndexList, function(publishIndex) { + var publishAttributes = JSON.parse(localStorage[publishIndex]); + if(publishAttributes.provider == "gdrive") { + // Change fileId to Id to be consistent with syncAttributes + publishAttributes.id = publishAttributes.fileId; + publishAttributes.fileId = undefined; + localStorage[publishIndex] = JSON.stringify(publishAttributes); + } + }); + }); + version = "v6"; + } + + localStorage["version"] = version; }); \ No newline at end of file diff --git a/js/synchronizer.js b/js/synchronizer.js index 929fb927..32d22411 100644 --- a/js/synchronizer.js +++ b/js/synchronizer.js @@ -9,250 +9,244 @@ define([ "dropbox-provider", "gdrive-provider" ], function($, _, core, utils, extensionMgr, fileSystem, fileMgr) { - - var synchronizer = {}; - - // Create a map with providerId: providerModule - var providerMap = _.chain( - arguments - ).map(function(argument) { - return argument && argument.providerId && [argument.providerId, argument]; - }).compact().object().value(); - - // Retrieve sync locations from localStorage - _.each(fileSystem, function(fileDesc) { - _.chain( - localStorage[fileDesc.fileIndex + ".sync"].split(";") - ).compact().each(function(syncIndex) { - var syncAttributes = JSON.parse(localStorage[syncIndex]); - // Store syncIndex - syncAttributes.syncIndex = syncIndex; - // Replace provider ID by provider module in attributes - syncAttributes.provider = providerMap[syncAttributes.provider]; - fileDesc.syncLocations[syncIndex] = syncAttributes; - }); - }); - // Force the synchronization - synchronizer.forceSync = function() { - lastSync = 0; - synchronizer.sync(); - }; - - // Recursive function to upload a single file on multiple locations - var uploadSyncAttributesList = []; - var uploadContent = undefined; - var uploadContentCRC = undefined; - var uploadTitle = undefined; - var uploadTitleCRC = undefined; - function locationUp(callback) { - - // No more synchronized location for this document - if (uploadSyncAttributesList.length === 0) { - fileUp(callback); - return; - } - - // Dequeue a synchronized location - var syncAttributes = uploadSyncAttributesList.pop(); - // Use the specified provider to perform the upload - syncAttributes.provider.syncUp( - uploadContent, - uploadContentCRC, - uploadTitle, - uploadTitleCRC, - syncAttributes, - function(error, uploadFlag) { - if(uploadFlag === true) { - // If uploadFlag is true, request another upload cycle - uploadCycle = true; - } - if(error) { - callback(error); - return; - } - if(uploadFlag) { - // Update syncAttributes in localStorage - utils.storeAttributes(syncAttributes); - } - locationUp(callback); - } - ); - } + var synchronizer = {}; - // Recursive function to upload multiple files - var uploadFileList = []; - function fileUp(callback) { - - // No more fileDesc to synchronize - if (uploadFileList.length === 0) { - syncUp(callback); - return; - } - - // Dequeue a fileDesc to synchronize - var fileDesc = uploadFileList.pop(); - uploadSyncAttributesList = _.values(fileDesc.syncLocations); - if(uploadSyncAttributesList.length === 0) { - fileUp(callback); - return; - } + // Create a map with providerId: providerModule + var providerMap = _.chain(arguments).map(function(argument) { + return argument && argument.providerId && [ + argument.providerId, + argument + ]; + }).compact().object().value(); - // Get document title/content - uploadContent = fileDesc.getContent(); - uploadContentCRC = utils.crc32(uploadContent); - uploadTitle = fileDesc.title; - uploadTitleCRC = utils.crc32(uploadTitle); - locationUp(callback); - } + // Retrieve sync locations from localStorage + _.each(fileSystem, function(fileDesc) { + _.chain(localStorage[fileDesc.fileIndex + ".sync"].split(";")).compact().each(function(syncIndex) { + var syncAttributes = JSON.parse(localStorage[syncIndex]); + // Store syncIndex + syncAttributes.syncIndex = syncIndex; + // Replace provider ID by provider module in attributes + syncAttributes.provider = providerMap[syncAttributes.provider]; + fileDesc.syncLocations[syncIndex] = syncAttributes; + }); + }); - // Entry point for up synchronization (upload changes) - var uploadCycle = false; - function syncUp(callback) { - if(uploadCycle === true) { - // New upload cycle - uploadCycle = false; - uploadFileList = _.values(fileSystem); - fileUp(callback); - } - else { - callback(); - } - } + // Force the synchronization + synchronizer.forceSync = function() { + lastSync = 0; + synchronizer.sync(); + }; - // Recursive function to download changes from multiple providers - var providerList = []; - function providerDown(callback) { - if(providerList.length === 0) { - callback(); - return; - } - var provider = providerList.pop(); - - // Check that provider has files to sync - if(!fileMgr.hasSync(provider)) { - providerDown(callback); - return; - } - - // Perform provider's syncDown - provider.syncDown(function(error) { - if(error) { - callback(error); - return; - } - providerDown(callback); - }); - } - - // Entry point for down synchronization (download changes) - function syncDown(callback) { - providerList = _.values(providerMap); - providerDown(callback); - }; - - // Main entry point for synchronization - var syncRunning = false; - var lastSync = 0; - synchronizer.sync = function() { - // If sync is already running or timeout is not reached or offline - if (syncRunning || lastSync + SYNC_PERIOD > utils.currentTime || core.isOffline) { - return; - } - syncRunning = true; - extensionMgr.onSyncRunning(true); - uploadCycle = true; - lastSync = utils.currentTime; - - function isError(error) { - if(error !== undefined) { - syncRunning = false; - extensionMgr.onSyncRunning(false); - return true; - } - return false; - } + // Recursive function to upload a single file on multiple locations + var uploadSyncAttributesList = []; + var uploadContent = undefined; + var uploadContentCRC = undefined; + var uploadTitle = undefined; + var uploadTitleCRC = undefined; + function locationUp(callback) { - syncDown(function(error) { - if(isError(error)) { - return; - } - syncUp(function(error) { - if(isError(error)) { - return; - } - syncRunning = false; - extensionMgr.onSyncRunning(false); - extensionMgr.onSyncSuccess(); - }); - }); - }; - // Run sync function periodically - if(viewerMode === false) { - core.addPeriodicCallback(synchronizer.sync); - } - - // Initialize the export dialog - function initExportDialog(provider) { - - // Reset fields - utils.resetModalInputs(); - - // Load preferences - var serializedPreferences = localStorage[provider.providerId + ".exportPreferences"]; - if(serializedPreferences) { - var exportPreferences = JSON.parse(serializedPreferences); - _.each(provider.exportPreferencesInputIds, function(inputId) { - utils.setInputValue("#input-sync-export-" + inputId, exportPreferences[inputId]); - }); - } - - // Open dialog box - $("#modal-upload-" + provider.providerId).modal(); - } - - core.onReady(function() { - // Init each provider - _.each(providerMap, function(provider) { - // Provider's import button - $(".action-sync-import-" + provider.providerId).click(function(event) { - provider.importFiles(event); - }); - // Provider's export action - $(".action-sync-export-dialog-" + provider.providerId).click(function() { - initExportDialog(provider); - }); - $(".action-sync-export-" + provider.providerId).click(function(event) { + // No more synchronized location for this document + if(uploadSyncAttributesList.length === 0) { + fileUp(callback); + return; + } - // Perform the provider's export - var fileDesc = fileMgr.getCurrentFile(); - provider.exportFile(event, fileDesc.title, fileDesc.getContent(), function(error, syncAttributes) { - if(error) { - return; - } - fileMgr.addSync(fileDesc, syncAttributes); - }); - - // Store input values as preferences for next time we open the export dialog - var exportPreferences = {}; - _.each(provider.exportPreferencesInputIds, function(inputId) { - exportPreferences[inputId] = $("#input-sync-export-" + inputId).val(); - }); - localStorage[provider.providerId + ".exportPreferences"] = JSON.stringify(exportPreferences); - }); - // Provider's manual export button - $(".action-sync-manual-" + provider.providerId).click(function(event) { - var fileDesc = fileMgr.getCurrentFile(); - provider.exportManual(event, fileDesc.title, fileDesc.getContent(), function(error, syncAttributes) { - if(error) { - return; - } - fileMgr.addSync(fileDesc, syncAttributes); - }); - }); - }); - }); + // Dequeue a synchronized location + var syncAttributes = uploadSyncAttributesList.pop(); + // Use the specified provider to perform the upload + syncAttributes.provider.syncUp(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, function(error, uploadFlag) { + if(uploadFlag === true) { + // If uploadFlag is true, request another upload cycle + uploadCycle = true; + } + if(error) { + callback(error); + return; + } + if(uploadFlag) { + // Update syncAttributes in localStorage + utils.storeAttributes(syncAttributes); + } + locationUp(callback); + }); + } - extensionMgr.onSynchronizerCreated(synchronizer); - return synchronizer; + // Recursive function to upload multiple files + var uploadFileList = []; + function fileUp(callback) { + + // No more fileDesc to synchronize + if(uploadFileList.length === 0) { + syncUp(callback); + return; + } + + // Dequeue a fileDesc to synchronize + var fileDesc = uploadFileList.pop(); + uploadSyncAttributesList = _.values(fileDesc.syncLocations); + if(uploadSyncAttributesList.length === 0) { + fileUp(callback); + return; + } + + // Get document title/content + uploadContent = fileDesc.content; + uploadContentCRC = utils.crc32(uploadContent); + uploadTitle = fileDesc.title; + uploadTitleCRC = utils.crc32(uploadTitle); + locationUp(callback); + } + + // Entry point for up synchronization (upload changes) + var uploadCycle = false; + function syncUp(callback) { + if(uploadCycle === true) { + // New upload cycle + uploadCycle = false; + uploadFileList = _.values(fileSystem); + fileUp(callback); + } + else { + callback(); + } + } + + // Recursive function to download changes from multiple providers + var providerList = []; + function providerDown(callback) { + if(providerList.length === 0) { + callback(); + return; + } + var provider = providerList.pop(); + + // Check that provider has files to sync + if(!fileMgr.hasSync(provider)) { + providerDown(callback); + return; + } + + // Perform provider's syncDown + provider.syncDown(function(error) { + if(error) { + callback(error); + return; + } + providerDown(callback); + }); + } + + // Entry point for down synchronization (download changes) + function syncDown(callback) { + providerList = _.values(providerMap); + providerDown(callback); + } + ; + + // Main entry point for synchronization + var syncRunning = false; + var lastSync = 0; + synchronizer.sync = function() { + // If sync is already running or timeout is not reached or offline + if(syncRunning || lastSync + SYNC_PERIOD > utils.currentTime || core.isOffline) { + return; + } + syncRunning = true; + extensionMgr.onSyncRunning(true); + uploadCycle = true; + lastSync = utils.currentTime; + + function isError(error) { + if(error !== undefined) { + syncRunning = false; + extensionMgr.onSyncRunning(false); + return true; + } + return false; + } + + syncDown(function(error) { + if(isError(error)) { + return; + } + syncUp(function(error) { + if(isError(error)) { + return; + } + syncRunning = false; + extensionMgr.onSyncRunning(false); + extensionMgr.onSyncSuccess(); + }); + }); + }; + // Run sync function periodically + if(viewerMode === false) { + core.addPeriodicCallback(synchronizer.sync); + } + + // Initialize the export dialog + function initExportDialog(provider) { + + // Reset fields + utils.resetModalInputs(); + + // Load preferences + var serializedPreferences = localStorage[provider.providerId + ".exportPreferences"]; + if(serializedPreferences) { + var exportPreferences = JSON.parse(serializedPreferences); + _.each(provider.exportPreferencesInputIds, function(inputId) { + utils.setInputValue("#input-sync-export-" + inputId, exportPreferences[inputId]); + }); + } + + // Open dialog box + $("#modal-upload-" + provider.providerId).modal(); + } + + core.onReady(function() { + // Init each provider + _.each(providerMap, function(provider) { + // Provider's import button + $(".action-sync-import-" + provider.providerId).click(function(event) { + provider.importFiles(event); + }); + // Provider's export action + $(".action-sync-export-dialog-" + provider.providerId).click(function() { + initExportDialog(provider); + }); + $(".action-sync-export-" + provider.providerId).click(function(event) { + + // Perform the provider's export + var fileDesc = fileMgr.getCurrentFile(); + provider.exportFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) { + if(error) { + return; + } + fileMgr.addSync(fileDesc, syncAttributes); + }); + + // Store input values as preferences for next time we open the + // export dialog + var exportPreferences = {}; + _.each(provider.exportPreferencesInputIds, function(inputId) { + exportPreferences[inputId] = $("#input-sync-export-" + inputId).val(); + }); + localStorage[provider.providerId + ".exportPreferences"] = JSON.stringify(exportPreferences); + }); + // Provider's manual export button + $(".action-sync-manual-" + provider.providerId).click(function(event) { + var fileDesc = fileMgr.getCurrentFile(); + provider.exportManual(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) { + if(error) { + return; + } + fileMgr.addSync(fileDesc, syncAttributes); + }); + }); + }); + }); + + extensionMgr.onSynchronizerCreated(synchronizer); + return synchronizer; }); diff --git a/js/tumblr-helper.js b/js/tumblr-helper.js index 647b4cef..0c9e6fb8 100644 --- a/js/tumblr-helper.js +++ b/js/tumblr-helper.js @@ -6,164 +6,163 @@ define([ "async-runner" ], function($, core, utils, extensionMgr, asyncRunner) { - var oauthParams = undefined; + var oauthParams = undefined; - var tumblrHelper = {}; + var tumblrHelper = {}; - // Only used to check the offline status - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - task.chain(); - }); - } + // Only used to check the offline status + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + task.chain(); + }); + } - // Try to authenticate with OAuth - function authenticate(task) { - var authWindow = undefined; - var intervalId = undefined; - task.onRun(function() { - if (oauthParams !== undefined) { - task.chain(); - return; - } - var serializedOauthParams = localStorage["tumblrOauthParams"]; - if(serializedOauthParams !== undefined) { - oauthParams = JSON.parse(serializedOauthParams); - task.chain(); - return; - } - extensionMgr.onMessage("Please make sure the Tumblr authorization popup is not blocked by your browser."); - var errorMsg = "Failed to retrieve a token from Tumblr."; - // We add time for user to enter his credentials - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - var oauth_object = undefined; - function getOauthToken() { - $.getJSON(TUMBLR_PROXY_URL + "request_token", function(data) { - if(data.oauth_token !== undefined) { - oauth_object = data; - task.chain(getVerifier); - } - else { - task.error(new Error(errorMsg)); - } - }); - } - function getVerifier() { - localStorage.removeItem("tumblrVerifier"); - authWindow = utils.popupWindow( - 'tumblr-oauth-client.html?oauth_token=' + oauth_object.oauth_token, - 'stackedit-tumblr-oauth', 800, 600); - authWindow.focus(); - intervalId = setInterval(function() { - if(authWindow.closed === true) { - clearInterval(intervalId); - authWindow = undefined; - intervalId = undefined; - oauth_object.oauth_verifier = localStorage["tumblrVerifier"]; - if(oauth_object.oauth_verifier === undefined) { - task.error(new Error(errorMsg)); - return; - } - localStorage.removeItem("tumblrVerifier"); - task.chain(getAccessToken); - } - }, 500); - } - function getAccessToken() { - $.getJSON(TUMBLR_PROXY_URL + "access_token", oauth_object, function(data) { - if(data.access_token !== undefined && data.access_token_secret !== undefined) { - localStorage["tumblrOauthParams"] = JSON.stringify(data); - oauthParams = data; - task.chain(); - } - else { - task.error(new Error(errorMsg)); - } - }); - } - task.chain(getOauthToken); - }); - task.onError(function() { - if(intervalId !== undefined) { - clearInterval(intervalId); - } - if(authWindow !== undefined) { - authWindow.close(); - } - }); - } + // Try to authenticate with OAuth + function authenticate(task) { + var authWindow = undefined; + var intervalId = undefined; + task.onRun(function() { + if(oauthParams !== undefined) { + task.chain(); + return; + } + var serializedOauthParams = localStorage["tumblrOauthParams"]; + if(serializedOauthParams !== undefined) { + oauthParams = JSON.parse(serializedOauthParams); + task.chain(); + return; + } + extensionMgr.onMessage("Please make sure the Tumblr authorization popup is not blocked by your browser."); + var errorMsg = "Failed to retrieve a token from Tumblr."; + // We add time for user to enter his credentials + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + var oauth_object = undefined; + function getOauthToken() { + $.getJSON(TUMBLR_PROXY_URL + "request_token", function(data) { + if(data.oauth_token !== undefined) { + oauth_object = data; + task.chain(getVerifier); + } + else { + task.error(new Error(errorMsg)); + } + }); + } + function getVerifier() { + localStorage.removeItem("tumblrVerifier"); + authWindow = utils.popupWindow('tumblr-oauth-client.html?oauth_token=' + oauth_object.oauth_token, 'stackedit-tumblr-oauth', 800, 600); + authWindow.focus(); + intervalId = setInterval(function() { + if(authWindow.closed === true) { + clearInterval(intervalId); + authWindow = undefined; + intervalId = undefined; + oauth_object.oauth_verifier = localStorage["tumblrVerifier"]; + if(oauth_object.oauth_verifier === undefined) { + task.error(new Error(errorMsg)); + return; + } + localStorage.removeItem("tumblrVerifier"); + task.chain(getAccessToken); + } + }, 500); + } + function getAccessToken() { + $.getJSON(TUMBLR_PROXY_URL + "access_token", oauth_object, function(data) { + if(data.access_token !== undefined && data.access_token_secret !== undefined) { + localStorage["tumblrOauthParams"] = JSON.stringify(data); + oauthParams = data; + task.chain(); + } + else { + task.error(new Error(errorMsg)); + } + }); + } + task.chain(getOauthToken); + }); + task.onError(function() { + if(intervalId !== undefined) { + clearInterval(intervalId); + } + if(authWindow !== undefined) { + authWindow.close(); + } + }); + } - tumblrHelper.upload = function(blogHostname, postId, tags, format, title, content, callback) { - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var data = $.extend({ - blog_hostname: blogHostname, - post_id: postId, - tags: tags, - format: format, - title: title, - content: content - }, oauthParams); - $.ajax({ - url : TUMBLR_PROXY_URL + "post", - data: data, - type: "POST", - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(post, textStatus, jqXHR) { - postId = post.id; - task.chain(); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - // Handle error - if(error.code === 404 && postId !== undefined) { - error = 'Post ' + postId + ' not found on Tumblr.|removePublish'; - } - handleError(error, task); - }); - }); - task.onSuccess(function() { - callback(undefined, postId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = undefined; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = error; - } - else { - errorMsg = "Could not publish on Tumblr."; - if (error.code === 401 || error.code === 403) { - oauthParams = undefined; - localStorage.removeItem("tumblrOauthParams"); - errorMsg = "Access to Tumblr account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; - } else if (error.code <= 0) { - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + tumblrHelper.upload = function(blogHostname, postId, tags, format, title, content, callback) { + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var data = $.extend({ + blog_hostname: blogHostname, + post_id: postId, + tags: tags, + format: format, + title: title, + content: content + }, oauthParams); + $.ajax({ + url: TUMBLR_PROXY_URL + "post", + data: data, + type: "POST", + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(post, textStatus, jqXHR) { + postId = post.id; + task.chain(); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + // Handle error + if(error.code === 404 && postId !== undefined) { + error = 'Post ' + postId + ' not found on Tumblr.|removePublish'; + } + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, postId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - return tumblrHelper; + function handleError(error, task) { + var errorMsg = undefined; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Could not publish on Tumblr."; + if(error.code === 401 || error.code === 403) { + oauthParams = undefined; + localStorage.removeItem("tumblrOauthParams"); + errorMsg = "Access to Tumblr account is not authorized."; + task.retry(new Error(errorMsg), 1); + return; + } + else if(error.code <= 0) { + core.setOffline(); + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } + + return tumblrHelper; }); diff --git a/js/tumblr-provider.js b/js/tumblr-provider.js index 39feba95..9b0977d1 100644 --- a/js/tumblr-provider.js +++ b/js/tumblr-provider.js @@ -2,48 +2,38 @@ define([ "utils", "tumblr-helper" ], function(utils, tumblrHelper) { - - var PROVIDER_TUMBLR = "tumblr"; - - var tumblrProvider = { - providerId: PROVIDER_TUMBLR, - providerName: "Tumblr", - publishPreferencesInputIds: ["tumblr-hostname"] - }; - - tumblrProvider.publish = function(publishAttributes, title, content, callback) { - tumblrHelper.upload( - publishAttributes.blogHostname, - publishAttributes.postId, - publishAttributes.tags, - publishAttributes.format == "markdown" ? "markdown" : "html", - title, - content, - function(error, postId) { - if(error) { - callback(error); - return; - } - publishAttributes.postId = postId; - callback(); - } - ); - }; - tumblrProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.blogHostname = utils - .getInputTextValue( - "#input-publish-tumblr-hostname", - event, - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); - publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); - publishAttributes.tags = utils.getInputTextValue("#input-publish-tags"); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + var PROVIDER_TUMBLR = "tumblr"; - return tumblrProvider; + var tumblrProvider = { + providerId: PROVIDER_TUMBLR, + providerName: "Tumblr", + publishPreferencesInputIds: [ + "tumblr-hostname" + ] + }; + + tumblrProvider.publish = function(publishAttributes, title, content, callback) { + tumblrHelper.upload(publishAttributes.blogHostname, publishAttributes.postId, publishAttributes.tags, publishAttributes.format == "markdown" ? "markdown" : "html", title, content, function(error, postId) { + if(error) { + callback(error); + return; + } + publishAttributes.postId = postId; + callback(); + }); + }; + + tumblrProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.blogHostname = utils.getInputTextValue("#input-publish-tumblr-hostname", event, /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); + publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); + publishAttributes.tags = utils.getInputTextValue("#input-publish-tags"); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + return tumblrProvider; }); \ No newline at end of file diff --git a/js/utils.js b/js/utils.js index d12072d2..d7acfa14 100644 --- a/js/utils.js +++ b/js/utils.js @@ -3,311 +3,518 @@ define([ "underscore", "lib/FileSaver" ], function($, _) { - - var utils = {}; - // Return a parameter from the URL - utils.getURLParameter = function(name) { - var regex = new RegExp(name + "=(.+?)(&|$)"); - try { - return decodeURIComponent(regex.exec(location.search)[1]); - } catch (e) { - return undefined; - } - }; - - // Transform a selector into a jQuery object - function jqElt(element) { - if(_.isString(element)) { - return $(element); - } - return element; - } - - // For input control - function inputError(element, event) { - if(event !== undefined) { - element.stop(true, true).addClass("error").delay(1000).switchClass("error"); - event.stopPropagation(); - } - } - - // Return input value - utils.getInputValue = function(element) { - element = jqElt(element); - return element.val(); - }; - - // Set input value - utils.setInputValue = function(element, value) { - element = jqElt(element); - element.val(value); - }; - - // Return input text value - utils.getInputTextValue = function(element, event, validationRegex) { - element = jqElt(element); - var value = element.val(); - if (value === undefined) { - inputError(element, event); - return undefined; - } - // trim - value = utils.trim(value); - if((value.length === 0) - || (validationRegex !== undefined && !value.match(validationRegex))) { - inputError(element, event); - return undefined; - } - return value; - }; - - // Return input integer value - utils.getInputIntValue = function(element, event, min, max) { - element = jqElt(element); - var value = utils.getInputTextValue(element, event); - if(value === undefined) { - return undefined; - } - value = parseInt(value); - if((value === NaN) - || (min !== undefined && value < min) - || (max !== undefined && value > max)) { - inputError(element, event); - return undefined; - } - return value; - }; - - // Return checkbox boolean value - utils.getInputChecked = function(element) { - element = jqElt(element); - return element.prop("checked"); - }; - - // Set checkbox state - utils.setInputChecked = function(element, checked) { - element = jqElt(element); - element.prop("checked", checked); - }; - - // Get radio button value - utils.getInputRadio = function(name) { - return $("input:radio[name=" + name + "]:checked").prop("value"); - }; - - // Set radio button value - utils.setInputRadio = function(name, value) { - $("input:radio[name=" + name + "][value=" + value + "]").prop("checked", true); - }; - - // Reset input control in all modals - utils.resetModalInputs = function() { - $(".modal input[type=text]:not([disabled]), .modal input[type=password]").val(""); - }; - - // Basic trim function - utils.trim = function(str) { - return $.trim(str); - }; - - // Slug function - utils.slugify = function(text) { - return text.toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with - - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '-') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text - }; - - // Check an URL - utils.checkUrl = function(url, addSlash) { - if(!url) { - return url; - } - if(url.indexOf("http") !== 0) { - url = "http://" + url; - } - if(addSlash && url.indexOf("/", url.length - 1) === -1) { - url += "/"; - } - return url; - }; - - // Base64 conversion - utils.encodeBase64 = function(str) { - if (str.length === 0) { - return ""; - } - - // UTF-8 to byte array - var bytes = [], offset = 0, length, char; + var utils = {}; - str = encodeURI(str); - length = str.length; + // Return a parameter from the URL + utils.getURLParameter = function(name) { + var regex = new RegExp(name + "=(.+?)(&|$)"); + try { + return decodeURIComponent(regex.exec(location.search)[1]); + } + catch (e) { + return undefined; + } + }; - while (offset < length) { - char = str[offset]; - offset += 1; + // Transform a selector into a jQuery object + function jqElt(element) { + if(_.isString(element)) { + return $(element); + } + return element; + } - if ('%' !== char) { - bytes.push(char.charCodeAt(0)); - } else { - char = str[offset] + str[offset + 1]; - bytes.push(parseInt(char, 16)); - offset += 2; - } - } + // For input control + function inputError(element, event) { + if(event !== undefined) { + element.stop(true, true).addClass("error").delay(1000).switchClass("error"); + event.stopPropagation(); + } + } - // byte array to base64 - var padchar = '='; - var alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Return input value + utils.getInputValue = function(element) { + element = jqElt(element); + return element.val(); + }; - var i, b10; - var x = []; + // Set input value + utils.setInputValue = function(element, value) { + element = jqElt(element); + element.val(value); + }; - var imax = bytes.length - bytes.length % 3; + // Return input text value + utils.getInputTextValue = function(element, event, validationRegex) { + element = jqElt(element); + var value = element.val(); + if(value === undefined) { + inputError(element, event); + return undefined; + } + // trim + value = utils.trim(value); + if((value.length === 0) || (validationRegex !== undefined && !value.match(validationRegex))) { + inputError(element, event); + return undefined; + } + return value; + }; - for (i = 0; i < imax; i += 3) { - b10 = (bytes[i] << 16) | (bytes[i+1] << 8) | bytes[i+2]; - x.push(alpha.charAt(b10 >> 18)); - x.push(alpha.charAt((b10 >> 12) & 0x3F)); - x.push(alpha.charAt((b10 >> 6) & 0x3f)); - x.push(alpha.charAt(b10 & 0x3f)); - } - switch (bytes.length - imax) { - case 1: - b10 = bytes[i] << 16; - x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + - padchar + padchar); - break; - case 2: - b10 = (bytes[i] << 16) | (bytes[i+1] << 8); - x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + - alpha.charAt((b10 >> 6) & 0x3f) + padchar); - break; - } - return x.join(''); - }; - - // CRC32 algorithm - var mHash = [ 0, 1996959894, 3993919788, 2567524794, 124634137, - 1886057615, 3915621685, 2657392035, 249268274, 2044508324, - 3772115230, 2547177864, 162941995, 2125561021, 3887607047, - 2428444049, 498536548, 1789927666, 4089016648, 2227061214, - 450548861, 1843258603, 4107580753, 2211677639, 325883990, - 1684777152, 4251122042, 2321926636, 335633487, 1661365465, - 4195302755, 2366115317, 997073096, 1281953886, 3579855332, - 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, - 901097722, 1119000684, 3686517206, 2898065728, 853044451, - 1172266101, 3705015759, 2882616665, 651767980, 1373503546, - 3369554304, 3218104598, 565507253, 1454621731, 3485111705, - 3099436303, 671266974, 1594198024, 3322730930, 2970347812, - 795835527, 1483230225, 3244367275, 3060149565, 1994146192, - 31158534, 2563907772, 4023717930, 1907459465, 112637215, - 2680153253, 3904427059, 2013776290, 251722036, 2517215374, - 3775830040, 2137656763, 141376813, 2439277719, 3865271297, - 1802195444, 476864866, 2238001368, 4066508878, 1812370925, - 453092731, 2181625025, 4111451223, 1706088902, 314042704, - 2344532202, 4240017532, 1658658271, 366619977, 2362670323, - 4224994405, 1303535960, 984961486, 2747007092, 3569037538, - 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, - 879679996, 2909243462, 3663771856, 1141124467, 855842277, - 2852801631, 3708648649, 1342533948, 654459306, 3188396048, - 3373015174, 1466479909, 544179635, 3110523913, 3462522015, - 1591671054, 702138776, 2966460450, 3352799412, 1504918807, - 783551873, 3082640443, 3233442989, 3988292384, 2596254646, - 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, - 3814918930, 2489596804, 225274430, 2053790376, 3826175755, - 2466906013, 167816743, 2097651377, 4027552580, 2265490386, - 503444072, 1762050814, 4150417245, 2154129355, 426522225, - 1852507879, 4275313526, 2312317920, 282753626, 1742555852, - 4189708143, 2394877945, 397917763, 1622183637, 3604390888, - 2714866558, 953729732, 1340076626, 3518719985, 2797360999, - 1068828381, 1219638859, 3624741850, 2936675148, 906185462, - 1090812512, 3747672003, 2825379669, 829329135, 1181335161, - 3412177804, 3160834842, 628085408, 1382605366, 3423369109, - 3138078467, 570562233, 1426400815, 3317316542, 2998733608, - 733239954, 1555261956, 3268935591, 3050360625, 752459403, - 1541320221, 2607071920, 3965973030, 1969922972, 40735498, - 2617837225, 3943577151, 1913087877, 83908371, 2512341634, - 3803740692, 2075208622, 213261112, 2463272603, 3855990285, - 2094854071, 198958881, 2262029012, 4057260610, 1759359992, - 534414190, 2176718541, 4139329115, 1873836001, 414664567, - 2282248934, 4279200368, 1711684554, 285281116, 2405801727, - 4167216745, 1634467795, 376229701, 2685067896, 3608007406, - 1308918612, 956543938, 2808555105, 3495958263, 1231636301, - 1047427035, 2932959818, 3654703836, 1088359270, 936918000, - 2847714899, 3736837829, 1202900863, 817233897, 3183342108, - 3401237130, 1404277552, 615818150, 3134207493, 3453421203, - 1423857449, 601450431, 3009837614, 3294710456, 1567103746, - 711928724, 3020668471, 3272380065, 1510334235, 755167117 ]; - utils.crc32 = function(str) { - var n = 0, crc = -1; - for ( var i = 0; i < str.length; i++) { - n = (crc ^ str.charCodeAt(i)) & 0xFF; - crc = (crc >>> 8) ^ mHash[n]; - } - crc = crc ^ (-1); - if (crc < 0) { - crc = 0xFFFFFFFF + crc + 1; - } - return crc.toString(16); - }; - - // Create an centered popup window - utils.popupWindow = function(url, title, width, height) { - var left = (screen.width / 2) - (width / 2); - var top = (screen.height / 2) - (height / 2); - return window.open( - url, - title, - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=' - + width - + ', height=' - + height - + ', top=' - + top - + ', left=' - + left); - }; - - // Export data on disk - utils.saveAs = function(content, filename) { - if(saveAs !== undefined) { - var blob = new Blob([content], {type: "text/plain;charset=utf-8"}); - saveAs(blob, filename); - } - else { - var uriContent = "data:application/octet-stream;base64," - + utils.encodeBase64(content); - window.open(uriContent, 'file'); - } - }; + // Return input integer value + utils.getInputIntValue = function(element, event, min, max) { + element = jqElt(element); + var value = utils.getInputTextValue(element, event); + if(value === undefined) { + return undefined; + } + value = parseInt(value); + if((value === NaN) || (min !== undefined && value < min) || (max !== undefined && value > max)) { + inputError(element, event); + return undefined; + } + return value; + }; - // Generates a random string - utils.randomString = function() { - return _.random(4294967296).toString(36); - }; - - // Time shared by others modules - utils.updateCurrentTime = function() { - utils.currentTime = new Date().getTime(); - }; - utils.updateCurrentTime(); - - - // Serialize sync/publish attributes and store it in the fileStorage - utils.storeAttributes = function(attributes) { - var storeIndex = attributes.syncIndex || attributes.publishIndex; - // Don't store sync/publish index - attributes = _.omit(attributes, "syncIndex", "publishIndex"); - // Store providerId instead of provider - attributes.provider = attributes.provider.providerId; - localStorage[storeIndex] = JSON.stringify(attributes); - }; + // Return checkbox boolean value + utils.getInputChecked = function(element) { + element = jqElt(element); + return element.prop("checked"); + }; - return utils; + // Set checkbox state + utils.setInputChecked = function(element, checked) { + element = jqElt(element); + element.prop("checked", checked); + }; + // Get radio button value + utils.getInputRadio = function(name) { + return $("input:radio[name=" + name + "]:checked").prop("value"); + }; + + // Set radio button value + utils.setInputRadio = function(name, value) { + $("input:radio[name=" + name + "][value=" + value + "]").prop("checked", true); + }; + + // Reset input control in all modals + utils.resetModalInputs = function() { + $(".modal input[type=text]:not([disabled]), .modal input[type=password]").val(""); + }; + + // Basic trim function + utils.trim = function(str) { + return $.trim(str); + }; + + // Slug function + utils.slugify = function(text) { + return text.toLowerCase().replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + }; + + // Check an URL + utils.checkUrl = function(url, addSlash) { + if(!url) { + return url; + } + if(url.indexOf("http") !== 0) { + url = "http://" + url; + } + if(addSlash && url.indexOf("/", url.length - 1) === -1) { + url += "/"; + } + return url; + }; + + // Create an centered popup window + utils.popupWindow = function(url, title, width, height) { + var left = (screen.width / 2) - (width / 2); + var top = (screen.height / 2) - (height / 2); + return window.open(url, title, [ + 'toolbar=no, ', + 'location=no, ', + 'directories=no, ', + 'status=no, ', + 'menubar=no, ', + 'scrollbars=no, ', + 'resizable=no, ', + 'copyhistory=no, ', + 'width=' + width + ', ', + 'height=' + height + ', ', + 'top=' + top + ', ', + 'left=' + left + ].join("")); + }; + + // Export data on disk + utils.saveAs = function(content, filename) { + if(saveAs !== undefined) { + var blob = new Blob([ + content + ], { + type: "text/plain;charset=utf-8" + }); + saveAs(blob, filename); + } + else { + var uriContent = "data:application/octet-stream;base64," + utils.encodeBase64(content); + window.open(uriContent, 'file'); + } + }; + + // Generates a random string + utils.randomString = function() { + return _.random(4294967296).toString(36); + }; + + // Time shared by others modules + utils.updateCurrentTime = function() { + utils.currentTime = new Date().getTime(); + }; + utils.updateCurrentTime(); + + // Serialize sync/publish attributes and store it in the fileStorage + utils.storeAttributes = function(attributes) { + var storeIndex = attributes.syncIndex || attributes.publishIndex; + // Don't store sync/publish index + attributes = _.omit(attributes, "syncIndex", "publishIndex"); + // Store providerId instead of provider + attributes.provider = attributes.provider.providerId; + localStorage[storeIndex] = JSON.stringify(attributes); + }; + + // Base64 conversion + utils.encodeBase64 = function(str) { + if(str.length === 0) { + return ""; + } + + // UTF-8 to byte array + var bytes = [], offset = 0, length, char; + + str = encodeURI(str); + length = str.length; + + while (offset < length) { + char = str[offset]; + offset += 1; + + if('%' !== char) { + bytes.push(char.charCodeAt(0)); + } + else { + char = str[offset] + str[offset + 1]; + bytes.push(parseInt(char, 16)); + offset += 2; + } + } + + // byte array to base64 + var padchar = '='; + var alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + var i, b10; + var x = []; + + var imax = bytes.length - bytes.length % 3; + + for (i = 0; i < imax; i += 3) { + b10 = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + x.push(alpha.charAt(b10 >> 18)); + x.push(alpha.charAt((b10 >> 12) & 0x3F)); + x.push(alpha.charAt((b10 >> 6) & 0x3f)); + x.push(alpha.charAt(b10 & 0x3f)); + } + switch (bytes.length - imax) { + case 1: + b10 = bytes[i] << 16; + x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + padchar + padchar); + break; + case 2: + b10 = (bytes[i] << 16) | (bytes[i + 1] << 8); + x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + alpha.charAt((b10 >> 6) & 0x3f) + padchar); + break; + } + return x.join(''); + }; + + // CRC32 algorithm + var mHash = [ + 0, + 1996959894, + 3993919788, + 2567524794, + 124634137, + 1886057615, + 3915621685, + 2657392035, + 249268274, + 2044508324, + 3772115230, + 2547177864, + 162941995, + 2125561021, + 3887607047, + 2428444049, + 498536548, + 1789927666, + 4089016648, + 2227061214, + 450548861, + 1843258603, + 4107580753, + 2211677639, + 325883990, + 1684777152, + 4251122042, + 2321926636, + 335633487, + 1661365465, + 4195302755, + 2366115317, + 997073096, + 1281953886, + 3579855332, + 2724688242, + 1006888145, + 1258607687, + 3524101629, + 2768942443, + 901097722, + 1119000684, + 3686517206, + 2898065728, + 853044451, + 1172266101, + 3705015759, + 2882616665, + 651767980, + 1373503546, + 3369554304, + 3218104598, + 565507253, + 1454621731, + 3485111705, + 3099436303, + 671266974, + 1594198024, + 3322730930, + 2970347812, + 795835527, + 1483230225, + 3244367275, + 3060149565, + 1994146192, + 31158534, + 2563907772, + 4023717930, + 1907459465, + 112637215, + 2680153253, + 3904427059, + 2013776290, + 251722036, + 2517215374, + 3775830040, + 2137656763, + 141376813, + 2439277719, + 3865271297, + 1802195444, + 476864866, + 2238001368, + 4066508878, + 1812370925, + 453092731, + 2181625025, + 4111451223, + 1706088902, + 314042704, + 2344532202, + 4240017532, + 1658658271, + 366619977, + 2362670323, + 4224994405, + 1303535960, + 984961486, + 2747007092, + 3569037538, + 1256170817, + 1037604311, + 2765210733, + 3554079995, + 1131014506, + 879679996, + 2909243462, + 3663771856, + 1141124467, + 855842277, + 2852801631, + 3708648649, + 1342533948, + 654459306, + 3188396048, + 3373015174, + 1466479909, + 544179635, + 3110523913, + 3462522015, + 1591671054, + 702138776, + 2966460450, + 3352799412, + 1504918807, + 783551873, + 3082640443, + 3233442989, + 3988292384, + 2596254646, + 62317068, + 1957810842, + 3939845945, + 2647816111, + 81470997, + 1943803523, + 3814918930, + 2489596804, + 225274430, + 2053790376, + 3826175755, + 2466906013, + 167816743, + 2097651377, + 4027552580, + 2265490386, + 503444072, + 1762050814, + 4150417245, + 2154129355, + 426522225, + 1852507879, + 4275313526, + 2312317920, + 282753626, + 1742555852, + 4189708143, + 2394877945, + 397917763, + 1622183637, + 3604390888, + 2714866558, + 953729732, + 1340076626, + 3518719985, + 2797360999, + 1068828381, + 1219638859, + 3624741850, + 2936675148, + 906185462, + 1090812512, + 3747672003, + 2825379669, + 829329135, + 1181335161, + 3412177804, + 3160834842, + 628085408, + 1382605366, + 3423369109, + 3138078467, + 570562233, + 1426400815, + 3317316542, + 2998733608, + 733239954, + 1555261956, + 3268935591, + 3050360625, + 752459403, + 1541320221, + 2607071920, + 3965973030, + 1969922972, + 40735498, + 2617837225, + 3943577151, + 1913087877, + 83908371, + 2512341634, + 3803740692, + 2075208622, + 213261112, + 2463272603, + 3855990285, + 2094854071, + 198958881, + 2262029012, + 4057260610, + 1759359992, + 534414190, + 2176718541, + 4139329115, + 1873836001, + 414664567, + 2282248934, + 4279200368, + 1711684554, + 285281116, + 2405801727, + 4167216745, + 1634467795, + 376229701, + 2685067896, + 3608007406, + 1308918612, + 956543938, + 2808555105, + 3495958263, + 1231636301, + 1047427035, + 2932959818, + 3654703836, + 1088359270, + 936918000, + 2847714899, + 3736837829, + 1202900863, + 817233897, + 3183342108, + 3401237130, + 1404277552, + 615818150, + 3134207493, + 3453421203, + 1423857449, + 601450431, + 3009837614, + 3294710456, + 1567103746, + 711928724, + 3020668471, + 3272380065, + 1510334235, + 755167117 + ]; + utils.crc32 = function(str) { + var n = 0, crc = -1; + for ( var i = 0; i < str.length; i++) { + n = (crc ^ str.charCodeAt(i)) & 0xFF; + crc = (crc >>> 8) ^ mHash[n]; + } + crc = crc ^ (-1); + if(crc < 0) { + crc = 0xFFFFFFFF + crc + 1; + } + return crc.toString(16); + }; + + return utils; }); diff --git a/js/wordpress-helper.js b/js/wordpress-helper.js index 2a10eab7..f79c694e 100644 --- a/js/wordpress-helper.js +++ b/js/wordpress-helper.js @@ -6,161 +6,160 @@ define([ "async-runner" ], function($, core, utils, extensionMgr, asyncRunner) { - var token = undefined; + var token = undefined; - var wordpressHelper = {}; + var wordpressHelper = {}; - // Only used to check the offline status - function connect(task) { - task.onRun(function() { - if(core.isOffline === true) { - task.error(new Error("Operation not available in offline mode.|stopPublish")); - return; - } - task.chain(); - }); - } + // Only used to check the offline status + function connect(task) { + task.onRun(function() { + if(core.isOffline === true) { + task.error(new Error("Operation not available in offline mode.|stopPublish")); + return; + } + task.chain(); + }); + } - // Try to authenticate with OAuth - function authenticate(task) { - var authWindow = undefined; - var intervalId = undefined; - task.onRun(function() { - token = localStorage["wordpressToken"]; - if(token !== undefined) { - task.chain(); - return; - } - extensionMgr.onMessage("Please make sure the Wordpress authorization popup is not blocked by your browser."); - var errorMsg = "Failed to retrieve a token from Wordpress."; - // We add time for user to enter his credentials - task.timeout = ASYNC_TASK_LONG_TIMEOUT; - var code = undefined; - function getCode() { - localStorage.removeItem("wordpressCode"); - authWindow = utils.popupWindow( - 'wordpress-oauth-client.html?client_id=' + WORDPRESS_CLIENT_ID, - 'stackedit-wordpress-oauth', 960, 600); - authWindow.focus(); - intervalId = setInterval(function() { - if(authWindow.closed === true) { - clearInterval(intervalId); - authWindow = undefined; - intervalId = undefined; - code = localStorage["wordpressCode"]; - if(code === undefined) { - task.error(new Error(errorMsg)); - return; - } - localStorage.removeItem("wordpressCode"); - task.chain(getToken); - } - }, 500); - } - function getToken() { - $.getJSON(WORDPRESS_PROXY_URL + "authenticate/" + code, function(data) { - if(data.token !== undefined) { - token = data.token; - localStorage["wordpressToken"] = token; - task.chain(); - } - else { - task.error(new Error(errorMsg)); - } - }); - } - task.chain(getCode); - }); - task.onError(function() { - if(intervalId !== undefined) { - clearInterval(intervalId); - } - if(authWindow !== undefined) { - authWindow.close(); - } - }); - } + // Try to authenticate with OAuth + function authenticate(task) { + var authWindow = undefined; + var intervalId = undefined; + task.onRun(function() { + token = localStorage["wordpressToken"]; + if(token !== undefined) { + task.chain(); + return; + } + extensionMgr.onMessage("Please make sure the Wordpress authorization popup is not blocked by your browser."); + var errorMsg = "Failed to retrieve a token from Wordpress."; + // We add time for user to enter his credentials + task.timeout = ASYNC_TASK_LONG_TIMEOUT; + var code = undefined; + function getCode() { + localStorage.removeItem("wordpressCode"); + authWindow = utils.popupWindow('wordpress-oauth-client.html?client_id=' + WORDPRESS_CLIENT_ID, 'stackedit-wordpress-oauth', 960, 600); + authWindow.focus(); + intervalId = setInterval(function() { + if(authWindow.closed === true) { + clearInterval(intervalId); + authWindow = undefined; + intervalId = undefined; + code = localStorage["wordpressCode"]; + if(code === undefined) { + task.error(new Error(errorMsg)); + return; + } + localStorage.removeItem("wordpressCode"); + task.chain(getToken); + } + }, 500); + } + function getToken() { + $.getJSON(WORDPRESS_PROXY_URL + "authenticate/" + code, function(data) { + if(data.token !== undefined) { + token = data.token; + localStorage["wordpressToken"] = token; + task.chain(); + } + else { + task.error(new Error(errorMsg)); + } + }); + } + task.chain(getCode); + }); + task.onError(function() { + if(intervalId !== undefined) { + clearInterval(intervalId); + } + if(authWindow !== undefined) { + authWindow.close(); + } + }); + } - wordpressHelper.upload = function(site, postId, tags, title, content, callback) { - var task = asyncRunner.createTask(); - connect(task); - authenticate(task); - task.onRun(function() { - var url = WORDPRESS_PROXY_URL + "post"; - var data = { - token: token, - site: site, - postId: postId, - tags: tags, - title: title, - content: content - }; - $.ajax({ - url : url, - data: data, - type: "POST", - dataType : "json", - timeout : AJAX_TIMEOUT - }).done(function(response, textStatus, jqXHR) { - if(response.body.ID) { - postId = response.body.ID; - task.chain(); - return; - } - var error = { - code: response.code, - message: response.body.error - }; - // Handle error - if(error.code === 404) { - if(error.message == "unknown_blog") { - error = 'Site "' + site + '" not found on WordPress.|removePublish'; - } - else if(error.message == "unknown_post"){ - error = 'Post ' + postId + ' not found on WordPress.|removePublish'; - } - } - handleError(error, task); - }).fail(function(jqXHR) { - var error = { - code: jqXHR.status, - message: jqXHR.statusText - }; - handleError(error, task); - }); - }); - task.onSuccess(function() { - callback(undefined, postId); - }); - task.onError(function(error) { - callback(error); - }); - asyncRunner.addTask(task); - }; - - function handleError(error, task) { - var errorMsg = undefined; - if (error) { - logger.error(error); - // Try to analyze the error - if (typeof error === "string") { - errorMsg = error; - } - else { - errorMsg = "Could not publish on WordPress."; - if ((error.code === 400 && error.message == "invalid_token") || error.code === 401 || error.code === 403) { - localStorage.removeItem("wordpressToken"); - errorMsg = "Access to WordPress account is not authorized."; - task.retry(new Error(errorMsg), 1); - return; - } else if (error.code <= 0) { - core.setOffline(); - errorMsg = "|stopPublish"; - } - } - } - task.error(new Error(errorMsg)); - } + wordpressHelper.upload = function(site, postId, tags, title, content, callback) { + var task = asyncRunner.createTask(); + connect(task); + authenticate(task); + task.onRun(function() { + var url = WORDPRESS_PROXY_URL + "post"; + var data = { + token: token, + site: site, + postId: postId, + tags: tags, + title: title, + content: content + }; + $.ajax({ + url: url, + data: data, + type: "POST", + dataType: "json", + timeout: AJAX_TIMEOUT + }).done(function(response, textStatus, jqXHR) { + if(response.body.ID) { + postId = response.body.ID; + task.chain(); + return; + } + var error = { + code: response.code, + message: response.body.error + }; + // Handle error + if(error.code === 404) { + if(error.message == "unknown_blog") { + error = 'Site "' + site + '" not found on WordPress.|removePublish'; + } + else if(error.message == "unknown_post") { + error = 'Post ' + postId + ' not found on WordPress.|removePublish'; + } + } + handleError(error, task); + }).fail(function(jqXHR) { + var error = { + code: jqXHR.status, + message: jqXHR.statusText + }; + handleError(error, task); + }); + }); + task.onSuccess(function() { + callback(undefined, postId); + }); + task.onError(function(error) { + callback(error); + }); + asyncRunner.addTask(task); + }; - return wordpressHelper; + function handleError(error, task) { + var errorMsg = undefined; + if(error) { + logger.error(error); + // Try to analyze the error + if(typeof error === "string") { + errorMsg = error; + } + else { + errorMsg = "Could not publish on WordPress."; + if((error.code === 400 && error.message == "invalid_token") || error.code === 401 || error.code === 403) { + localStorage.removeItem("wordpressToken"); + errorMsg = "Access to WordPress account is not authorized."; + task.retry(new Error(errorMsg), 1); + return; + } + else if(error.code <= 0) { + core.setOffline(); + errorMsg = "|stopPublish"; + } + } + } + task.error(new Error(errorMsg)); + } + + return wordpressHelper; }); diff --git a/js/wordpress-provider.js b/js/wordpress-provider.js index c9ba492d..4eb046a2 100644 --- a/js/wordpress-provider.js +++ b/js/wordpress-provider.js @@ -2,48 +2,39 @@ define([ "utils", "wordpress-helper" ], function(utils, wordpressHelper) { - - var PROVIDER_WORDPRESS = "wordpress"; - - var wordpressProvider = { - providerId: PROVIDER_WORDPRESS, - providerName: "WordPress", - defaultPublishFormat: "html", - publishPreferencesInputIds: ["wordpress-site"] - }; - - wordpressProvider.publish = function(publishAttributes, title, content, callback) { - wordpressHelper.upload( - publishAttributes.site, - publishAttributes.postId, - publishAttributes.tags, - title, - content, - function(error, postId) { - if(error) { - callback(error); - return; - } - publishAttributes.postId = postId; - callback(); - } - ); - }; - wordpressProvider.newPublishAttributes = function(event) { - var publishAttributes = {}; - publishAttributes.site = utils - .getInputTextValue( - "#input-publish-wordpress-site", - event, - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); - publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); - publishAttributes.tags = utils.getInputTextValue("#input-publish-tags"); - if(event.isPropagationStopped()) { - return undefined; - } - return publishAttributes; - }; + var PROVIDER_WORDPRESS = "wordpress"; - return wordpressProvider; + var wordpressProvider = { + providerId: PROVIDER_WORDPRESS, + providerName: "WordPress", + defaultPublishFormat: "html", + publishPreferencesInputIds: [ + "wordpress-site" + ] + }; + + wordpressProvider.publish = function(publishAttributes, title, content, callback) { + wordpressHelper.upload(publishAttributes.site, publishAttributes.postId, publishAttributes.tags, title, content, function(error, postId) { + if(error) { + callback(error); + return; + } + publishAttributes.postId = postId; + callback(); + }); + }; + + wordpressProvider.newPublishAttributes = function(event) { + var publishAttributes = {}; + publishAttributes.site = utils.getInputTextValue("#input-publish-wordpress-site", event, /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); + publishAttributes.postId = utils.getInputTextValue("#input-publish-postid"); + publishAttributes.tags = utils.getInputTextValue("#input-publish-tags"); + if(event.isPropagationStopped()) { + return undefined; + } + return publishAttributes; + }; + + return wordpressProvider; }); \ No newline at end of file diff --git a/tools/eclipse-formatter-config.xml b/tools/eclipse-formatter-config.xml new file mode 100644 index 00000000..c29f4b41 --- /dev/null +++ b/tools/eclipse-formatter-config.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +