From 1d86c0d656c67532386580a95f4e0a0a387bc359 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Sun, 7 Oct 2018 02:38:43 -0700 Subject: [PATCH] Add Token Select Modal --- package.json | 2 + public/index.html | 1 + src/assets/images/ethereum-logo.png | Bin 0 -> 27037 bytes src/assets/images/magnifying-glass.svg | 9 + .../address-input-panel.scss | 3 +- .../CurrencyInputPanel/currency-panel.scss | 99 ++++- src/components/CurrencyInputPanel/index.js | 136 +++++- src/components/Exchange.js | 1 + src/components/Modal/index.js | 57 +++ src/components/Modal/modal.scss | 18 + src/components/TokenLogo/index.js | 49 +++ .../fuse/bitap/bitap_matched_indices.js | 26 ++ .../fuse/bitap/bitap_pattern_alphabet.js | 14 + src/helpers/fuse/bitap/bitap_regex_search.js | 22 + src/helpers/fuse/bitap/bitap_score.js | 11 + src/helpers/fuse/bitap/bitap_search.js | 157 +++++++ src/helpers/fuse/bitap/index.js | 84 ++++ src/helpers/fuse/helpers/deep_value.js | 39 ++ src/helpers/fuse/helpers/is_array.js | 1 + src/helpers/fuse/index.js | 414 ++++++++++++++++++ src/index.scss | 10 + src/variables.scss | 15 + yarn.lock | 70 ++- 23 files changed, 1219 insertions(+), 19 deletions(-) create mode 100644 src/assets/images/ethereum-logo.png create mode 100644 src/assets/images/magnifying-glass.svg create mode 100644 src/components/Modal/index.js create mode 100644 src/components/Modal/modal.scss create mode 100644 src/components/TokenLogo/index.js create mode 100644 src/helpers/fuse/bitap/bitap_matched_indices.js create mode 100644 src/helpers/fuse/bitap/bitap_pattern_alphabet.js create mode 100644 src/helpers/fuse/bitap/bitap_regex_search.js create mode 100644 src/helpers/fuse/bitap/bitap_score.js create mode 100644 src/helpers/fuse/bitap/bitap_search.js create mode 100644 src/helpers/fuse/bitap/index.js create mode 100644 src/helpers/fuse/helpers/deep_value.js create mode 100644 src/helpers/fuse/helpers/is_array.js create mode 100644 src/helpers/fuse/index.js diff --git a/package.json b/package.json index fd48d84ff7..e6df597b30 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "axios": "^0.18.0", "classnames": "^2.2.6", "d3": "^4.13.0", + "fuse": "^0.4.0", "jazzicon": "^1.5.0", "node-sass": "^4.9.3", "npm": "^6.0.0", @@ -20,6 +21,7 @@ "react-scripts": "2.0.4", "react-scroll-to-component": "^1.0.2", "react-select": "^1.2.1", + "react-transition-group": "1.x", "redux": "^3.7.2", "redux-subscriber": "^1.1.0", "redux-thunk": "^2.2.0", diff --git a/public/index.html b/public/index.html index 691b9bfa4f..8c0539a453 100644 --- a/public/index.html +++ b/public/index.html @@ -26,6 +26,7 @@ You need to enable JavaScript to run this app.
+ pHAcP?cNoQQx=#^q#>vRoj7rUQ8 zp!=4zQdhdmfQmWfy01^tDLIfxZs95g)*QpKpuJv3sFo>;q_l_Gs8=!A(Z9V?$b2jfyt_U9l z#x`O`dk+g-PgWDugAetE(#?T8gcy^6nu< zw<0VkI#Q%?!>tlEL4NQ{$`jgf$gc5~D?Y$FLW)Iw2A~YQ(W`BApBV6e|SpBWAa_Qs#@`Ft1 z84yHHo%(5RWwpDJn{n$#Yte2Ld$YPMzB})c<-41wef<1l`HW%yrS^5<^gcmBK@(N- z_Hs=~sXCr!GZy+Aehpcg5j?#>lw5~g6P^NnMa2C873B-qQKswnx3lKhIgQ=+#|C3x zat+9x!}Sq8GyNsglxoLd1PAeIV#@~gY>Tqc28TeSUxMNH{Ie}hN8ob3=Lf0=)jftZ zs0}DIU=*@aM@PpBSh;kdV>)&%%wN(>?G6cN_>U|;lyQ_RD^T9-%+ee+dcnd=hp4?z zM<@oXZ*Bq4Vqq1u_`#rR^Y=@OE-TgM1_>GWfyy<=+w<~4ClQzh<5P~3f&sih|F6$4 zCA&gnn;b{3(%ba{;IZ>w3{h@SX++264FzFodCP0AsQ4W@desXpg&!-TG2pu!%QL)+%;@eMcz z$|8nlq-`Z{F>))dJcTUb<2deUzjbDXk^PnpC}4hE6dm}Cna|e@EH;)7pQiXjIQa^Q zq};o?tgw*(YaGLD%Q43?p5A?oDjwfkgfoM>%pBAI*zhq8y@cWQUq2&O*F&g>oZRWz z`(HbD><{=&VT=&Zo{q~>4Y-O7NJ2L^)hc9Te2YyQonWI3+~F1h(tNYeG1D^P86(Hr( zC1OP-+>2!#+tW7SRIk0?6N!G&*UJOKv%JgCia)$dvqj^xmaBjD>jS8~^v#=cT-%K!Op4y+=)Ruay9j=e`V;|06Q4DTwjON{pJHOEE5&R zR-^V(;hma8vwgsueYvw#Mz@4f0N-WgQt@9n13o5(8^v zD28Zs#2+1dW{KI&QcZU6D}I`9L&p?Bp&5=I^&MVRnqrskJhOS#p!J0t;q;*}+dAA* zKqw~zC!1Wo^ij+bbC$wC6`U``VX!*5Cg{&M69aw*a1t`7lwE^0B!V6g{%wq`2r_@4 zUlUGlw_E*rX%`KOu!UROEWM(C;6X>8ed!p^33|#;D5*Sm^HJH4up%dy{2ry8r7oymTRyU^$ z=Y?Wc^it(c`EO3;7WM<1e-(gR&}($!jt|oR`??6sgNy+Jr+I@^clMt}f`~c+Uh%Y^v z(BLk~iLbtQL^wp2$-^8Id_nrl=;FFM`HxWvOo*Y}`=3qmkJdW>=D&Oyc&E3khag{< z3W8GRK=hIH*wGA}3xIy=oKW=2#ZHWVmIpKHA4X?iKh2(_GBq~7nV;jh zMgD#gYXIyKr%9pJrxvk`j4BJYu8uHTJB1dR^)dC`5Rv3}T6Y+elan*ww-B_`0POa) z*bjaoXTEy5_Wkld#Y@Q-`_d7q{pin4oS%)h<>&u zswF|#gd_VXNnGu>`nyJjOcy*paGjfuhyf<&=k=)a*;C{`oy|Mb6?ClP*UBROzsLt~ zuDpY^TD1LO2E?CYIk=xiznj%IykGIxjiOql9-efGdnk@mh5iD zXF?fDgp<)^YG!Zpbb%4BnhcNk2NNycx&-`lu1f|6RfmA93eW_m!LXRHe28_8^VY3H z9l=vilLvfq8~ju@PFlR3343wnMt079+;-)un9(1m78YB9PYnJlg7&Cppfql2tR++O z!YPtkNR+ylq4X%6Z&K-QEY2#p(XI%lD%As%Gk3mg@Ojj4Z!DKC%I3I(P$d#oD_tZQ zpJqlC5fJi2>W#2~$qsA!Lp_=#ZHMl~-88Ay`)}W_lIOX}gNwU%z_ujjxL3B$f5zpz zLiiZ!(#CkDVWDbSzK2r0Wu~!#|06W@B^It8)^N3;zdXyqCBT>d@UhWX?4B( zd3)D-WU*n8dzzp$zx0nKE-GNyGDPEr)I^M-$Q!|9k)I}}upO~b3Ti?Pxcgg6ee?5T zF0|hq=7bLL@v`Brn453r9)SkW3`7shmKOD3sH&4=IDAJOTEuy4TL@Yw#;t;XOiug^ zv61CSKQYC4D}o`cX<7SJ`t9r2%Em9rJbO`3-OkCt;hI;A z;CRF#?thbK?66_I0uG!bi>n{zS9586jG&`mcD^e+Y`rQnujDku2?Hs6kzjgu${}Gi zMT&_jhb53ZpX7m?Z+B*er3wZ%@fqyK&VKhBTBX>D3*AjIiz_+xR=?9ab^Nybk% z%GjhIP(ahKulTyI4?xw^AOy^G9Xmj^2|#H6XNB){I>ZXl(k0SyQkB)dLGQH*JmDpt z`CdaTO=X*8FE3N{1#)bTZp4Cub)ruXWSOd^X=}35yE9)D}&gzWZ;AT`U&75Tv!M#EFHR1TTrvw@4d#(eMGEk%_s6cZ)@geO3#kLh@7Bw8&E zhA#cylgq-`c(oAvg@ZrdI3Jn+>`WU`e;$Sgda$eFSoXd)*Qlh@62XbwKYm|!vf{Pv z&%tXv!p<%<%eKviG?MlQSP*+!)P;Y4#b)|gemU-}M?>yY&oy$puBD~b@#+|Yz2WA@ z1Me`o(_XJ?jQKMNpe1)_HI>osy6s3~fwi)|@pWK`erI)w-a{)h?zJOmx^4 zPDveET;I{$WDVaGGsRy#d2A~^r^8XSsfG>bX7l1xjg28;s9f`Zy`KJ~Y8AikiW}*lQoi(|| z$F!c1vt}>#MO`80@?{H)R^@J{whYz4b^szKRjd8@@tia8ijBr5gle%;Ks$vwQaMM+ z5nIB4g4H6`CywIMid$Bvq#5XzbS3Rm z=Jlwmy!?DsfiFj8HXnLGNV}KwGMs-t8Q%2*T9XXJX}4}Za`^Pcoo zaq}lcOm>Rjz~kN9)@xGpU$z*EtXPxO;=_dR@oTb60&>+|o%^{3DESgHWDOPGJ8))k z05{(SAXdD%EPQdcCUTHkaedJ>TPt(--f4Wd{q}n~ynMJbyuBd9bU;Pi`N|>6*Q5~F zDAaOpvnmFCX^P{gMYeNc*2xiY@ShHK^7e@??ZL5@1Oxd-@lTzd89p<~ZPbhY+jsKv zPTDWa$L`@Cp@@#bGkDiGIXPq1paTJ}_Nj;R?gQ9Du#=jFBFJL{jhz{7&G5$?{m#aa zm-+cSCT3e}uXhl*eYb^8My9D_@)%YgGKOkHRyxE_*pwyss|8Fyilgi$S&w5XRr>l! zZk{iIrIX1u5ha^sh<``TPOF)m8)1%&DoALKi+>jpj zYelhnep!Y+U4|{?U=!Lsxx6!TT2&XOU1q0sV<1LZ8q2#LYIWp*q09vCrU29WwZwa) zQ$^i!z(s}k_xJN0+FFscze`E{fhHH|gcd`!e1~M2(`NAYqE?sh?AO)R)fgA@W81({ zw;MMqTG_l)BQpHL>90e@0`qWIXkrl;uU-Kn0GF`LbOn6yM!u_5;}Nm1I~|2{PI%oP zr%$Tm%XqF}m=A`Z&62~i2F|eAKuiMoJ9!@}`+NwP)JR`Fs51qCjuy=O>#n}$+4{ze zp0a%3`bTShfLf>i2cOW!-f54^qW}A~I{4c9N^ZR{m)bS2*Y*?m^*Li@!=Eo6C>LaB zkM`MGxU$V@u|<9;c+Mv2th__w1wZ&ls3eT~10m=5{Cb8DYDLuss?qQUX%7!j^)bXqW#uKfpftr}-XP`x_n$W|LzW#C0bo#9}$8^MCkNGJq zhPiYs&u3z(j{HkY{RHGz9qoUYS{6URSv*t=-u(IQ=B)XTFJJ1x<233}_4wO387L_M z!=WykEW_!=^R@VTYdgF?6ISxJO{<9$j)KUq$wEB)r?Ha<`voHI&6IOp-d~}J9~Hw=>dh!YMP$Ub$Fy_Jqpm$L!WtIaceEw1D-RJ#w2a4eJ%cQ5$*{>c?A(Oy-l z*_mtn1wrXtyQ94S6KG_hDJ42bH~7{`G*UaXrM8j$7T!dHQtx0nCRt38qhc87*#N`yvhLCa|Dpchk-4*FJ7 zumGG`h!$ZL0Po}&56xnm+HGF_x($fkLL_icfR} z-<|E+3fh*;E7`O=9>370)tI6D8PN36?*93SI;mFBZVJ?03;?2yaDIMk2f7|3>*t{; z-?fqSA77pQ&WnM1DNSK9yY8`fO-V%qIkvHJWbvj!@a#`-v=tL|;Lh3Uv4nZgYKW|V z-8KZgCaP+}Ku;*aPiIvLyRmR}_+p*SrR#Y7%UpOlujT3L#KeSD!L(Z(L47hn?7!c$ z`T%xuzEAy4F19Ohc}$91uCW{d;;d)Ko!M^<%6+?!Di|MAWZ*3{gF z%DWtmm(L3d4sqg4OkdvHSSic<#&GYP-BaQ3Nq(BIg^oq;=yy#n&nWl_?i81m`JcIX z@e_~L>J$vGwga&fQrkqyLcDg6iZU25knUTzpB((KZ+K&23|?~nF1Tem?xbOP%%F(O z6WgR-*fW1zmx3({eB%qW`bSCn?e(J>kY%9IrmcNd<17s&$Y#XqHHj(xmW;RvMo$At zQ(1s#zR8ZU*9(Q&qAy4!Jm^q%me;8OmPu0v-q-Ay?R!0$xo0*)|FJmPsu0$i<>eKA z^Ohh*fI+Fbt$Dm82p8({K392acmM_Sqb6#viL!8TX0u==c)q{q6{S)8L-?Utz^UdKY}H?C&Y<8tuiDx`{vG0fH+%6P;z1e#X1-U+FBdP z$1&2zfHa-Kl4fSx#RjUmo0(K6nWfp_`ja*hX!?p8%`Z|Sn4Yk zv-OMqfTcD~szi~#PC#T1WRrbbM|-D%lVO(x-0K;*Ii$S;G`%a)d^G9BToIirgB)8g^kK9`ViGw2W4!|;{( z*NK1!TJT0BWcR|+VA0?^a`w+VX*IxDUIGMq4Tio+Q0)IA4-|k7?#f^?L?;V^+Oy)q>5uAA{NnBR9)$hR4;?TVgb{yzU7^7F2W?kBzROab zdR5vuI=i;Had^ENy36r4A}lwuqj&cZ|3GEH{C6Pm3Q^eaSv_!3a*(c;Q#0^FG>2~= zObe`eU&##~=~#oO+>PZ4-h2ZXY<*KzPNJHz%B8QE@tM(!Xi^Fzne4&>dFfr6OTH0K|1uHfud=IQr9uk2WeyRj z5EC4>Z^ad@KR4v;MKWm-wCEXWxc3wqC>M%LPs~TG=6IgJ{s$&S@<6v}zQj}5^` zNlAF5R9`FyZU6$lFv`H)6@Ugq2YLJBqtp0j>E7*%nJVKCF7KW_K455WUg0~|9M6ls zqG6)Aky}_%vDu|j+lT3UnzlXoIkGY3QP3@o06&8jCt0PdUAzq@`(h(dBL{-wjzrD& zl4S_D^w=5{{8_5x;|k_O3U|OGZY9davQq3?Lq<>QO9|!Any@I6dyB z8BeI^_id}#Hy2V90gHRK2qr(^Rq}aWy#U7L`9!wjUH&Q0z-rZk%5Cw<=6Hb`;i*f9 zzK@=M{ZuprGgW44j_5~Q; z#8!ksgdY9FuW2QYKvqqW4_`een0}TkY{^C?WBrDjdTk}Sw)lT@dE=sr6B^qJ53w)Z zKwU*9aJoCcX7m>p#nAtv@ALH?%#m_Zz7@D7ZdwzvV+GTP=#S6Px%J$T$2}Z9gh~bb zX8hhENL~N<;B)sg4vb8~TII^Xa+IfpGk$t~K8Wy`+S>!z9igaW)dCK@uK*-LfEwB!+BPX8DoOM2)KIvkBQM_rbhea%Io zgdPd2$`lI;#xvsJl*I`6jtm^kr3mh(^j9}sn~vOBBGSz*XF17MXc#@fsS2PTRL&v& zAd+%OAqKi#J3G)`tA>=$GpF(J85nLP=G|SoNA&6R(NzUd{p_dylR`I(og?2EnuoWJ zHPvt#QjI^}Kp95XWtn<6a$iiX9Pq$?DDeVss2wBzpFZP+!(~lFOUvLF_VdXr6kW~# zC2J1&(rT#Tp|(>+Yr84?n60QyS3agP&)w|}i+jg|>rdM|mF-!`%@MK(`5xIjC;rm^ z&@+BnstdPw?rVdI>2-y1ZLv22lwynF*X$EVpce{c5doM9vO|{3zaPW1!&iTte35ca z`XdKU!WEB~Xb#k8S@DBu$X^B+%MW-yv0b~U^6PcCsnvz=uY7{$=nu%IA$xR$R%d*tAcY~%qH>Jm^(Z30(9KHXuy`Ak1uq1`wAC`l;cLie^}qL-MmlF}`BS>zop zQ_@%Qr0!x-=ctVkYwN+;NLC3o4X!E08_k_j5Wx6rZpxVLIUf2hl_*S(0piP3KK?Kg z$d*i6MJl%=qN0B@eFUZ$biytP?O<=T_cskW?BhAq!1n5Pp()jdc+2h^A;;rQt$ryQ zA}gE0{hC>w1l>m$IO-QiUUPCamD_?|V&g7sI{e>msX1f-^F0}W@O;z{tOlJn&UD)m zd@_%AMvTBh97TWXz-#i$&}{gij1=OWpZ~J3knm8UOQH~%>y0{Hi7KCO(5T&i+Fs-* zI;pn_y)hHF?{D*y9QlG1gbmbwvb}?LUH$K)c;7R}<9o3zTt4p1(HCljoEfn6C~mE- z3|Jm0g;w(*{RA$w?id2ljYc!j>(yk=FSvb6a(1iYkfg{jdx`3S*j!r{MT6(}+@G{M% zOP}LOB+_@NXu7rkol2E;?oy1w%_aCKI*$#Vb6j!ZGn6eTDM?ICxP-(M!q2EG9#vqB zEF2!Ag$zFQGn}5gaL+&A4||wF-|1}l>ev+{>2HhvLEM)N5gH6f^uVbpV^Xsyy1z6< zZ?zI=HCR{sHg^{7c|$g&BO|Ly@mSQ+8d=im2T| zM1dCx-bD_+Kv<3*<%Zt#%V6lhgq$;U& z847LPlgHk5nQzIE7NWQtQ|6_Bx&k`P_cM$WZhnUe>!65H1?xT-V_ME3?QS^%0=R(9 zWsY#nNSXq+CbK1!QW66%Y?+X%&oE~|yL0RZ(5;OYcoXfL%+CJ58XQ()U0v^$!)thK zP~trBo)sN&|0z`(%$mnxO@9rgj?QQB?(U5`1#r#J^HlksW0 zBDTSP4P)R2#{XYkR~`@L`o4!2hm;d4R46m(v>+8CqeIMCOC6Qn;gD21S<5oCDBr{w zPNjtwB?*zS5w{`vjo^BM2F^SsY}Klgpz+x2>Ls){YE zrcT6hw>Jf8f5{SN&x0kUAv3?2QxO~Cq2czEZ2439qzlzi@*8|R2RZ=a^%38>mH!(l zN3GF=>yY6P3$?OX>;v)%QZiiNGmjI8h1tI$=XS`e*OW7GjD9~OXqsV!x(rfINvCU< zHhp50E=akn^D*#3CKUND7?}WTyr~YRa>WnWOA6Eeje8|MN8e8F;OoXRf=^*qsPy{3 z&eB*;U#IbuPDlPeREP&`#Ct#Xrj4ngh3+usRJK=PkmeRpmj`WwQFQSq43vh3giQ9I z5GI8ZQ~zV&B#Owq7wt;V^4~kIKo%^8j@V;4BL&pgX5zBHIKw{9Q1>7Bl6$!vwh z!l*tkFSFnnEJ8;r(#RMc#KWA&Dzlw_uiPAdFl3k2pkMr$5@8Jvj@}ZvQ>)n0G|^VCoO;yk zFHcxII3R@0xNRfFEd894-e1=KQ6X*`CcVbEa-+J;B$e5%z$J-i34IdgW%A5M3f|Pe zKRBSD)=HI(e26ikHXSr}rHBZJ^M=QYx?Z!yWd~E?8yn2>x87A@GaMUR8qO3tqux;v ziHn(nA#Mqdk-mdp<7|}JNS4xsQ96DzEiI*MxM1{-hE}Fy#F+Kc*ikOd$Urta+_!r5 z>TxPFwG^3L=Sp?}3H85)j>)m!9;}sSRLLAD`9R?hx574l)ugtprlqCm7s*I!q|#>) zc5T($U3fCM)#L+)Vk;nlg<)6+v{ zV8k^TJXPB0BjVC5luiYQ#{FLTbia-u5~NW%PcoNJn%LRB@i`Pi=J%(be-8#@jb8=T z+Xg|^?p|fx2bn|`Q>njKqvHN0WEp5zkomAhCjDIUTIv>c0F)*v{*1}zW7pZnF1Kx* zEUA+GhKJe6va0(1gK9UtY94LaVA5R`J}<2B`PUZB;|C3w zGt#5mYisN4^*2iM)_sl5tZOzUZ`nU)-(b1V+nW`SA-RJn4hc-PDnnd$eFU+Fr_D!R zElWr%`^U{CF)C_)ZVv4n7Uybh?CpailQo%F+q5~W&>hxow(5RIB~Yn6 z3J)tgGqu4ia|NNu`$wphR?4%_`0u_ z-SPKR`5~uCmS!wtLJjk_((%Y+E+!+aAsbYs;z5ygFMSDUI90n{T5d z8FjoC`sRv(`3K<$h*sTQm-Es#Bs@jhglU_$EYj%BT!>#f8*3%85VOrf%~C-^Hot>o z_77mowkJ>J7qxd5uau9`T(*{XuX`vA9@vhymnypGku9cl?h{IuDtwt6RkW5^C&W$r zv)uaII${FYG8+yJg_NP(KlY!HhB1vd;KbQ?1I-HmoaDZ&O_k!^&v?Ex^<0V=Py6kn zpJv7_$j#NiduoL$McWjvWQ2IABHO1Wd2jH zOb!>StYTkg#wq`%6P;y!S1M3UJ#6bEC+ewFy!P=*P!+V3z5Z+H=O+>~cQryzNKt3! zq|U6W?iekj+~<7RrYG-B6BDD%v=O#3%?CTsPeC-ykGB+IK3?qjkr8yj9Ng|PJ*OaDIo8DfS z&|qA|iRoK9(+2#4xI#gzLhl#P>7LHF&p61vVP8(&nv(c7zbZ(!`N*2Z@ztY&dd0_Z zSQv(D2nMyLAht57wxfu?B6UV{mi&69J+U}dM;hIoF#oh(6YvJ<(IQmv{GKW*Z zLGvg2HTZuP(zlTaA}6<8rYNaWeY!+Ndt@e=GdKKJ`wO(sWL3-(FZZT}+8@IEZ zP&oZzlA~{8By(kPQBlYO!Mq7<99!s{nVudoaiw2jwJ%5_ul4r!hLdRB>=)(5;;~*) zK}|DZcO|??u9%ZmHnePp#^j z{k-Fc)-V;7yzA-{F_R}dR*jxAF-OvG}Edy>~qR}?|vdL!s|wx-su zV4Bwpv(174P=7l#`8W>4_1Y_Jvpdx|#TJs317)V~{=xknaQ_nZw6wJGoMb1?^e`9t zxLtY_uboR11uY9T!Q4^Jb0L@#j9LB;0fM=ILlSDV<@cF0m(@_z#iJSShfqM4Ap7Qu z_7`(Tehii3mNe<8C(oBcywElm$`xRG=qNsY#zGMP5ToT338Zza3>4K3trvJwDi4Q5 zOPy7zB5JAUPJ+AioKH+)gLeu2w;T#aHXXPG@=>%lv6Q=5vD3rE6DL@bF}wygq$>}W z8B%J&;Q8t{9j{(t9GDW3cTbYItumT+!8Rh!yEvSLSO&Gx&PXWCi5ToX>3cC!-*wj} zkJtXu51upZ2_mnTzj%TRgkd;=W;$;wXcvNST)+N~q4@LJeNj3_VA*)QXupH5&!cl| zU%G&=f_sk6nsO(6UnPq6FJ8SWzKDom;65s9{g~b2fT)8W!)xc8V+y`Q$Te!^s#S?9 zk@s6b_w=Q>xMMa(oekuX>upl<%%GadQC=Yf+IXU$)+L#gWwRcJIxBcF@M4IrFDGk+*|aqhANU#_L@mXZ)*B%~tC_)EZ#!`^~M<^~y8N6_}fsqj6fgsi&Ts-Z;G)q_UyJR!5{Feh~!rce1^ zlknFZUmPKT;mo-xW>lu4zmjzZseQyIGA%7gTWplP1kO$5(|f2JHcA@U&@6R(hk^+6 zQ=lK0swfvl-<@QS5@9pzR&;S0 z!j+v}{08g_R}os+wi8`R7T2tOsJ*n=ib^VcZnAZ1KO3w|1BIJT&hN!!kA-9j`+j}% zH$BHc2qlb&K+hFJ>UVoW5f#WUOy&Fj*|LzfcZsMXTERc}bK?lhm%?_TIi}}UrN*oY z+o-cE-F;7UcIp{0(uJvEJ$|P7C#hlKn#Nin*h(SPVxzVw=}9uvsY2pMCmYb&VO52v z9R;DqpQkjkXuV3a+wS>~j3{Rk7A3Y8GP^n3n0ILn`jPzq602)LNM7er}QaSxn7rk9bXx%xNdCneCPzEx7{QCP!b|3v|Z zyu6mjt%0WrwY#_91 zVZRJE_Vn}=RLFb+51jg0AnoG|I1q+vpjK<}r{XEQ83oDVdI4pAEnOI3JAMMUH?aP$ z;e!+1{L`dxMGoTrc$5L{s2|^*(ECFnn%5q0kA~X3o2jWfl0QwqZq~hIc1J=8r<7-p z?qA+&qT2Pg>ljP88I$Q5k-YXjzOZQ8iO|U3IHUiQ9(>!ue~xU)b_c?$2a=-s3l=d}3wXl3NrNoHgmCXBkciKFvo3WYGuh1@{SEs9P&`+c$;rv#61@(iH!+ZVF~Me0 zJUfZ;F1xX9)FODOQ&Z+3u4HGRud&fq*P+IxCX9X-g^=C#C^lHTk$%|e#B_sbTS;`Z z%r8AXW)C^BVd97&@}Gi%zmsL8qRCTCBv zbUtkNbK9V`%#tH0DC|TANns0mt=%@l-W&nskDXpvX4~={!JRtfOmX{U{SYW6`xv!t z?!q|=`oR*gxD+?mytYfpU7Rt-G#&rVt2HqzuQMO}5@G1z;EMeuQ|dX&BE zOP!vE%=d}X*e;z_M4w-V%q}f6deOXo>ii;fgX>+w80dN?D1(~Z(x>iux9Y#M#$5u- z|5fNl_R8bwIaykJn`_O;edO;Vd44qKwCsmUG1onNgM!B6i1BaRh?cu320#hhMFQgD zv!RO=JrXXD)5SWM&UUMO7!p#ooVxQ0PWr&@9~xar@ro)sAc*IoizOlgRN}do>Ab6& zbg;I@;%UQThK7epw`n+)eH|TkspaI@wQ2rGr8ll$n()K3v>9Rr5@xo>k}~36IohZr zM^dqx;9DTyn9l?;%e0{+#6O|nMgznk-ALgE&iT~SJ|Fiv-m&p1-#&$_2T8}RmYo;a z+M=J>pJ7n_$vFbZNK3P$b+s~tc>`Agv?IuK4)!)b{$Z?3&d1E&uuyz{oqz~7@vBzP z$@43J#^LB*#mdeTKFz@$ICcHBkR<9;)+Ec0xRnD~jw%J<&X1F*7B`5fW{aBaot(@&~MjILV85i&DYsqgQ z#({DZ-e7Z0Yn)MW*5~ZJyZ|=gDV}Ogeb6k&v|}u#%U(4JIuyNPi6|CR==WW(mHK@b zN}YYy)KujI2K=x*;9&tC>1Y||52G_LBw`UUF(1szzv}i<`i?aI;uszaG+8hkIL2?} zT;0u@P2`HXgZz^{`K`(#_yKW&ddK@tV~(Ed(eArIGG>v&c&uqQ+qZuH}m&KOMa^rNdpmivA0$bEf2m%&0u6Dji5_HX}ALmrC8ngZn`zQ_H?5m z$&!$dkmn8aK!h;mSR=t$YSu7ioCg)6gANSm?gGr(45H}OcWu0^0888jH0LV7d8PHu zU7R^_#uXp5DynU>+%5h`*4no7y8(}ka+q|Iis3malC2AL{I2RtNYaEPR}PS0R)Yx# z>wykugk#*CYwNX7KzTiCicJ9tRUI|pgRY1`gx6K(t_>)mHV)Pu8yqY*@}GK1SK;3o#9j$ZYhisd#B2$@54r zD$W^>1x|n%3!ed~WG35iw6Cv)**PPSZ6Vr|?Om()0C%_Vb&Rk6sx}8bQTQR=hVjP2 zzN#8-S6T^=*x@HSdQ9V;5GU}60|GTWflLo(ely;`e#;KvLg}OBCl@HN*O*x^VYT#L za=RYj?>{)qApePmE54WG@A+EUc{fQ^A^M;g2ssKX=*h%pXHXQzjv%jXM+MXLC3VOk zxhG$fIq`h*QaCp6$J?`Q?x1;< z|4tUK0HBOPTnlcNy&$9DgvVT5%rGQKoA>9$i0s(hD)au%&d%4ejs)y8W%VtX%bgsNl&*v%B+%FDk)b?;`9IY8P-nxT$F>Dw9wJy| zm013}qo^Hrgr4EMXF??dd|6KIT66y~D)g_v%xS{#^b00NP!N=KwAKy&lxHJFhX!w+WO0>EYtPn~r zSPs$|-Q92Mo3G&|E4S(bFd;E6UPb(ue1HIHo4>sB*$KVCbPjR8ZPRe>yYlB2)@%!}hFpI5bYLDz z62MA+lN1=!rLB(&O6FkvM(!gbmFQj$t-dtT-l`aZ}Li63U!`IEl_mB0i2mZhx%4&5D zt(EH9D>c-&X=tq1Shs$S_HuRg_3G+3XE|d{|M|jRZ#Pf(ga7{*Uf47)!WSsaO_oL( Izd9fOKU3-xPyhe` literal 0 HcmV?d00001 diff --git a/src/assets/images/magnifying-glass.svg b/src/assets/images/magnifying-glass.svg new file mode 100644 index 0000000000..fc31e2836e --- /dev/null +++ b/src/assets/images/magnifying-glass.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/AddressInputPanel/address-input-panel.scss b/src/components/AddressInputPanel/address-input-panel.scss index 722254bef4..717f7bb2de 100644 --- a/src/components/AddressInputPanel/address-input-panel.scss +++ b/src/components/AddressInputPanel/address-input-panel.scss @@ -4,8 +4,7 @@ @extend %col-nowrap; &__input { - color: $mine-shaft-gray; - font-size: .9rem; + font-size: .75rem; outline: none; border: none; flex: 1 1 auto; diff --git a/src/components/CurrencyInputPanel/currency-panel.scss b/src/components/CurrencyInputPanel/currency-panel.scss index 85b86c524a..fb93af8138 100644 --- a/src/components/CurrencyInputPanel/currency-panel.scss +++ b/src/components/CurrencyInputPanel/currency-panel.scss @@ -41,16 +41,7 @@ } &__input { - color: $mine-shaft-gray; - font-size: 1.5rem; - outline: none; - border: none; - flex: 1 1 auto; - width: 0; - - &::placeholder { - color: $chalice-gray; - } + @extend %borderless-input; } &__currency-select { @@ -70,6 +61,17 @@ &:active { background-color: rgba($zumthor-blue, .8); } + + &--selected { + background-color: $concrete-gray; + border-color: $mercury-gray; + color: $black; + padding: 0 .5rem; + + .currency-input-panel__dropdown-icon { + background-image: url(../../assets/images/dropdown.svg); + } + } } &__dropdown-icon { @@ -81,5 +83,82 @@ background-size: contain; background-position: 50% 50%; } + + &__selected-token-logo { + margin-right: .4rem; + object-fit: contain; + } } + +.token-modal { + background-color: $white; + position: relative; + bottom: 21rem; + width: 100%; + height: 21rem; + z-index: 2000; + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + transition: 250ms ease-in-out; + + &__search-container { + @extend %row-nowrap; + padding: 1rem; + border-bottom: 1px solid $mercury-gray; + } + + &__search-input { + @extend %borderless-input; + } + + &__search-icon { + margin-right: .2rem; + } + + &__token-list { + height: 17.5rem; + overflow-y: auto; + } + + &__token-row { + @extend %row-nowrap; + align-items: center; + padding: 1rem 1.5rem; + justify-content: space-between; + cursor: pointer; + user-select: none; + + &:hover { + background-color: $concrete-gray; + + .token-modal__token-label { + color: $black; + } + } + + &:active { + background-color: darken($concrete-gray, 1); + } + } + + &__token-logo { + object-fit: contain; + } + + &__token-label { + color: $silver-gray; + font-weight: 200; + } +} + + +.token-modal-appear { + bottom: 0; +} + +.token-modal-appear.modal-container-appear-active { + bottom: 0; +} + + diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js index 503e808d8a..671d5c8177 100644 --- a/src/components/CurrencyInputPanel/index.js +++ b/src/components/CurrencyInputPanel/index.js @@ -1,9 +1,32 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { CSSTransitionGroup } from "react-transition-group"; +import classnames from 'classnames'; +import Fuse from '../../helpers/fuse'; +import Modal from '../Modal'; +import TokenLogo from '../TokenLogo'; +import SearchIcon from '../../assets/images/magnifying-glass.svg'; import './currency-panel.scss'; +const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images'; +const FUSE_OPTIONS = { + includeMatches: false, + threshold: 0.0, + tokenize:true, + location: 0, + distance: 100, + maxPatternLength: 45, + minMatchCharLength: 1, + keys: [ + {name:"address",weight:0.8}, + {name:"label",weight:0.5}, + ] +}; + +const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' }; + class CurrencyInputPanel extends Component { static propTypes = { title: PropTypes.string, @@ -11,6 +34,93 @@ class CurrencyInputPanel extends Component { extraText: PropTypes.string, }; + state = { + isShowingModal: false, + searchQuery: '', + selectedTokenAddress: '', + }; + + createTokenList = () => { + let tokens = this.props.web3Store.tokenAddresses.addresses; + let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH', clearableValue: false } ]; + + for (let i = 0; i < tokens.length; i++) { + let entry = { value: '', label: '', clearableValue: false } + entry.value = tokens[i][0]; + entry.label = tokens[i][0]; + entry.address = tokens[i][1]; + tokenList.push(entry); + TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0]; + } + + return tokenList; + } + + renderTokenList() { + const tokens = this.createTokenList(); + const { searchQuery } = this.state; + let results; + + if (!searchQuery) { + results = tokens; + } else { + const fuse = new Fuse(tokens, FUSE_OPTIONS); + results = fuse.search(this.state.searchQuery); + + } + + return results.map(({ label, address }) => ( +
this.setState({ + selectedTokenAddress: address || 'ETH', + searchQuery: '', + isShowingModal: false, + })} + > + +
{label}
+
+ )); + } + + renderModal() { + if (!this.state.isShowingModal) { + return null; + } + + return ( + this.setState({ isShowingModal: false })}> + +
+
+ this.setState({ + searchQuery: e.target.value, + })} + /> + +
+
+ {this.renderTokenList()} +
+
+
+
+ ); + } + render() { const { title, @@ -18,6 +128,8 @@ class CurrencyInputPanel extends Component { extraText, } = this.props; + const { selectedTokenAddress } = this.state; + return (
@@ -30,15 +142,33 @@ class CurrencyInputPanel extends Component {
-
+ {this.renderModal()} ) } } -export default connect()(CurrencyInputPanel); +export default connect( + state => ({ web3Store: state.web3Store }) +)(CurrencyInputPanel); diff --git a/src/components/Exchange.js b/src/components/Exchange.js index f1b007a78f..0fab887ee2 100644 --- a/src/components/Exchange.js +++ b/src/components/Exchange.js @@ -1,4 +1,5 @@ import React, { Component }from 'react'; +import React, { Component }from 'react'; import SelectToken from './SelectToken'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js new file mode 100644 index 0000000000..10de386274 --- /dev/null +++ b/src/components/Modal/index.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { CSSTransitionGroup } from 'react-transition-group'; +import './modal.scss'; + +const modalRoot = document.querySelector('#modal-root'); + +export default class Modal extends Component { + static propTypes = { + onClose: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + // this.el = document.createElement('div'); + } + + componentDidMount() { + // The portal element is inserted in the DOM tree after + // the Modal's children are mounted, meaning that children + // will be mounted on a detached DOM node. If a child + // component requires to be attached to the DOM tree + // immediately when mounted, for example to measure a + // DOM node, or uses 'autoFocus' in a descendant, add + // state to Modal and only render the children when Modal + // is inserted in the DOM tree. + // modalRoot.style.display = 'block'; + // modalRoot.appendChild(this.el); + } + + componentWillUnmount() { + setTimeout(() => { + // modalRoot.style.display = 'none'; + // modalRoot.removeChild(this.el); + }, 500); + } + + render() { + return ReactDOM.createPortal( +
+ +
+ + {this.props.children} +
, + modalRoot, + ); + } +} diff --git a/src/components/Modal/modal.scss b/src/components/Modal/modal.scss new file mode 100644 index 0000000000..a4d0c68a51 --- /dev/null +++ b/src/components/Modal/modal.scss @@ -0,0 +1,18 @@ +@import '../../variables.scss'; + +.modal-container { + position: relative; + height: 100vh; + width: 100vw; + background-color: rgba($black, .6); + z-index: 1000; +} + +.modal-container-appear { + opacity: 0.01; +} + +.modal-container-appear.modal-container-appear-active { + opacity: 1; + transition: opacity 200ms ease-in-out; +} diff --git a/src/components/TokenLogo/index.js b/src/components/TokenLogo/index.js new file mode 100644 index 0000000000..c131a4623d --- /dev/null +++ b/src/components/TokenLogo/index.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import EthereumLogo from '../../assets/images/ethereum-logo.png'; + +const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images'; + +export default class TokenLogo extends Component { + static propTypes = { + address: PropTypes.string, + size: PropTypes.string, + className: PropTypes.string, + }; + + static defaultProps = { + address: '', + size: '1.5rem', + className: '', + }; + + state = { + error: false, + }; + + render() { + const { address, size, className } = this.props; + let path = 'https://png2.kisspng.com/sh/59799e422ec61954a8e126f9203cd0b3/L0KzQYm3U8I6N5xniZH0aYP2gLBuTflxcJDzfZ9ubXBteX76gf10fZ9sRdlqbHH7iX7ulfV0e155gNc2cYXog8XwjB50NZR3kdt3Zz3ofbFxib02aZNoetUAYkC6QoXrVr4zP2Y1SKkBNkG4QoO6Ucg1Omg1Sqs8LoDxd1==/kisspng-iphone-emoji-samsung-galaxy-guess-the-questions-crying-emoji-5abcbc5b0724d6.2750076615223184270293.png'; + + if (address === 'ETH') { + path = EthereumLogo; + } + + if (!this.state.error) { + path = `${TOKEN_ICON_API}/${address}.png`; + } + + + return ( + this.setState({ error: true })} + /> + ); + } +} diff --git a/src/helpers/fuse/bitap/bitap_matched_indices.js b/src/helpers/fuse/bitap/bitap_matched_indices.js new file mode 100644 index 0000000000..30aba36bb4 --- /dev/null +++ b/src/helpers/fuse/bitap/bitap_matched_indices.js @@ -0,0 +1,26 @@ +export default function (matchmask = [], minMatchCharLength = 1) { + let matchedIndices = [] + let start = -1 + let end = -1 + let i = 0 + + for (let len = matchmask.length; i < len; i += 1) { + let match = matchmask[i] + if (match && start === -1) { + start = i + } else if (!match && start !== -1) { + end = i - 1 + if ((end - start) + 1 >= minMatchCharLength) { + matchedIndices.push([start, end]) + } + start = -1 + } + } + + // (i-1 - start) + 1 => i - start + if (matchmask[i - 1] && (i - start) >= minMatchCharLength) { + matchedIndices.push([start, i - 1]) + } + + return matchedIndices +} diff --git a/src/helpers/fuse/bitap/bitap_pattern_alphabet.js b/src/helpers/fuse/bitap/bitap_pattern_alphabet.js new file mode 100644 index 0000000000..9fee434831 --- /dev/null +++ b/src/helpers/fuse/bitap/bitap_pattern_alphabet.js @@ -0,0 +1,14 @@ +export default function (pattern) { + let mask = {} + let len = pattern.length + + for (let i = 0; i < len; i += 1) { + mask[pattern.charAt(i)] = 0 + } + + for (let i = 0; i < len; i += 1) { + mask[pattern.charAt(i)] |= 1 << (len - i - 1) + } + + return mask +} diff --git a/src/helpers/fuse/bitap/bitap_regex_search.js b/src/helpers/fuse/bitap/bitap_regex_search.js new file mode 100644 index 0000000000..41b6684a80 --- /dev/null +++ b/src/helpers/fuse/bitap/bitap_regex_search.js @@ -0,0 +1,22 @@ +const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g + +export default function (text, pattern, tokenSeparator = / +/g) { + let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|')) + let matches = text.match(regex) + let isMatch = !!matches + let matchedIndices = [] + + if (isMatch) { + for (let i = 0, matchesLen = matches.length; i < matchesLen; i += 1) { + let match = matches[i] + matchedIndices.push([text.indexOf(match), match.length - 1]) + } + } + + return { + // TODO: revisit this score + score: isMatch ? 0.5 : 1, + isMatch, + matchedIndices + } +} diff --git a/src/helpers/fuse/bitap/bitap_score.js b/src/helpers/fuse/bitap/bitap_score.js new file mode 100644 index 0000000000..04b35244e9 --- /dev/null +++ b/src/helpers/fuse/bitap/bitap_score.js @@ -0,0 +1,11 @@ +export default function (pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) { + const accuracy = errors / pattern.length + const proximity = Math.abs(expectedLocation - currentLocation) + + if (!distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy + } + + return accuracy + (proximity / distance) +} diff --git a/src/helpers/fuse/bitap/bitap_search.js b/src/helpers/fuse/bitap/bitap_search.js new file mode 100644 index 0000000000..e5fbd92b29 --- /dev/null +++ b/src/helpers/fuse/bitap/bitap_search.js @@ -0,0 +1,157 @@ +import Bitap from "./index"; + +import bitapScore from './bitap_score'; +import matchedIndices from './bitap_matched_indices'; + +export default function (text, pattern, patternAlphabet, { location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }) { + const expectedLocation = location + // Set starting location at beginning text and initialize the alphabet. + const textLen = text.length + // Highest score beyond which we give up. + let currentThreshold = threshold + // Is there a nearby exact match? (speedup) + let bestLocation = text.indexOf(pattern, expectedLocation) + + const patternLen = pattern.length + + // a mask of the matches + const matchMask = [] + for (let i = 0; i < textLen; i += 1) { + matchMask[i] = 0 + } + + if (bestLocation !== -1) { + let score = bitapScore(pattern, { + errors: 0, + currentLocation: bestLocation, + expectedLocation, + distance + }) + currentThreshold = Math.min(score, currentThreshold) + + // What about in the other direction? (speed up) + bestLocation = text.lastIndexOf(pattern, expectedLocation + patternLen) + + if (bestLocation !== -1) { + let score = bitapScore(pattern, { + errors: 0, + currentLocation: bestLocation, + expectedLocation, + distance + }) + currentThreshold = Math.min(score, currentThreshold) + } + } + + // Reset the best location + bestLocation = -1 + + let lastBitArr = [] + let finalScore = 1 + let binMax = patternLen + textLen + + const mask = 1 << (patternLen - 1) + + for (let i = 0; i < patternLen; i += 1) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from the match location we can stray + // at this error level. + let binMin = 0 + let binMid = binMax + + while (binMin < binMid) { + const score = bitapScore(pattern, { + errors: i, + currentLocation: expectedLocation + binMid, + expectedLocation, + distance + }); + + if (score <= currentThreshold) { + binMin = binMid + } else { + binMax = binMid + } + + binMid = Math.floor((binMax - binMin) / 2 + binMin) + } + + // Use the result from this iteration as the maximum for the next. + binMax = binMid + + let start = Math.max(1, expectedLocation - binMid + 1) + let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen + + // Initialize the bit array + let bitArr = Array(finish + 2) + + bitArr[finish + 1] = (1 << i) - 1 + + for (let j = finish; j >= start; j -= 1) { + let currentLocation = j - 1 + let charMatch = patternAlphabet[text.charAt(currentLocation)] + + if (charMatch) { + matchMask[currentLocation] = 1 + } + + // First pass: exact match + bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch + + // Subsequent passes: fuzzy match + if (i !== 0) { + bitArr[j] |= (((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1) | lastBitArr[j + 1] + } + + if (bitArr[j] & mask) { + finalScore = bitapScore(pattern, { + errors: i, + currentLocation, + expectedLocation, + distance + }) + + // This match will almost certainly be better than any existing match. + // But check anyway. + if (finalScore <= currentThreshold) { + // Indeed it is + currentThreshold = finalScore + bestLocation = currentLocation + + // Already passed `loc`, downhill from here on in. + if (bestLocation <= expectedLocation) { + break + } + + // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`. + start = Math.max(1, 2 * expectedLocation - bestLocation) + } + } + } + + // No hope for a (better) match at greater error levels. + const score = bitapScore(pattern, { + errors: i + 1, + currentLocation: expectedLocation, + expectedLocation, + distance + }) + + // console.log('score', score, finalScore) + + if (score > currentThreshold) { + break + } + + lastBitArr = bitArr + } + + // console.log('FINAL SCORE', finalScore) + + // Count exact matches (those with a score of 0) to be "almost" exact + return { + isMatch: bestLocation >= 0, + score: finalScore === 0 ? 0.001 : finalScore, + matchedIndices: matchedIndices(matchMask, minMatchCharLength) + } +} diff --git a/src/helpers/fuse/bitap/index.js b/src/helpers/fuse/bitap/index.js new file mode 100644 index 0000000000..760677f184 --- /dev/null +++ b/src/helpers/fuse/bitap/index.js @@ -0,0 +1,84 @@ +import bitapRegexSearch from './bitap_regex_search'; +import bitapSearch from './bitap_search'; +import patternAlphabet from './bitap_pattern_alphabet'; + +class Bitap { + constructor (pattern, { + // Approximately where in the text is the pattern expected to be found? + location = 0, + // Determines how close the match must be to the fuzzy location (specified above). + // An exact letter match which is 'distance' characters away from the fuzzy location + // would score as a complete mismatch. A distance of '0' requires the match be at + // the exact location specified, a threshold of '1000' would require a perfect match + // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold. + distance = 100, + // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match + // (of both letters and location), a threshold of '1.0' would match anything. + threshold = 0.6, + // Machine word size + maxPatternLength = 32, + // Indicates whether comparisons should be case sensitive. + isCaseSensitive = false, + // Regex used to separate words when searching. Only applicable when `tokenize` is `true`. + tokenSeparator = / +/g, + // When true, the algorithm continues searching to the end of the input even if a perfect + // match is found before the end of the same input. + findAllMatches = false, + // Minimum number of characters that must be matched before a result is considered a match + minMatchCharLength = 1 + }) { + this.options = { + location, + distance, + threshold, + maxPatternLength, + isCaseSensitive, + tokenSeparator, + findAllMatches, + minMatchCharLength + } + + this.pattern = this.options.isCaseSensitive ? pattern : pattern.toLowerCase() + + if (this.pattern.length <= maxPatternLength) { + this.patternAlphabet = patternAlphabet(this.pattern) + } + } + + search (text) { + if (!this.options.isCaseSensitive) { + text = text.toLowerCase() + } + + // Exact match + if (this.pattern === text) { + return { + isMatch: true, + score: 0, + matchedIndices: [[0, text.length - 1]] + } + } + + // When pattern length is greater than the machine word length, just do a a regex comparison + const { maxPatternLength, tokenSeparator } = this.options + if (this.pattern.length > maxPatternLength) { + return bitapRegexSearch(text, this.pattern, tokenSeparator) + } + + // Otherwise, use Bitap algorithm + const { location, distance, threshold, findAllMatches, minMatchCharLength } = this.options + return bitapSearch(text, this.pattern, this.patternAlphabet, { + location, + distance, + threshold, + findAllMatches, + minMatchCharLength + }) + } +} + +// let x = new Bitap("od mn war", {}) +// let result = x.search("Old Man's War") +// console.log(result) + +export default Bitap diff --git a/src/helpers/fuse/helpers/deep_value.js b/src/helpers/fuse/helpers/deep_value.js new file mode 100644 index 0000000000..4e892f2733 --- /dev/null +++ b/src/helpers/fuse/helpers/deep_value.js @@ -0,0 +1,39 @@ +const isArray = require('./is_array') + +const deepValue = (obj, path, list) => { + if (!path) { + // If there's no path left, we've gotten to the object we care about. + list.push(obj) + } else { + const dotIndex = path.indexOf('.') + let firstSegment = path + let remaining = null + + if (dotIndex !== -1) { + firstSegment = path.slice(0, dotIndex) + remaining = path.slice(dotIndex + 1) + } + + const value = obj[firstSegment] + + if (value !== null && value !== undefined) { + if (!remaining && (typeof value === 'string' || typeof value === 'number')) { + list.push(value.toString()) + } else if (isArray(value)) { + // Search each item in the array. + for (let i = 0, len = value.length; i < len; i += 1) { + deepValue(value[i], remaining, list) + } + } else if (remaining) { + // An object. Recurse further. + deepValue(value, remaining, list) + } + } + } + + return list +} + +module.exports = (obj, path) => { + return deepValue(obj, path, []) +} diff --git a/src/helpers/fuse/helpers/is_array.js b/src/helpers/fuse/helpers/is_array.js new file mode 100644 index 0000000000..4da9206001 --- /dev/null +++ b/src/helpers/fuse/helpers/is_array.js @@ -0,0 +1 @@ +module.exports = obj => !Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj) diff --git a/src/helpers/fuse/index.js b/src/helpers/fuse/index.js new file mode 100644 index 0000000000..da440b7ed9 --- /dev/null +++ b/src/helpers/fuse/index.js @@ -0,0 +1,414 @@ +import Bitap from'./bitap'; +const deepValue = require('./helpers/deep_value') +const isArray = require('./helpers/is_array') + +class Fuse { + constructor (list, { + // Approximately where in the text is the pattern expected to be found? + location = 0, + // Determines how close the match must be to the fuzzy location (specified above). + // An exact letter match which is 'distance' characters away from the fuzzy location + // would score as a complete mismatch. A distance of '0' requires the match be at + // the exact location specified, a threshold of '1000' would require a perfect match + // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold. + distance = 100, + // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match + // (of both letters and location), a threshold of '1.0' would match anything. + threshold = 0.6, + // Machine word size + maxPatternLength = 32, + // Indicates whether comparisons should be case sensitive. + caseSensitive = false, + // Regex used to separate words when searching. Only applicable when `tokenize` is `true`. + tokenSeparator = / +/g, + // When true, the algorithm continues searching to the end of the input even if a perfect + // match is found before the end of the same input. + findAllMatches = false, + // Minimum number of characters that must be matched before a result is considered a match + minMatchCharLength = 1, + // The name of the identifier property. If specified, the returned result will be a list + // of the items' dentifiers, otherwise it will be a list of the items. + id = null, + // List of properties that will be searched. This also supports nested properties. + keys = [], + // Whether to sort the result list, by score + shouldSort = true, + // The get function to use when fetching an object's properties. + // The default will search nested paths *ie foo.bar.baz* + getFn = deepValue, + // Default sort function + sortFn = (a, b) => (a.score - b.score), + // When true, the search algorithm will search individual words **and** the full string, + // computing the final score as a function of both. Note that when `tokenize` is `true`, + // the `threshold`, `distance`, and `location` are inconsequential for individual tokens. + tokenize = false, + // When true, the result set will only include records that match all tokens. Will only work + // if `tokenize` is also true. + matchAllTokens = false, + + includeMatches = false, + includeScore = false, + + // Will print to the console. Useful for debugging. + verbose = false + }) { + this.options = { + location, + distance, + threshold, + maxPatternLength, + isCaseSensitive: caseSensitive, + tokenSeparator, + findAllMatches, + minMatchCharLength, + id, + keys, + includeMatches, + includeScore, + shouldSort, + getFn, + sortFn, + verbose, + tokenize, + matchAllTokens + } + + this.setCollection(list) + } + + setCollection (list) { + this.list = list + return list + } + + search (pattern) { + this._log(`---------\nSearch pattern: "${pattern}"`) + + const { + tokenSearchers, + fullSearcher + } = this._prepareSearchers(pattern) + + let { weights, results } = this._search(tokenSearchers, fullSearcher) + + this._computeScore(weights, results) + + if (this.options.shouldSort) { + this._sort(results) + } + + return this._format(results) + } + + _prepareSearchers (pattern = '') { + const tokenSearchers = [] + + if (this.options.tokenize) { + // Tokenize on the separator + const tokens = pattern.split(this.options.tokenSeparator) + for (let i = 0, len = tokens.length; i < len; i += 1) { + tokenSearchers.push(new Bitap(tokens[i], this.options)) + } + } + + let fullSearcher = new Bitap(pattern, this.options) + + return { tokenSearchers, fullSearcher } + } + + _search (tokenSearchers = [], fullSearcher) { + const list = this.list + const resultMap = {} + const results = [] + + // Check the first item in the list, if it's a string, then we assume + // that every item in the list is also a string, and thus it's a flattened array. + if (typeof list[0] === 'string') { + // Iterate over every item + for (let i = 0, len = list.length; i < len; i += 1) { + this._analyze({ + key: '', + value: list[i], + record: i, + index: i + }, { + resultMap, + results, + tokenSearchers, + fullSearcher + }) + } + + return { weights: null, results } + } + + // Otherwise, the first item is an Object (hopefully), and thus the searching + // is done on the values of the keys of each item. + const weights = {} + for (let i = 0, len = list.length; i < len; i += 1) { + let item = list[i] + // Iterate over every key + for (let j = 0, keysLen = this.options.keys.length; j < keysLen; j += 1) { + let key = this.options.keys[j] + if (typeof key !== 'string') { + weights[key.name] = { + weight: (1 - key.weight) || 1 + } + if (key.weight <= 0 || key.weight > 1) { + throw new Error('Key weight has to be > 0 and <= 1') + } + key = key.name + } else { + weights[key] = { + weight: 1 + } + } + + this._analyze({ + key, + value: this.options.getFn(item, key), + record: item, + index: i + }, { + resultMap, + results, + tokenSearchers, + fullSearcher + }) + } + } + + return { weights, results } + } + + _analyze ({ key, arrayIndex = -1, value, record, index }, { tokenSearchers = [], fullSearcher = [], resultMap = {}, results = [] }) { + // Check if the texvaluet can be searched + if (value === undefined || value === null) { + return + } + + let exists = false + let averageScore = -1 + let numTextMatches = 0 + + if (typeof value === 'string') { + this._log(`\nKey: ${key === '' ? '-' : key}`) + + let mainSearchResult = fullSearcher.search(value) + this._log(`Full text: "${value}", score: ${mainSearchResult.score}`) + + if (this.options.tokenize) { + let words = value.split(this.options.tokenSeparator) + let scores = [] + + for (let i = 0; i < tokenSearchers.length; i += 1) { + let tokenSearcher = tokenSearchers[i] + + this._log(`\nPattern: "${tokenSearcher.pattern}"`) + + // let tokenScores = [] + let hasMatchInText = false + + for (let j = 0; j < words.length; j += 1) { + let word = words[j] + let tokenSearchResult = tokenSearcher.search(word) + let obj = {} + if (tokenSearchResult.isMatch) { + obj[word] = tokenSearchResult.score + exists = true + hasMatchInText = true + scores.push(tokenSearchResult.score) + } else { + obj[word] = 1 + if (!this.options.matchAllTokens) { + scores.push(1) + } + } + this._log(`Token: "${word}", score: ${obj[word]}`) + // tokenScores.push(obj) + } + + if (hasMatchInText) { + numTextMatches += 1 + } + } + + averageScore = scores[0] + let scoresLen = scores.length + for (let i = 1; i < scoresLen; i += 1) { + averageScore += scores[i] + } + averageScore = averageScore / scoresLen + + this._log('Token score average:', averageScore) + } + + let finalScore = mainSearchResult.score + if (averageScore > -1) { + finalScore = (finalScore + averageScore) / 2 + } + + this._log('Score average:', finalScore) + + let checkTextMatches = (this.options.tokenize && this.options.matchAllTokens) ? numTextMatches >= tokenSearchers.length : true + + this._log(`\nCheck Matches: ${checkTextMatches}`) + + // If a match is found, add the item to , including its score + if ((exists || mainSearchResult.isMatch) && checkTextMatches) { + // Check if the item already exists in our results + let existingResult = resultMap[index] + if (existingResult) { + // Use the lowest score + // existingResult.score, bitapResult.score + existingResult.output.push({ + key, + arrayIndex, + value, + score: finalScore, + matchedIndices: mainSearchResult.matchedIndices + }) + } else { + // Add it to the raw result list + resultMap[index] = { + item: record, + output: [{ + key, + arrayIndex, + value, + score: finalScore, + matchedIndices: mainSearchResult.matchedIndices + }] + } + + results.push(resultMap[index]) + } + } + } else if (isArray(value)) { + for (let i = 0, len = value.length; i < len; i += 1) { + this._analyze({ + key, + arrayIndex: i, + value: value[i], + record, + index + }, { + resultMap, + results, + tokenSearchers, + fullSearcher + }) + } + } + } + + _computeScore (weights, results) { + this._log('\n\nComputing score:\n') + + for (let i = 0, len = results.length; i < len; i += 1) { + const output = results[i].output + const scoreLen = output.length + + let currScore = 1 + let bestScore = 1 + + for (let j = 0; j < scoreLen; j += 1) { + let weight = weights ? weights[output[j].key].weight : 1 + let score = weight === 1 ? output[j].score : (output[j].score || 0.001) + let nScore = score * weight + + if (weight !== 1) { + bestScore = Math.min(bestScore, nScore) + } else { + output[j].nScore = nScore + currScore *= nScore + } + } + + results[i].score = bestScore === 1 ? currScore : bestScore + + this._log(results[i]) + } + } + + _sort (results) { + this._log('\n\nSorting....') + results.sort(this.options.sortFn) + } + + _format (results) { + const finalOutput = [] + + if (this.options.verbose) { + this._log('\n\nOutput:\n\n', JSON.stringify(results)) + } + + let transformers = [] + + if (this.options.includeMatches) { + transformers.push((result, data) => { + const output = result.output + data.matches = [] + + for (let i = 0, len = output.length; i < len; i += 1) { + let item = output[i] + + if (item.matchedIndices.length === 0) { + continue + } + + let obj = { + indices: item.matchedIndices, + value: item.value + } + if (item.key) { + obj.key = item.key + } + if (item.hasOwnProperty('arrayIndex') && item.arrayIndex > -1) { + obj.arrayIndex = item.arrayIndex + } + data.matches.push(obj) + } + }) + } + + if (this.options.includeScore) { + transformers.push((result, data) => { + data.score = result.score + }) + } + + for (let i = 0, len = results.length; i < len; i += 1) { + const result = results[i] + + if (this.options.id) { + result.item = this.options.getFn(result.item, this.options.id)[0] + } + + if (!transformers.length) { + finalOutput.push(result.item) + continue + } + + const data = { + item: result.item + } + + for (let j = 0, len = transformers.length; j < len; j += 1) { + transformers[j](result, data) + } + + finalOutput.push(data) + } + + return finalOutput + } + + _log () { + if (this.options.verbose) { + console.log(...arguments) + } + } +} + +export default Fuse diff --git a/src/index.scss b/src/index.scss index 5e8839775f..81b41d3126 100644 --- a/src/index.scss +++ b/src/index.scss @@ -12,6 +12,7 @@ html, body { } #root { + position: relative; display: flex; flex-flow: column nowrap; height: 100vh; @@ -19,4 +20,13 @@ html, body { overflow-x: hidden; overflow-y: auto; background-color: $white; + z-index: 100; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); +} + +#modal-root { + position: fixed; + top: 0; + left: 0; + z-index: 200; } diff --git a/src/variables.scss b/src/variables.scss index f3eeffdc2c..aa188951e0 100644 --- a/src/variables.scss +++ b/src/variables.scss @@ -36,6 +36,7 @@ $wisteria-purple: #AE60B9; color: $white; outline: none; border: 1px solid transparent; + user-select: none; &:hover { background-color: lighten($royal-blue, 5); @@ -46,3 +47,17 @@ $wisteria-purple: #AE60B9; } } + +%borderless-input { + color: $mine-shaft-gray; + font-size: 1rem; + outline: none; + border: none; + flex: 1 1 auto; + width: 0; + + + &::placeholder { + color: $chalice-gray; + } +} diff --git a/yarn.lock b/yarn.lock index 9cef30043c..dd04087485 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2021,6 +2021,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc" + chalk@2.4.1, chalk@^2.0.1, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" @@ -2166,6 +2170,12 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +cli@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.3.tgz#e6819c8d5faa957f64f98f66a8506268c1d1f17d" + dependencies: + glob ">= 3.1.4" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2285,7 +2295,7 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colors@^1.1.2: +colors@>=0.6.x, colors@^1.1.2: version "1.3.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" @@ -3412,6 +3422,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -4485,6 +4499,16 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" +fuse@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/fuse/-/fuse-0.4.0.tgz#2c38eaf888abb0a9ba7960cfe3339d1f3f53f6e6" + dependencies: + colors ">=0.6.x" + jshint "0.9.x" + optimist ">=0.3.5" + uglify-js ">=2.2.x" + underscore ">=1.4.x" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4586,7 +4610,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob@^7.0.0, glob@~7.1.1, glob@~7.1.2: +"glob@>= 3.1.4", glob@^7.0.0, glob@~7.1.1, glob@~7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" dependencies: @@ -6148,6 +6172,13 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +jshint@0.9.x: + version "0.9.1" + resolved "https://registry.yarnpkg.com/jshint/-/jshint-0.9.1.tgz#ff32ec7f09f84001f7498eeafd63c9e4fbb2dc0e" + dependencies: + cli "0.4.3" + minimatch "0.0.x" + json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -6559,6 +6590,10 @@ lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@~1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-1.0.6.tgz#aa50f97047422ac72543bda177a9c9d018d98452" + make-dir@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" @@ -6817,6 +6852,12 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" +minimatch@0.0.x: + version "0.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.0.5.tgz#96bb490bbd3ba6836bbfac111adf75301b1584de" + dependencies: + lru-cache "~1.0.2" + minimatch@0.3: version "0.3.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" @@ -7572,7 +7613,7 @@ opn@^5.1.0: dependencies: is-wsl "^1.1.0" -optimist@^0.6.1: +optimist@>=0.3.5, optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -8623,7 +8664,7 @@ prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.6, prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: @@ -9006,6 +9047,16 @@ react-side-effect@^1.1.0: exenv "^1.2.1" shallowequal "^1.0.1" +react-transition-group@1.x: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -10761,6 +10812,13 @@ uglify-js@3.3.x: commander "~2.13.0" source-map "~0.6.1" +uglify-js@>=2.2.x: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -10810,6 +10868,10 @@ underscore@1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" +underscore@>=1.4.x: + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"