From 26aa342f6f5be3db921f2c828d543be3a9761b7b Mon Sep 17 00:00:00 2001 From: Nar Chhantyal Date: Sun, 15 Feb 2015 22:50:31 +0100 Subject: [PATCH] initial commit --- .gitignore | 46 + AUTHORS.rst | 13 + CONTRIBUTING.rst | 96 + HISTORY.rst | 9 + LICENSE | 12 + MANIFEST.in | 6 + README.rst | 23 + example_app/__init__.py | 1 + example_app/db.sqlite3 | Bin 0 -> 167936 bytes example_app/example_app/__init__.py | 0 example_app/example_app/app/__init__.py | 0 example_app/example_app/app/admin.py | 14 + .../app/migrations/0001_initial.py | 29 + .../example_app/app/migrations/__init__.py | 0 example_app/example_app/app/models.py | 18 + example_app/example_app/app/tests.py | 3 + example_app/example_app/app/views.py | 5 + example_app/example_app/settings.py | 108 + .../templates/admin/base_site.html | 65 + example_app/example_app/urls.py | 10 + example_app/example_app/wsgi.py | 14 + example_app/manage.py | 10 + requirements.txt | 2 + setup.py | 52 + taggit_selectize/__init__.py | 1 + taggit_selectize/conf.py | 7 + taggit_selectize/models.py | 1 + .../static/css/selectize.default.css | 387 ++ taggit_selectize/static/img/.gitignore | 0 taggit_selectize/static/js/selectize.js | 3668 +++++++++++++++++ .../templates/taggit_selectize/base.html | 21 + taggit_selectize/urls.py | 7 + taggit_selectize/views.py | 30 + 33 files changed, 4658 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS.rst create mode 100644 CONTRIBUTING.rst create mode 100644 HISTORY.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 example_app/__init__.py create mode 100644 example_app/db.sqlite3 create mode 100644 example_app/example_app/__init__.py create mode 100644 example_app/example_app/app/__init__.py create mode 100644 example_app/example_app/app/admin.py create mode 100644 example_app/example_app/app/migrations/0001_initial.py create mode 100644 example_app/example_app/app/migrations/__init__.py create mode 100644 example_app/example_app/app/models.py create mode 100644 example_app/example_app/app/tests.py create mode 100644 example_app/example_app/app/views.py create mode 100644 example_app/example_app/settings.py create mode 100644 example_app/example_app/templates/admin/base_site.html create mode 100644 example_app/example_app/urls.py create mode 100644 example_app/example_app/wsgi.py create mode 100755 example_app/manage.py create mode 100644 requirements.txt create mode 100755 setup.py create mode 100644 taggit_selectize/__init__.py create mode 100644 taggit_selectize/conf.py create mode 100644 taggit_selectize/models.py create mode 100755 taggit_selectize/static/css/selectize.default.css create mode 100644 taggit_selectize/static/img/.gitignore create mode 100755 taggit_selectize/static/js/selectize.js create mode 100644 taggit_selectize/templates/taggit_selectize/base.html create mode 100644 taggit_selectize/urls.py create mode 100644 taggit_selectize/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9b2f10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build + +# System +*.DS_Store +*.idea \ No newline at end of file diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..10a6a7e --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,13 @@ +======= +Credits +======= + +Development Lead +---------------- + +* Nar Chhantyal + +Contributors +------------ + +None yet. Why not be the first? \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..d387c02 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,96 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/chhantyal/taggit-selectize/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "feature" +is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +taggit-selectize could always use more documentation, whether as part of the +official taggit-selectize docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/chhantyal/taggit-selectize/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `taggit-selectize` for local development. + +1. Fork the `taggit-selectize` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/taggit-selectize.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv taggit-selectize + $ cd taggit-selectize/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + +Now you can make your changes locally. + + +5. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +2. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check + https://travis-ci.org/chhantyal/taggit-selectize/pull_requests + and make sure that the tests pass for all supported Python versions. \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..06cf188 --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,9 @@ +.. :changelog: + +History +------- + +0.1.0 (2014-01-01) +++++++++++++++++++ + +* First release on PyPI. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab5bbe3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2014, Nar Chhantyal +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of taggit-selectize nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..88ce58c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS.rst +include CONTRIBUTING.rst +include HISTORY.rst +include LICENSE +include README.rst +recursive-include taggit_selectize *.html *.png *.gif *js *.css *jpg *jpeg *svg *py \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..91ccb6b --- /dev/null +++ b/README.rst @@ -0,0 +1,23 @@ +============================= +taggit-selectize +============================= + +django-taggit + selectize.js + +Documentation +------------- + +The full documentation is at https://taggit-selectize.readthedocs.org. + +Quickstart +---------- + +Install taggit-selectize:: + + pip install taggit-selectize + + +Features +-------- + +* TODO \ No newline at end of file diff --git a/example_app/__init__.py b/example_app/__init__.py new file mode 100644 index 0000000..983691f --- /dev/null +++ b/example_app/__init__.py @@ -0,0 +1 @@ +__author__ = 'parvachhantyal' diff --git a/example_app/db.sqlite3 b/example_app/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..11f8a17bf2df209f4b096710385e4e12d2560b29 GIT binary patch literal 167936 zcmeI54{RG*e&1*KZ$wGtvn^?9txoHayxuj>CdDB|N%Ug7k!@P5U0Ytume+ESGDC7i zjm&@M483b7Z6kTJXwn2h8z2bMGzf|YEiNsZUeF7IT+qX{xfHkU9oin+^qLmU1-<6_ zCqdF)({!&z-+MFU4F5^8<7^z^%k@6ay!U(W{od#O-kbToc_ZGuaaA`pd8^i_sHU6} zjtQbDe3vW>f{>v9N9q5q&on*owNB`_=sF&Bm=I#$IOAu+G3h>5$W zyHqVTYR$TlOK6!?MxEbs6togv7^GHLzHVi#WX6miS-6A=ONW{MI(zq0PJ0tF99ThAEov!UNub$-bKI-%c^)ss;p?)ScIc6#N+I|Xl^j1%?V~0|( zRfdaNPf1<<=Q|-qUOy-9#vI)oY`J-5aZAZ71>Sv5{E0r=IH~PiE;XD~R)3*ZHMOdl zGauG9Zbmnc2O_0qQQ%4#_sewl%h5V({G1&Pnl<*BE{^XlACn@>%i;$iQ_YvP!KDmN zIc7KX*7C)x*@I>hIwobEHmsyIpVONxRq=x-`n6n8O*%WLlWk) z`@?M)7mX^RKqPzWV6G0((Bn^!N|95i#1Bt*l58U!9l-9Xpw(5wc(2x=#`QpL6t>mI zStYp?ZQC_9m9lD>xpJ+fS7+s-YHFrl(cI~}ku#dq?wEDW%K2KY>`rB|Rkfly<);!( z@v>!`w3aQsL8}=o*-~U*39X{)Wv8aav{Mt!&lEM)t*u@qX^L7f^#@eTZv9z{b31f` z(kP;y=11Qi2}E+4ef!t424`TlMo&-t$llVh6v<@74@R7x?~&qW&=Wh{$0mA&p2|S1 zQVuu!a9L+;JmcxBtqJW*2PeO-L{q^)WM$?M{a_6=UQ}@R*pL*NnGtvIb@6r>4-Mk_ zR4hANzksIgsZHKGrR%4t9X)hj-BInMDAB;IIa*}zooKs()wmGTKpn+*u z18JXi-wpK6?(WNBaYwC|YB_f%mt_@dMXi&jlIWLEY#V!yy_5!v9PlzfOLF z6v|vL2P{zbkDx;)AWJ?$#cLee)lFyRAOum;Si7))k@E5~B6W$AdZ}`uJ$3ou-{e#etg-W3}LT5t$(f>61`OzO8 z{qv(2M`fA~9}oZm5C8!X009sH0T2LzKQjd6DZg+|Jbg*6%5=lEMqdPG?n=-)=B|tU zVq1c~no}*=64o)7cyW?GdK9Nw8EUa;6+jPoTI{4>$cS^U@^5kPMqZ`v&5&J<Xn@OPLBD7)8dq? zBFV1cl@oqJu~e`D+K+|a65xtbw>ufepiNQ$Suw!|o zEn(NrCG1++@r%P9_QR7|HSAzE=oez*8CPTZYcpQE0_$a;p|*@&gC%2E!B0LV`Gr@- zQ>`Xh0<02#be`@TSnRM;=N%M}U6m7ap6(j3rS1BhG>=`W6Y~n)Fkl-9txt9mJgj=> z^eo*YkPdR_Hc2`VW%}l7D~Za(yqTPY1iJ8oTZxr=7O9atGg}6YHrCe z|365=0{Lz7PsraVKTR5BgIpvsjlu^6KmY_l00ck)1V8`;KmY_l00fRGfn#*NVPb58 zErQW!_v}lis|%+mXIyJbY*|S+oyDc&bZud5a*Rr}Q>N&G!qoJXy<*e)vcJ#L^@B5K zPP>;?Tq`Pu>-acbJD8Z5U<)DaX+HZNW3LiSMEJ4=i{l5=bVXohV#ZlaU@6Yo6kP^5 z6`6Lq|MWeCmj6FYdooBi1@gOOlYB(JPJWgAEcq+s`{`=|_<#TifB*=900@8p2!H?x zfB*=900dQ^s_Qtu_0cv`#pwHE5J&GUUtb8md zI~_jZ6VHepZzcq-C_4}L%7=(gJSBQrCVnn)lD#h(1gv;FtK-4@Ift%y2Ylnc;5o7D zH32V=vXdwuI00@8p2!H?xfB*=900@8p2(ah>B6(M!|L_3;5C8!X009sH0T2KI z5C8!X009tqG6K7=h#_J3)#cz=D6|+1F4yz-id)H?v8^W43ok3_L?ZF>quYy%H?BNL zJV@5pUw*Xn-qrh>xpwz*btS#geCx8A&K4hCzMoD$xcTnp+sYi01V8`; zKmY_l00ck)1V8`;KmY`K3E=(zUM7eE0T2KI5C8!X009sH0T2KI5CDOrMgY(Mk6MqS zJsW+{ zgF!t1hna!^2!H?xfB*=900@8p2!H?xfWXlw0RR8!_c2-s0w4eaAOHd&00JNY0w4ea zAOHgJ|6v0l00JNY0w4eaAOHd&00JNY0w8eo3Bdn9`hAQRf&d7B00@8p2!H?xfB*=9 z00@8p{D0U02!H?xfB*=900@8p2!H?xfB*;_eFE_RkA5Gcg&+U|AOHd&00JNY0w4ea zAOHd&5TXb|p}#56-~0P%QXv<~MEF04KOg?luo_N=J)wU^lkfon5C8!X009sH0T2KI z5C8!X_(TZIg@VHJ@l(N&7)&OVbS#mKDQP*GxR6d?P?GUvVm`T$R#KO>e6v*2t0gHm z$}&v#&5&4#&o8AG=QB$3VzXG(jfxZ>VdB$$#OE)}XW~jiNiNJQ%A0ycEvpYzDK}ZELrk{6Dhe%naXvl2 zxOAmfGxKV>EJY)P;01zMw2+7L>*8#`<;Xe1M7eW0JZspNyyHmBqx;!iv_Y zsMUwkIX{!0>|1au6;D!x3#oLr*{IbuDe4=nMRFld;ms#g^Kag~cu9(RnP|WEr7kF` z_5B5%aR-P2vFmKwEY-N+@hOe&+!Z#fED2`>y% zt1Dl(GFCEU#*Zvq!i1&6On)6(4eQon;s705`3CFYV0dXce9QPmV!>#x=omx zS5NYIA9Z?!+QY!5RF6z1FJ8?aG?P%CvQ8UT z(i-yG^;`1V#?`CxJL{|0me)7sx3in_^2V*}t828#YuUA1=jGX!3HRn&&tA!{XV)%e zZ@MaFKvYNw@XyZS#Jl)p~taexNoA+iK%%GM$dL zJBYTqs#dhYsWfX@*C^zoYHFrl(b`#~?|1@{bLYfO-ZI;24efqYs}{7bQ*TGBx)RSa z4bIb?(Y=6AilkEF;k zCqj209z2ZuG|w5zbVe;|`*v6xcXzE6dOLG8`nnW|+&DLgdlwRWxH|-;-D5*iWM)R( zz31@e69Lc3kB0hC<_XUL@RAF#Y_&L=3I-x8GyArPS8q3l9|!lAhNVa*BYrT_)sCFg zSi@P91rH)yztQuyHx^{ZC&-H-9(2bnY zqz$uF$k%G+_SJ%>ve?c+n(8%JDz~LKXf=Z+sR&nOUkRaNLovGbb86gAbYty#HS zNt&V-O#K19Bz7mUD`0Mi-jwyf94eteBztP#o@WGHHwmG=@vsz$#l*+o-Pym^9lABb zcph$XMtDGP+u~2v>Ad*E4JsyT*TKeAdldBEi}c*Tb&bdNY}~5dU0gI8k}e68!fyye z8$&-4{QB_R@aIO>NB+m?gVFyJ`nm9J>C4he;F=Whf7SPQ{XgZ`ydQWz_LRkM3cn%# z!zcC6KUw+RyRS%*>1pw?WOX;ahse{>#O?st_t~!QwWFV^TTR_8?{imYl$#}In0MR} z+7}MmBl%kK;jC>W0 zPei}8B6WZ&xy6O8L}or;=t+6i24$~=^rW|QS{_(WIwMLWEf0S5WlM`RceZ+D+umGH z$}0zH(efVm(W3iqB)S_oZK)F-ppLqgqRW$uy->~^q)sd6KI&L2cAU%JnOW|*JbuV+ zwWswx5ifUIZ%_JDnX#i#&m=<)w3}X!jO?C>_P>Lq`W}1}>NKh~4@P$vPVrm5y~Qq0 zj_k}JGEar1^Qzb3bT~t|bA0y`il%1vyD@aM&n2DX(<%9onG`3}?DxMUMP7@FyVe?+ z-3^Au7guvdElro`7LuLOa;GtOCbsifmfV@hm}>9W^@f(?GZpQiPf3wzRNQ-&cTlHv z=h;i09o3o03T`QVPoooK>n3U5eG}vI!l{9_{9aL%LOdMh;Rp|hc^Ks35Dz6D26*V_p^t}N z76v^&uNVwk{{I&Q@*VPrP6z~>E$Ar_3XV1xz3EC{k-hy@Z00xa;ez{dhF3p`$LFv#Bj7sFo? z!e1hRKkc@leh>fw5C8!X009sH0T2KI5C8!X*q^}OqQ`&j@n2XjJj|rGs@siJ#=L(| zt7(l)erZ1OP;F!yrR}Zv@87+Aw{mx9ZTs%ZX6o+BTKW2|)zs##a{2n@?R(c&vgzw9 zYx>%w<@c`LDy6PH+DOpzox3~r#np0VyLe~4b|-cF(cL?5ZK*3)9;ugB7gl#xQ)@eg z_ttK$rmwxr%420^A6|cVV}7%8V}5Oi{CT|9klF{}GQs{tNlnQL#`6m5Qv}&o?Iyf=v6Q-sv zHj72wsN}7$@e!XeJ^iLm|Kh*;P~En_PYg53>;tW#8K!1^g@ZmJ5?RygYPrgS@gbJ; zO08z*>8}NB+25m*Pnej+^Wvp#BVQQzvU0E0npIQPs}=kEgol-MMX###_kp*puW^w{zEh*Wc&!(Vy8Uha z{l9I2JfJ@RXUVUVAESQ(@aM>{kbg<`$iE|ho&3)u(8Xv12!H?xfB*=900@8p2!H?x zfB*=5LIgtIQ(}9KAvEHh677|R(NXWT*jiB-VF~s^!f?FB{?Ihs-zrBNi`~Q6J0Qdh}TL!rQ z&vpWE|KHvv!2N$~djR+U+0Fp&|F<>-aR1-l2*CaS*7JY1|KCG?S0MkJ2Kayg2!H?x zfB*=900@8p2!H?xfB*;_0RlrFzaWaDC+G=?eu4fAOHd&00JNY0w4ea zAOHd&00PYY4|;rFJpYF+fB*=900@8p2!H?xfB*=900@A<5hma~|L5=j5A6t{1P$;3 z0T2KI5C8!X0D)&p;D;`GBw=OcuYS++I}KGcw5ll=YL!Z@Sd$CQhS4Q{JLKuw+G5RKrl^ou*;dWJ5P)wW66a&DS*I@}*`&&Ff~fA?M2~EybjjR_e6U zI<3LfX(e&_a;>TrWW6YB^tYAdYON|)be^utrH1-IFRHR?%36t*N=xRYHdMo|uvXM- z=UYu;P1#ZldRaH9+Isao%Vn5UnCHIVR4c9On&q-uDbyNutsz(0iB&NxUsjt1*7~gm z{XHw4CdK7_k)M|zXl0p_Eo-#Z_nR7pWRw{-Lo1lH#D=Vy@z8K+c#YR8-)~awI3P=} zW}~Xgm0FpqE!XIAL*pfCO}VVqS_(f2pRB2?6&sRb7?ydS0s*S!Z)FhE{YpCN4i+Gp^Mdd7ZV$YWjIwKW2R{Qfh(WG)!ut zWvT(|_(qKy(rqV>k|^t?Chan2L@KpJ2F`AbOxsKQl;eH|y(WM1L&ok5tM{p`Fq?gc zj#N6>G;UqCiLj1s^%#?)*s5ApH7N@R;FE)U7PfBvV0tp9q~&DdLOOLJp~MrZbY>~_ zgV#h!sNDOZSC^l(L!CCo^kU31fEYJ`7&m~JZ2&RL0IsQ8fzmLjJrw048xDL5utwbj zwV~5--kOrylNO(Q*en{CZ_;e_n$e^`O4nhZ)C`$XmTP%xdpb4DKb~1X3lnWOb&F11 zY-VqbZ=G69gN|c97@ly9Ut&{iMWu5zpa0u4S-shy6|{-9uLA8!wMTB2&4ymkHD=}8dVx&?^lD_!j_iU{k&RkGr)tx&P-C-kosSB}gz$Ml3sAS>^XTMm5MJY%0fD2z5oC70{MCJ y&!6e#9)*Db2!H?xfB*=900@8p2!H?xfB* +{% endblock %} + +{% block extrahead %} + + +{% endblock %} + +{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block branding %} +

{{ site_header|default:_('Django Admin') }}

+{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block footer %} + + + + +{% endblock %} \ No newline at end of file diff --git a/example_app/example_app/urls.py b/example_app/example_app/urls.py new file mode 100644 index 0000000..c8fe527 --- /dev/null +++ b/example_app/example_app/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'example_app.views.home', name='home'), + + url(r'^taggit/', include('taggit_selectize.urls')), + url(r'^admin/', include(admin.site.urls)), +) diff --git a/example_app/example_app/wsgi.py b/example_app/example_app/wsgi.py new file mode 100644 index 0000000..11c596e --- /dev/null +++ b/example_app/example_app/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for example_app project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_app.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/example_app/manage.py b/example_app/manage.py new file mode 100755 index 0000000..c51d3b4 --- /dev/null +++ b/example_app/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_app.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2be41ab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +django>=1.5.1 +django-taggit==0.12.2 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..06404dd --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +import taggit_selectize + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +version = taggit_selectize.__version__ + +if sys.argv[-1] == 'publish': + os.system('python setup.py sdist upload') + print("You probably want to also tag the version now:") + print(" git tag -a %s -m 'version %s'" % (version, version)) + print(" git push --tags") + sys.exit() + +readme = open('README.rst').read() +history = open('HISTORY.rst').read().replace('.. :changelog:', '') + +setup( + name='taggit-selectize', + version=version, + description="""django-taggit + selectize.js""", + long_description=readme + '\n\n' + history, + author='Nar Chhantyal', + author_email='nkchhantyal@gmail.com', + url='https://github.com/chhantyal/taggit-selectize', + packages=[ + 'taggit_selectize', + ], + include_package_data=True, + install_requires=[ + ], + license="BSD", + zip_safe=False, + keywords='taggit-selectize', + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + ], +) \ No newline at end of file diff --git a/taggit_selectize/__init__.py b/taggit_selectize/__init__.py new file mode 100644 index 0000000..541f859 --- /dev/null +++ b/taggit_selectize/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' \ No newline at end of file diff --git a/taggit_selectize/conf.py b/taggit_selectize/conf.py new file mode 100644 index 0000000..1f477a9 --- /dev/null +++ b/taggit_selectize/conf.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings + +settings.TAGGIT_SELECTIZE_RECOMMENDATION_LIMIT = getattr(settings, + 'TAGGIT_SELECTIZE_RECOMMENDATION_LIMIT', 10 +) \ No newline at end of file diff --git a/taggit_selectize/models.py b/taggit_selectize/models.py new file mode 100644 index 0000000..7c68785 --- /dev/null +++ b/taggit_selectize/models.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/taggit_selectize/static/css/selectize.default.css b/taggit_selectize/static/css/selectize.default.css new file mode 100755 index 0000000..c1ffd7e --- /dev/null +++ b/taggit_selectize/static/css/selectize.default.css @@ -0,0 +1,387 @@ +/** + * selectize.default.css (v0.12.0) - Default Theme + * Copyright (c) 2013–2015 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ +.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { + visibility: visible !important; + background: #f2f2f2 !important; + background: rgba(0, 0, 0, 0.06) !important; + border: 0 none !important; + -webkit-box-shadow: inset 0 0 12px 4px #ffffff; + box-shadow: inset 0 0 12px 4px #ffffff; +} +.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { + content: '!'; + visibility: hidden; +} +.selectize-control.plugin-drag_drop .ui-sortable-helper { + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} +.selectize-dropdown-header { + position: relative; + padding: 5px 8px; + border-bottom: 1px solid #d0d0d0; + background: #f8f8f8; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-dropdown-header-close { + position: absolute; + right: 8px; + top: 50%; + color: #303030; + opacity: 0.4; + margin-top: -12px; + line-height: 20px; + font-size: 20px !important; +} +.selectize-dropdown-header-close:hover { + color: #000000; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup { + border-right: 1px solid #f2f2f2; + border-top: 0 none; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { + border-right: 0 none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:before { + display: none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup-header { + border-top: 0 none; +} +.selectize-control.plugin-remove_button [data-value] { + position: relative; + padding-right: 24px !important; +} +.selectize-control.plugin-remove_button [data-value] .remove { + z-index: 1; + /* fixes ie bug (see #392) */ + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 17px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: inherit; + text-decoration: none; + vertical-align: middle; + display: inline-block; + padding: 2px 0 0 0; + border-left: 1px solid #0073bb; + -webkit-border-radius: 0 2px 2px 0; + -moz-border-radius: 0 2px 2px 0; + border-radius: 0 2px 2px 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-control.plugin-remove_button [data-value] .remove:hover { + background: rgba(0, 0, 0, 0.05); +} +.selectize-control.plugin-remove_button [data-value].active .remove { + border-left-color: #00578d; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { + background: none; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove { + border-left-color: #aaaaaa; +} +.selectize-control { + position: relative; +} +.selectize-dropdown, +.selectize-input, +.selectize-input input { + color: #303030; + font-family: inherit; + font-size: 13px; + line-height: 18px; + -webkit-font-smoothing: inherit; +} +.selectize-input, +.selectize-control.single .selectize-input.input-active { + background: #ffffff; + cursor: text; + display: inline-block; +} +.selectize-input { + border: 1px solid #d0d0d0; + padding: 8px 8px; + display: inline-block; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.selectize-control.multi .selectize-input.has-items { + padding: 5px 8px 2px; +} +.selectize-input.full { + background-color: #ffffff; +} +.selectize-input.disabled, +.selectize-input.disabled * { + cursor: default !important; +} +.selectize-input.focus { + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); +} +.selectize-input.dropdown-active { + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-input > * { + vertical-align: baseline; + display: -moz-inline-stack; + display: inline-block; + zoom: 1; + *display: inline; +} +.selectize-control.multi .selectize-input > div { + cursor: pointer; + margin: 0 3px 3px 0; + padding: 2px 6px; + background: #1da7ee; + color: #ffffff; + border: 1px solid #0073bb; +} +.selectize-control.multi .selectize-input > div.active { + background: #92c836; + color: #ffffff; + border: 1px solid #00578d; +} +.selectize-control.multi .selectize-input.disabled > div, +.selectize-control.multi .selectize-input.disabled > div.active { + color: #ffffff; + background: #d2d2d2; + border: 1px solid #aaaaaa; +} +.selectize-input > input { + display: inline-block !important; + padding: 0 !important; + min-height: 0 !important; + max-height: none !important; + max-width: 100% !important; + margin: 0 1px !important; + text-indent: 0 !important; + border: 0 none !important; + background: none !important; + line-height: inherit !important; + -webkit-user-select: auto !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.selectize-input > input::-ms-clear { + display: none; +} +.selectize-input > input:focus { + outline: none !important; +} +.selectize-input::after { + content: ' '; + display: block; + clear: left; +} +.selectize-input.dropdown-active::before { + content: ' '; + display: block; + position: absolute; + background: #f0f0f0; + height: 1px; + bottom: 0; + left: 0; + right: 0; +} +.selectize-dropdown { + position: absolute; + z-index: 10; + border: 1px solid #d0d0d0; + background: #ffffff; + margin: -1px 0 0 0; + border-top: 0 none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.selectize-dropdown [data-selectable] { + cursor: pointer; + overflow: hidden; +} +.selectize-dropdown [data-selectable] .highlight { + background: rgba(125, 168, 208, 0.2); + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; +} +.selectize-dropdown [data-selectable], +.selectize-dropdown .optgroup-header { + padding: 5px 8px; +} +.selectize-dropdown .optgroup:first-child .optgroup-header { + border-top: 0 none; +} +.selectize-dropdown .optgroup-header { + color: #303030; + background: #ffffff; + cursor: default; +} +.selectize-dropdown .active { + background-color: #f5fafd; + color: #495c68; +} +.selectize-dropdown .active.create { + color: #495c68; +} +.selectize-dropdown .create { + color: rgba(48, 48, 48, 0.5); +} +.selectize-dropdown-content { + overflow-y: auto; + overflow-x: hidden; + max-height: 200px; +} +.selectize-control.single .selectize-input, +.selectize-control.single .selectize-input input { + cursor: pointer; +} +.selectize-control.single .selectize-input.input-active, +.selectize-control.single .selectize-input.input-active input { + cursor: text; +} +.selectize-control.single .selectize-input:after { + content: ' '; + display: block; + position: absolute; + top: 50%; + right: 15px; + margin-top: -3px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #808080 transparent transparent transparent; +} +.selectize-control.single .selectize-input.dropdown-active:after { + margin-top: -4px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent #808080 transparent; +} +.selectize-control.rtl.single .selectize-input:after { + left: 15px; + right: auto; +} +.selectize-control.rtl .selectize-input > input { + margin: 0 4px 0 -2px !important; +} +.selectize-control .selectize-input.disabled { + opacity: 0.5; + background-color: #fafafa; +} +.selectize-control.multi .selectize-input.has-items { + padding-left: 5px; + padding-right: 5px; +} +.selectize-control.multi .selectize-input.disabled [data-value] { + color: #999; + text-shadow: none; + background: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.selectize-control.multi .selectize-input.disabled [data-value], +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + border-color: #e6e6e6; +} +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + background: none; +} +.selectize-control.multi .selectize-input [data-value] { + text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #1b9dec; + background-image: -moz-linear-gradient(top, #1da7ee, #178ee9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9)); + background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9); + background-image: -o-linear-gradient(top, #1da7ee, #178ee9); + background-image: linear-gradient(to bottom, #1da7ee, #178ee9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0); + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); + box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); +} +.selectize-control.multi .selectize-input [data-value].active { + background-color: #0085d4; + background-image: -moz-linear-gradient(top, #008fd8, #0075cf); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf)); + background-image: -webkit-linear-gradient(top, #008fd8, #0075cf); + background-image: -o-linear-gradient(top, #008fd8, #0075cf); + background-image: linear-gradient(to bottom, #008fd8, #0075cf); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0); +} +.selectize-control.single .selectize-input { + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + background-color: #f9f9f9; + background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -o-linear-gradient(top, #fefefe, #f2f2f2); + background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); +} +.selectize-control.single .selectize-input, +.selectize-dropdown.single { + border-color: #b8b8b8; +} +.selectize-dropdown .optgroup-header { + padding-top: 7px; + font-weight: bold; + font-size: 0.85em; +} +.selectize-dropdown .optgroup { + border-top: 1px solid #f0f0f0; +} +.selectize-dropdown .optgroup:first-child { + border-top: 0 none; +} diff --git a/taggit_selectize/static/img/.gitignore b/taggit_selectize/static/img/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/taggit_selectize/static/js/selectize.js b/taggit_selectize/static/js/selectize.js new file mode 100755 index 0000000..a578bab --- /dev/null +++ b/taggit_selectize/static/js/selectize.js @@ -0,0 +1,3668 @@ +/** + * sifter.js + * Copyright (c) 2013 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define('sifter', factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.Sifter = factory(); + } +}(this, function() { + + /** + * Textually searches arrays and hashes of objects + * by property (or multiple properties). Designed + * specifically for autocomplete. + * + * @constructor + * @param {array|object} items + * @param {object} items + */ + var Sifter = function(items, settings) { + this.items = items; + this.settings = settings || {diacritics: true}; + }; + + /** + * Splits a search string into an array of individual + * regexps to be used to match results. + * + * @param {string} query + * @returns {array} + */ + Sifter.prototype.tokenize = function(query) { + query = trim(String(query || '').toLowerCase()); + if (!query || !query.length) return []; + + var i, n, regex, letter; + var tokens = []; + var words = query.split(/ +/); + + for (i = 0, n = words.length; i < n; i++) { + regex = escape_regex(words[i]); + if (this.settings.diacritics) { + for (letter in DIACRITICS) { + if (DIACRITICS.hasOwnProperty(letter)) { + regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]); + } + } + } + tokens.push({ + string : words[i], + regex : new RegExp(regex, 'i') + }); + } + + return tokens; + }; + + /** + * Iterates over arrays and hashes. + * + * ``` + * this.iterator(this.items, function(item, id) { + * // invoked for each item + * }); + * ``` + * + * @param {array|object} object + */ + Sifter.prototype.iterator = function(object, callback) { + var iterator; + if (is_array(object)) { + iterator = Array.prototype.forEach || function(callback) { + for (var i = 0, n = this.length; i < n; i++) { + callback(this[i], i, this); + } + }; + } else { + iterator = function(callback) { + for (var key in this) { + if (this.hasOwnProperty(key)) { + callback(this[key], key, this); + } + } + }; + } + + iterator.apply(object, [callback]); + }; + + /** + * Returns a function to be used to score individual results. + * + * Good matches will have a higher score than poor matches. + * If an item is not a match, 0 will be returned by the function. + * + * @param {object|string} search + * @param {object} options (optional) + * @returns {function} + */ + Sifter.prototype.getScoreFunction = function(search, options) { + var self, fields, tokens, token_count; + + self = this; + search = self.prepareSearch(search, options); + tokens = search.tokens; + fields = search.options.fields; + token_count = tokens.length; + + /** + * Calculates how close of a match the + * given value is against a search token. + * + * @param {mixed} value + * @param {object} token + * @return {number} + */ + var scoreValue = function(value, token) { + var score, pos; + + if (!value) return 0; + value = String(value || ''); + pos = value.search(token.regex); + if (pos === -1) return 0; + score = token.string.length / value.length; + if (pos === 0) score += 0.5; + return score; + }; + + /** + * Calculates the score of an object + * against the search query. + * + * @param {object} token + * @param {object} data + * @return {number} + */ + var scoreObject = (function() { + var field_count = fields.length; + if (!field_count) { + return function() { return 0; }; + } + if (field_count === 1) { + return function(token, data) { + return scoreValue(data[fields[0]], token); + }; + } + return function(token, data) { + for (var i = 0, sum = 0; i < field_count; i++) { + sum += scoreValue(data[fields[i]], token); + } + return sum / field_count; + }; + })(); + + if (!token_count) { + return function() { return 0; }; + } + if (token_count === 1) { + return function(data) { + return scoreObject(tokens[0], data); + }; + } + + if (search.options.conjunction === 'and') { + return function(data) { + var score; + for (var i = 0, sum = 0; i < token_count; i++) { + score = scoreObject(tokens[i], data); + if (score <= 0) return 0; + sum += score; + } + return sum / token_count; + }; + } else { + return function(data) { + for (var i = 0, sum = 0; i < token_count; i++) { + sum += scoreObject(tokens[i], data); + } + return sum / token_count; + }; + } + }; + + /** + * Returns a function that can be used to compare two + * results, for sorting purposes. If no sorting should + * be performed, `null` will be returned. + * + * @param {string|object} search + * @param {object} options + * @return function(a,b) + */ + Sifter.prototype.getSortFunction = function(search, options) { + var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort; + + self = this; + search = self.prepareSearch(search, options); + sort = (!search.query && options.sort_empty) || options.sort; + + /** + * Fetches the specified sort field value + * from a search result item. + * + * @param {string} name + * @param {object} result + * @return {mixed} + */ + get_field = function(name, result) { + if (name === '$score') return result.score; + return self.items[result.id][name]; + }; + + // parse options + fields = []; + if (sort) { + for (i = 0, n = sort.length; i < n; i++) { + if (search.query || sort[i].field !== '$score') { + fields.push(sort[i]); + } + } + } + + // the "$score" field is implied to be the primary + // sort field, unless it's manually specified + if (search.query) { + implicit_score = true; + for (i = 0, n = fields.length; i < n; i++) { + if (fields[i].field === '$score') { + implicit_score = false; + break; + } + } + if (implicit_score) { + fields.unshift({field: '$score', direction: 'desc'}); + } + } else { + for (i = 0, n = fields.length; i < n; i++) { + if (fields[i].field === '$score') { + fields.splice(i, 1); + break; + } + } + } + + multipliers = []; + for (i = 0, n = fields.length; i < n; i++) { + multipliers.push(fields[i].direction === 'desc' ? -1 : 1); + } + + // build function + fields_count = fields.length; + if (!fields_count) { + return null; + } else if (fields_count === 1) { + field = fields[0].field; + multiplier = multipliers[0]; + return function(a, b) { + return multiplier * cmp( + get_field(field, a), + get_field(field, b) + ); + }; + } else { + return function(a, b) { + var i, result, a_value, b_value, field; + for (i = 0; i < fields_count; i++) { + field = fields[i].field; + result = multipliers[i] * cmp( + get_field(field, a), + get_field(field, b) + ); + if (result) return result; + } + return 0; + }; + } + }; + + /** + * Parses a search query and returns an object + * with tokens and fields ready to be populated + * with results. + * + * @param {string} query + * @param {object} options + * @returns {object} + */ + Sifter.prototype.prepareSearch = function(query, options) { + if (typeof query === 'object') return query; + + options = extend({}, options); + + var option_fields = options.fields; + var option_sort = options.sort; + var option_sort_empty = options.sort_empty; + + if (option_fields && !is_array(option_fields)) options.fields = [option_fields]; + if (option_sort && !is_array(option_sort)) options.sort = [option_sort]; + if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty]; + + return { + options : options, + query : String(query || '').toLowerCase(), + tokens : this.tokenize(query), + total : 0, + items : [] + }; + }; + + /** + * Searches through all items and returns a sorted array of matches. + * + * The `options` parameter can contain: + * + * - fields {string|array} + * - sort {array} + * - score {function} + * - filter {bool} + * - limit {integer} + * + * Returns an object containing: + * + * - options {object} + * - query {string} + * - tokens {array} + * - total {int} + * - items {array} + * + * @param {string} query + * @param {object} options + * @returns {object} + */ + Sifter.prototype.search = function(query, options) { + var self = this, value, score, search, calculateScore; + var fn_sort; + var fn_score; + + search = this.prepareSearch(query, options); + options = search.options; + query = search.query; + + // generate result scoring function + fn_score = options.score || self.getScoreFunction(search); + + // perform search and sort + if (query.length) { + self.iterator(self.items, function(item, id) { + score = fn_score(item); + if (options.filter === false || score > 0) { + search.items.push({'score': score, 'id': id}); + } + }); + } else { + self.iterator(self.items, function(item, id) { + search.items.push({'score': 1, 'id': id}); + }); + } + + fn_sort = self.getSortFunction(search, options); + if (fn_sort) search.items.sort(fn_sort); + + // apply limits + search.total = search.items.length; + if (typeof options.limit === 'number') { + search.items = search.items.slice(0, options.limit); + } + + return search; + }; + + // utilities + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + var cmp = function(a, b) { + if (typeof a === 'number' && typeof b === 'number') { + return a > b ? 1 : (a < b ? -1 : 0); + } + a = asciifold(String(a || '')); + b = asciifold(String(b || '')); + if (a > b) return 1; + if (b > a) return -1; + return 0; + }; + + var extend = function(a, b) { + var i, n, k, object; + for (i = 1, n = arguments.length; i < n; i++) { + object = arguments[i]; + if (!object) continue; + for (k in object) { + if (object.hasOwnProperty(k)) { + a[k] = object[k]; + } + } + } + return a; + }; + + var trim = function(str) { + return (str + '').replace(/^\s+|\s+$|/g, ''); + }; + + var escape_regex = function(str) { + return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + }; + + var is_array = Array.isArray || ($ && $.isArray) || function(object) { + return Object.prototype.toString.call(object) === '[object Array]'; + }; + + var DIACRITICS = { + 'a': '[aÀÁÂÃÄÅàáâãäåĀāąĄ]', + 'c': '[cÇçćĆčČ]', + 'd': '[dđĐďĎ]', + 'e': '[eÈÉÊËèéêëěĚĒēęĘ]', + 'i': '[iÌÍÎÏìíîïĪī]', + 'l': '[lłŁ]', + 'n': '[nÑñňŇńŃ]', + 'o': '[oÒÓÔÕÕÖØòóôõöøŌō]', + 'r': '[rřŘ]', + 's': '[sŠšśŚ]', + 't': '[tťŤ]', + 'u': '[uÙÚÛÜùúûüůŮŪū]', + 'y': '[yŸÿýÝ]', + 'z': '[zŽžżŻźŹ]' + }; + + var asciifold = (function() { + var i, n, k, chunk; + var foreignletters = ''; + var lookup = {}; + for (k in DIACRITICS) { + if (DIACRITICS.hasOwnProperty(k)) { + chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1); + foreignletters += chunk; + for (i = 0, n = chunk.length; i < n; i++) { + lookup[chunk.charAt(i)] = k; + } + } + } + var regexp = new RegExp('[' + foreignletters + ']', 'g'); + return function(str) { + return str.replace(regexp, function(foreignletter) { + return lookup[foreignletter]; + }).toLowerCase(); + }; + })(); + + + // export + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + return Sifter; +})); + + + +/** + * microplugin.js + * Copyright (c) 2013 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define('microplugin', factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.MicroPlugin = factory(); + } +}(this, function() { + var MicroPlugin = {}; + + MicroPlugin.mixin = function(Interface) { + Interface.plugins = {}; + + /** + * Initializes the listed plugins (with options). + * Acceptable formats: + * + * List (without options): + * ['a', 'b', 'c'] + * + * List (with options): + * [{'name': 'a', options: {}}, {'name': 'b', options: {}}] + * + * Hash (with options): + * {'a': { ... }, 'b': { ... }, 'c': { ... }} + * + * @param {mixed} plugins + */ + Interface.prototype.initializePlugins = function(plugins) { + var i, n, key; + var self = this; + var queue = []; + + self.plugins = { + names : [], + settings : {}, + requested : {}, + loaded : {} + }; + + if (utils.isArray(plugins)) { + for (i = 0, n = plugins.length; i < n; i++) { + if (typeof plugins[i] === 'string') { + queue.push(plugins[i]); + } else { + self.plugins.settings[plugins[i].name] = plugins[i].options; + queue.push(plugins[i].name); + } + } + } else if (plugins) { + for (key in plugins) { + if (plugins.hasOwnProperty(key)) { + self.plugins.settings[key] = plugins[key]; + queue.push(key); + } + } + } + + while (queue.length) { + self.require(queue.shift()); + } + }; + + Interface.prototype.loadPlugin = function(name) { + var self = this; + var plugins = self.plugins; + var plugin = Interface.plugins[name]; + + if (!Interface.plugins.hasOwnProperty(name)) { + throw new Error('Unable to find "' + name + '" plugin'); + } + + plugins.requested[name] = true; + plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]); + plugins.names.push(name); + }; + + /** + * Initializes a plugin. + * + * @param {string} name + */ + Interface.prototype.require = function(name) { + var self = this; + var plugins = self.plugins; + + if (!self.plugins.loaded.hasOwnProperty(name)) { + if (plugins.requested[name]) { + throw new Error('Plugin has circular dependency ("' + name + '")'); + } + self.loadPlugin(name); + } + + return plugins.loaded[name]; + }; + + /** + * Registers a plugin. + * + * @param {string} name + * @param {function} fn + */ + Interface.define = function(name, fn) { + Interface.plugins[name] = { + 'name' : name, + 'fn' : fn + }; + }; + }; + + var utils = { + isArray: Array.isArray || function(vArg) { + return Object.prototype.toString.call(vArg) === '[object Array]'; + } + }; + + return MicroPlugin; +})); + +/** + * selectize.js (v0.12.0) + * Copyright (c) 2013–2015 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ + +/*jshint curly:false */ +/*jshint browser:true */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define('selectize', ['jquery','sifter','microplugin'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('jquery'), require('sifter'), require('microplugin')); + } else { + root.Selectize = factory(root.jQuery, root.Sifter, root.MicroPlugin); + } +}(this, function($, Sifter, MicroPlugin) { + 'use strict'; + + var highlight = function($element, pattern) { + if (typeof pattern === 'string' && !pattern.length) return; + var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern; + + var highlight = function(node) { + var skip = 0; + if (node.nodeType === 3) { + var pos = node.data.search(regex); + if (pos >= 0 && node.data.length > 0) { + var match = node.data.match(regex); + var spannode = document.createElement('span'); + spannode.className = 'highlight'; + var middlebit = node.splitText(pos); + var endbit = middlebit.splitText(match[0].length); + var middleclone = middlebit.cloneNode(true); + spannode.appendChild(middleclone); + middlebit.parentNode.replaceChild(spannode, middlebit); + skip = 1; + } + } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { + for (var i = 0; i < node.childNodes.length; ++i) { + i += highlight(node.childNodes[i]); + } + } + return skip; + }; + + return $element.each(function() { + highlight(this); + }); + }; + + var MicroEvent = function() {}; + MicroEvent.prototype = { + on: function(event, fct){ + this._events = this._events || {}; + this._events[event] = this._events[event] || []; + this._events[event].push(fct); + }, + off: function(event, fct){ + var n = arguments.length; + if (n === 0) return delete this._events; + if (n === 1) return delete this._events[event]; + + this._events = this._events || {}; + if (event in this._events === false) return; + this._events[event].splice(this._events[event].indexOf(fct), 1); + }, + trigger: function(event /* , args... */){ + this._events = this._events || {}; + if (event in this._events === false) return; + for (var i = 0; i < this._events[event].length; i++){ + this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + }; + + /** + * Mixin will delegate all MicroEvent.js function in the destination object. + * + * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent + * + * @param {object} the object which will support MicroEvent + */ + MicroEvent.mixin = function(destObject){ + var props = ['on', 'off', 'trigger']; + for (var i = 0; i < props.length; i++){ + destObject.prototype[props[i]] = MicroEvent.prototype[props[i]]; + } + }; + + var IS_MAC = /Mac/.test(navigator.userAgent); + + var KEY_A = 65; + var KEY_COMMA = 188; + var KEY_RETURN = 13; + var KEY_ESC = 27; + var KEY_LEFT = 37; + var KEY_UP = 38; + var KEY_P = 80; + var KEY_RIGHT = 39; + var KEY_DOWN = 40; + var KEY_N = 78; + var KEY_BACKSPACE = 8; + var KEY_DELETE = 46; + var KEY_SHIFT = 16; + var KEY_CMD = IS_MAC ? 91 : 17; + var KEY_CTRL = IS_MAC ? 18 : 17; + var KEY_TAB = 9; + + var TAG_SELECT = 1; + var TAG_INPUT = 2; + + // for now, android support in general is too spotty to support validity + var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('form').validity; + + var isset = function(object) { + return typeof object !== 'undefined'; + }; + + /** + * Converts a scalar to its best string representation + * for hash keys and HTML attribute values. + * + * Transformations: + * 'str' -> 'str' + * null -> '' + * undefined -> '' + * true -> '1' + * false -> '0' + * 0 -> '0' + * 1 -> '1' + * + * @param {string} value + * @returns {string|null} + */ + var hash_key = function(value) { + if (typeof value === 'undefined' || value === null) return null; + if (typeof value === 'boolean') return value ? '1' : '0'; + return value + ''; + }; + + /** + * Escapes a string for use within HTML. + * + * @param {string} str + * @returns {string} + */ + var escape_html = function(str) { + return (str + '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + + /** + * Escapes "$" characters in replacement strings. + * + * @param {string} str + * @returns {string} + */ + var escape_replace = function(str) { + return (str + '').replace(/\$/g, '$$$$'); + }; + + var hook = {}; + + /** + * Wraps `method` on `self` so that `fn` + * is invoked before the original method. + * + * @param {object} self + * @param {string} method + * @param {function} fn + */ + hook.before = function(self, method, fn) { + var original = self[method]; + self[method] = function() { + fn.apply(self, arguments); + return original.apply(self, arguments); + }; + }; + + /** + * Wraps `method` on `self` so that `fn` + * is invoked after the original method. + * + * @param {object} self + * @param {string} method + * @param {function} fn + */ + hook.after = function(self, method, fn) { + var original = self[method]; + self[method] = function() { + var result = original.apply(self, arguments); + fn.apply(self, arguments); + return result; + }; + }; + + /** + * Wraps `fn` so that it can only be invoked once. + * + * @param {function} fn + * @returns {function} + */ + var once = function(fn) { + var called = false; + return function() { + if (called) return; + called = true; + fn.apply(this, arguments); + }; + }; + + /** + * Wraps `fn` so that it can only be called once + * every `delay` milliseconds (invoked on the falling edge). + * + * @param {function} fn + * @param {int} delay + * @returns {function} + */ + var debounce = function(fn, delay) { + var timeout; + return function() { + var self = this; + var args = arguments; + window.clearTimeout(timeout); + timeout = window.setTimeout(function() { + fn.apply(self, args); + }, delay); + }; + }; + + /** + * Debounce all fired events types listed in `types` + * while executing the provided `fn`. + * + * @param {object} self + * @param {array} types + * @param {function} fn + */ + var debounce_events = function(self, types, fn) { + var type; + var trigger = self.trigger; + var event_args = {}; + + // override trigger method + self.trigger = function() { + var type = arguments[0]; + if (types.indexOf(type) !== -1) { + event_args[type] = arguments; + } else { + return trigger.apply(self, arguments); + } + }; + + // invoke provided function + fn.apply(self, []); + self.trigger = trigger; + + // trigger queued events + for (type in event_args) { + if (event_args.hasOwnProperty(type)) { + trigger.apply(self, event_args[type]); + } + } + }; + + /** + * A workaround for http://bugs.jquery.com/ticket/6696 + * + * @param {object} $parent - Parent element to listen on. + * @param {string} event - Event name. + * @param {string} selector - Descendant selector to filter by. + * @param {function} fn - Event handler. + */ + var watchChildEvent = function($parent, event, selector, fn) { + $parent.on(event, selector, function(e) { + var child = e.target; + while (child && child.parentNode !== $parent[0]) { + child = child.parentNode; + } + e.currentTarget = child; + return fn.apply(this, [e]); + }); + }; + + /** + * Determines the current selection within a text input control. + * Returns an object containing: + * - start + * - length + * + * @param {object} input + * @returns {object} + */ + var getSelection = function(input) { + var result = {}; + if ('selectionStart' in input) { + result.start = input.selectionStart; + result.length = input.selectionEnd - result.start; + } else if (document.selection) { + input.focus(); + var sel = document.selection.createRange(); + var selLen = document.selection.createRange().text.length; + sel.moveStart('character', -input.value.length); + result.start = sel.text.length - selLen; + result.length = selLen; + } + return result; + }; + + /** + * Copies CSS properties from one element to another. + * + * @param {object} $from + * @param {object} $to + * @param {array} properties + */ + var transferStyles = function($from, $to, properties) { + var i, n, styles = {}; + if (properties) { + for (i = 0, n = properties.length; i < n; i++) { + styles[properties[i]] = $from.css(properties[i]); + } + } else { + styles = $from.css(); + } + $to.css(styles); + }; + + /** + * Measures the width of a string within a + * parent element (in pixels). + * + * @param {string} str + * @param {object} $parent + * @returns {int} + */ + var measureString = function(str, $parent) { + if (!str) { + return 0; + } + + var $test = $('').css({ + position: 'absolute', + top: -99999, + left: -99999, + width: 'auto', + padding: 0, + whiteSpace: 'pre' + }).text(str).appendTo('body'); + + transferStyles($parent, $test, [ + 'letterSpacing', + 'fontSize', + 'fontFamily', + 'fontWeight', + 'textTransform' + ]); + + var width = $test.width(); + $test.remove(); + + return width; + }; + + /** + * Sets up an input to grow horizontally as the user + * types. If the value is changed manually, you can + * trigger the "update" handler to resize: + * + * $input.trigger('update'); + * + * @param {object} $input + */ + var autoGrow = function($input) { + var currentWidth = null; + + var update = function(e, options) { + var value, keyCode, printable, placeholder, width; + var shift, character, selection; + e = e || window.event || {}; + options = options || {}; + + if (e.metaKey || e.altKey) return; + if (!options.force && $input.data('grow') === false) return; + + value = $input.val(); + if (e.type && e.type.toLowerCase() === 'keydown') { + keyCode = e.keyCode; + printable = ( + (keyCode >= 97 && keyCode <= 122) || // a-z + (keyCode >= 65 && keyCode <= 90) || // A-Z + (keyCode >= 48 && keyCode <= 57) || // 0-9 + keyCode === 32 // space + ); + + if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) { + selection = getSelection($input[0]); + if (selection.length) { + value = value.substring(0, selection.start) + value.substring(selection.start + selection.length); + } else if (keyCode === KEY_BACKSPACE && selection.start) { + value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1); + } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') { + value = value.substring(0, selection.start) + value.substring(selection.start + 1); + } + } else if (printable) { + shift = e.shiftKey; + character = String.fromCharCode(e.keyCode); + if (shift) character = character.toUpperCase(); + else character = character.toLowerCase(); + value += character; + } + } + + placeholder = $input.attr('placeholder'); + if (!value && placeholder) { + value = placeholder; + } + + width = measureString(value, $input) + 4; + if (width !== currentWidth) { + currentWidth = width; + $input.width(width); + $input.triggerHandler('resize'); + } + }; + + $input.on('keydown keyup update blur', update); + update(); + }; + + var Selectize = function($input, settings) { + var key, i, n, dir, input, self = this; + input = $input[0]; + input.selectize = self; + + // detect rtl environment + var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null); + dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction; + dir = dir || $input.parents('[dir]:first').attr('dir') || ''; + + // setup default state + $.extend(self, { + order : 0, + settings : settings, + $input : $input, + tabIndex : $input.attr('tabindex') || '', + tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT, + rtl : /rtl/i.test(dir), + + eventNS : '.selectize' + (++Selectize.count), + highlightedValue : null, + isOpen : false, + isDisabled : false, + isRequired : $input.is('[required]'), + isInvalid : false, + isLocked : false, + isFocused : false, + isInputHidden : false, + isSetup : false, + isShiftDown : false, + isCmdDown : false, + isCtrlDown : false, + ignoreFocus : false, + ignoreBlur : false, + ignoreHover : false, + hasOptions : false, + currentResults : null, + lastValue : '', + caretPos : 0, + loading : 0, + loadedSearches : {}, + + $activeOption : null, + $activeItems : [], + + optgroups : {}, + options : {}, + userOptions : {}, + items : [], + renderCache : {}, + onSearchChange : settings.loadThrottle === null ? self.onSearchChange : debounce(self.onSearchChange, settings.loadThrottle) + }); + + // search system + self.sifter = new Sifter(this.options, {diacritics: settings.diacritics}); + + // build options table + if (self.settings.options) { + for (i = 0, n = self.settings.options.length; i < n; i++) { + self.registerOption(self.settings.options[i]); + } + delete self.settings.options; + } + + // build optgroup table + if (self.settings.optgroups) { + for (i = 0, n = self.settings.optgroups.length; i < n; i++) { + self.registerOptionGroup(self.settings.optgroups[i]); + } + delete self.settings.optgroups; + } + + // option-dependent defaults + self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi'); + if (typeof self.settings.hideSelected !== 'boolean') { + self.settings.hideSelected = self.settings.mode === 'multi'; + } + + self.initializePlugins(self.settings.plugins); + self.setupCallbacks(); + self.setupTemplates(); + self.setup(); + }; + + // mixins + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + MicroEvent.mixin(Selectize); + MicroPlugin.mixin(Selectize); + + // methods + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + $.extend(Selectize.prototype, { + + /** + * Creates all elements and sets up event bindings. + */ + setup: function() { + var self = this; + var settings = self.settings; + var eventNS = self.eventNS; + var $window = $(window); + var $document = $(document); + var $input = self.$input; + + var $wrapper; + var $control; + var $control_input; + var $dropdown; + var $dropdown_content; + var $dropdown_parent; + var inputMode; + var timeout_blur; + var timeout_focus; + var classes; + var classes_plugins; + + inputMode = self.settings.mode; + classes = $input.attr('class') || ''; + + $wrapper = $('
').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode); + $control = $('
').addClass(settings.inputClass).addClass('items').appendTo($wrapper); + $control_input = $('').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex); + $dropdown_parent = $(settings.dropdownParent || $wrapper); + $dropdown = $('
').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdown_parent); + $dropdown_content = $('
').addClass(settings.dropdownContentClass).appendTo($dropdown); + + if(self.settings.copyClassesToDropdown) { + $dropdown.addClass(classes); + } + + $wrapper.css({ + width: $input[0].style.width + }); + + if (self.plugins.names.length) { + classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-'); + $wrapper.addClass(classes_plugins); + $dropdown.addClass(classes_plugins); + } + + if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) { + $input.attr('multiple', 'multiple'); + } + + if (self.settings.placeholder) { + $control_input.attr('placeholder', settings.placeholder); + } + + // if splitOn was not passed in, construct it from the delimiter to allow pasting universally + if (!self.settings.splitOn && self.settings.delimiter) { + var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*'); + } + + if ($input.attr('autocorrect')) { + $control_input.attr('autocorrect', $input.attr('autocorrect')); + } + + if ($input.attr('autocapitalize')) { + $control_input.attr('autocapitalize', $input.attr('autocapitalize')); + } + + self.$wrapper = $wrapper; + self.$control = $control; + self.$control_input = $control_input; + self.$dropdown = $dropdown; + self.$dropdown_content = $dropdown_content; + + $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); }); + $dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); }); + watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); }); + autoGrow($control_input); + + $control.on({ + mousedown : function() { return self.onMouseDown.apply(self, arguments); }, + click : function() { return self.onClick.apply(self, arguments); } + }); + + $control_input.on({ + mousedown : function(e) { e.stopPropagation(); }, + keydown : function() { return self.onKeyDown.apply(self, arguments); }, + keyup : function() { return self.onKeyUp.apply(self, arguments); }, + keypress : function() { return self.onKeyPress.apply(self, arguments); }, + resize : function() { self.positionDropdown.apply(self, []); }, + blur : function() { return self.onBlur.apply(self, arguments); }, + focus : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); }, + paste : function() { return self.onPaste.apply(self, arguments); } + }); + + $document.on('keydown' + eventNS, function(e) { + self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey']; + self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey']; + self.isShiftDown = e.shiftKey; + }); + + $document.on('keyup' + eventNS, function(e) { + if (e.keyCode === KEY_CTRL) self.isCtrlDown = false; + if (e.keyCode === KEY_SHIFT) self.isShiftDown = false; + if (e.keyCode === KEY_CMD) self.isCmdDown = false; + }); + + $document.on('mousedown' + eventNS, function(e) { + if (self.isFocused) { + // prevent events on the dropdown scrollbar from causing the control to blur + if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) { + return false; + } + // blur on click outside + if (!self.$control.has(e.target).length && e.target !== self.$control[0]) { + self.blur(e.target); + } + } + }); + + $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() { + if (self.isOpen) { + self.positionDropdown.apply(self, arguments); + } + }); + $window.on('mousemove' + eventNS, function() { + self.ignoreHover = false; + }); + + // store original children and tab index so that they can be + // restored when the destroy() method is called. + this.revertSettings = { + $children : $input.children().detach(), + tabindex : $input.attr('tabindex') + }; + + $input.attr('tabindex', -1).hide().after(self.$wrapper); + + if ($.isArray(settings.items)) { + self.setValue(settings.items); + delete settings.items; + } + + // feature detect for the validation API + if (SUPPORTS_VALIDITY_API) { + $input.on('invalid' + eventNS, function(e) { + e.preventDefault(); + self.isInvalid = true; + self.refreshState(); + }); + } + + self.updateOriginalInput(); + self.refreshItems(); + self.refreshState(); + self.updatePlaceholder(); + self.isSetup = true; + + if ($input.is(':disabled')) { + self.disable(); + } + + self.on('change', this.onChange); + + $input.data('selectize', self); + $input.addClass('selectized'); + self.trigger('initialize'); + + // preload options + if (settings.preload === true) { + self.onSearchChange(''); + } + + }, + + /** + * Sets up default rendering functions. + */ + setupTemplates: function() { + var self = this; + var field_label = self.settings.labelField; + var field_optgroup = self.settings.optgroupLabelField; + + var templates = { + 'optgroup': function(data) { + return '
' + data.html + '
'; + }, + 'optgroup_header': function(data, escape) { + return '
' + escape(data[field_optgroup]) + '
'; + }, + 'option': function(data, escape) { + return '
' + escape(data[field_label]) + '
'; + }, + 'item': function(data, escape) { + return '
' + escape(data[field_label]) + '
'; + }, + 'option_create': function(data, escape) { + return '
Add ' + escape(data.input) + '
'; + } + }; + + self.settings.render = $.extend({}, templates, self.settings.render); + }, + + /** + * Maps fired events to callbacks provided + * in the settings used when creating the control. + */ + setupCallbacks: function() { + var key, fn, callbacks = { + 'initialize' : 'onInitialize', + 'change' : 'onChange', + 'item_add' : 'onItemAdd', + 'item_remove' : 'onItemRemove', + 'clear' : 'onClear', + 'option_add' : 'onOptionAdd', + 'option_remove' : 'onOptionRemove', + 'option_clear' : 'onOptionClear', + 'optgroup_add' : 'onOptionGroupAdd', + 'optgroup_remove' : 'onOptionGroupRemove', + 'optgroup_clear' : 'onOptionGroupClear', + 'dropdown_open' : 'onDropdownOpen', + 'dropdown_close' : 'onDropdownClose', + 'type' : 'onType', + 'load' : 'onLoad', + 'focus' : 'onFocus', + 'blur' : 'onBlur' + }; + + for (key in callbacks) { + if (callbacks.hasOwnProperty(key)) { + fn = this.settings[callbacks[key]]; + if (fn) this.on(key, fn); + } + } + }, + + /** + * Triggered when the main control element + * has a click event. + * + * @param {object} e + * @return {boolean} + */ + onClick: function(e) { + var self = this; + + // necessary for mobile webkit devices (manual focus triggering + // is ignored unless invoked within a click event) + if (!self.isFocused) { + self.focus(); + e.preventDefault(); + } + }, + + /** + * Triggered when the main control element + * has a mouse down event. + * + * @param {object} e + * @return {boolean} + */ + onMouseDown: function(e) { + var self = this; + var defaultPrevented = e.isDefaultPrevented(); + var $target = $(e.target); + + if (self.isFocused) { + // retain focus by preventing native handling. if the + // event target is the input it should not be modified. + // otherwise, text selection within the input won't work. + if (e.target !== self.$control_input[0]) { + if (self.settings.mode === 'single') { + // toggle dropdown + self.isOpen ? self.close() : self.open(); + } else if (!defaultPrevented) { + self.setActiveItem(null); + } + return false; + } + } else { + // give control focus + if (!defaultPrevented) { + window.setTimeout(function() { + self.focus(); + }, 0); + } + } + }, + + /** + * Triggered when the value of the control has been changed. + * This should propagate the event to the original DOM + * input / select element. + */ + onChange: function() { + this.$input.trigger('change'); + }, + + /** + * Triggered on paste. + * + * @param {object} e + * @returns {boolean} + */ + onPaste: function(e) { + var self = this; + if (self.isFull() || self.isInputHidden || self.isLocked) { + e.preventDefault(); + } else { + // If a regex or string is included, this will split the pasted + // input and create Items for each separate value + if (self.settings.splitOn) { + setTimeout(function() { + var splitInput = $.trim(self.$control_input.val() || '').split(self.settings.splitOn); + for (var i = 0, n = splitInput.length; i < n; i++) { + self.createItem(splitInput[i]); + } + }, 0); + } + } + }, + + /** + * Triggered on keypress. + * + * @param {object} e + * @returns {boolean} + */ + onKeyPress: function(e) { + if (this.isLocked) return e && e.preventDefault(); + var character = String.fromCharCode(e.keyCode || e.which); + if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) { + this.createItem(); + e.preventDefault(); + return false; + } + }, + + /** + * Triggered on keydown. + * + * @param {object} e + * @returns {boolean} + */ + onKeyDown: function(e) { + var isInput = e.target === this.$control_input[0]; + var self = this; + + if (self.isLocked) { + if (e.keyCode !== KEY_TAB) { + e.preventDefault(); + } + return; + } + + switch (e.keyCode) { + case KEY_A: + if (self.isCmdDown) { + self.selectAll(); + return; + } + break; + case KEY_ESC: + if (self.isOpen) { + e.preventDefault(); + e.stopPropagation(); + self.close(); + } + return; + case KEY_N: + if (!e.ctrlKey || e.altKey) break; + case KEY_DOWN: + if (!self.isOpen && self.hasOptions) { + self.open(); + } else if (self.$activeOption) { + self.ignoreHover = true; + var $next = self.getAdjacentOption(self.$activeOption, 1); + if ($next.length) self.setActiveOption($next, true, true); + } + e.preventDefault(); + return; + case KEY_P: + if (!e.ctrlKey || e.altKey) break; + case KEY_UP: + if (self.$activeOption) { + self.ignoreHover = true; + var $prev = self.getAdjacentOption(self.$activeOption, -1); + if ($prev.length) self.setActiveOption($prev, true, true); + } + e.preventDefault(); + return; + case KEY_RETURN: + if (self.isOpen && self.$activeOption) { + self.onOptionSelect({currentTarget: self.$activeOption}); + e.preventDefault(); + } + return; + case KEY_LEFT: + self.advanceSelection(-1, e); + return; + case KEY_RIGHT: + self.advanceSelection(1, e); + return; + case KEY_TAB: + if (self.settings.selectOnTab && self.isOpen && self.$activeOption) { + self.onOptionSelect({currentTarget: self.$activeOption}); + + // Default behaviour is to jump to the next field, we only want this + // if the current field doesn't accept any more entries + if (!self.isFull()) { + e.preventDefault(); + } + } + if (self.settings.create && self.createItem()) { + e.preventDefault(); + } + return; + case KEY_BACKSPACE: + case KEY_DELETE: + self.deleteSelection(e); + return; + } + + if ((self.isFull() || self.isInputHidden) && !(IS_MAC ? e.metaKey : e.ctrlKey)) { + e.preventDefault(); + return; + } + }, + + /** + * Triggered on keyup. + * + * @param {object} e + * @returns {boolean} + */ + onKeyUp: function(e) { + var self = this; + + if (self.isLocked) return e && e.preventDefault(); + var value = self.$control_input.val() || ''; + if (self.lastValue !== value) { + self.lastValue = value; + self.onSearchChange(value); + self.refreshOptions(); + self.trigger('type', value); + } + }, + + /** + * Invokes the user-provide option provider / loader. + * + * Note: this function is debounced in the Selectize + * constructor (by `settings.loadDelay` milliseconds) + * + * @param {string} value + */ + onSearchChange: function(value) { + var self = this; + var fn = self.settings.load; + if (!fn) return; + if (self.loadedSearches.hasOwnProperty(value)) return; + self.loadedSearches[value] = true; + self.load(function(callback) { + fn.apply(self, [value, callback]); + }); + }, + + /** + * Triggered on focus. + * + * @param {object} e (optional) + * @returns {boolean} + */ + onFocus: function(e) { + var self = this; + var wasFocused = self.isFocused; + + if (self.isDisabled) { + self.blur(); + e && e.preventDefault(); + return false; + } + + if (self.ignoreFocus) return; + self.isFocused = true; + if (self.settings.preload === 'focus') self.onSearchChange(''); + + if (!wasFocused) self.trigger('focus'); + + if (!self.$activeItems.length) { + self.showInput(); + self.setActiveItem(null); + self.refreshOptions(!!self.settings.openOnFocus); + } + + self.refreshState(); + }, + + /** + * Triggered on blur. + * + * @param {object} e + * @param {Element} dest + */ + onBlur: function(e, dest) { + var self = this; + if (!self.isFocused) return; + self.isFocused = false; + + if (self.ignoreFocus) { + return; + } else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) { + // necessary to prevent IE closing the dropdown when the scrollbar is clicked + self.ignoreBlur = true; + self.onFocus(e); + return; + } + + var deactivate = function() { + self.close(); + self.setTextboxValue(''); + self.setActiveItem(null); + self.setActiveOption(null); + self.setCaret(self.items.length); + self.refreshState(); + + // IE11 bug: element still marked as active + (dest || document.body).focus(); + + self.ignoreFocus = false; + self.trigger('blur'); + }; + + self.ignoreFocus = true; + if (self.settings.create && self.settings.createOnBlur) { + self.createItem(null, false, deactivate); + } else { + deactivate(); + } + }, + + /** + * Triggered when the user rolls over + * an option in the autocomplete dropdown menu. + * + * @param {object} e + * @returns {boolean} + */ + onOptionHover: function(e) { + if (this.ignoreHover) return; + this.setActiveOption(e.currentTarget, false); + }, + + /** + * Triggered when the user clicks on an option + * in the autocomplete dropdown menu. + * + * @param {object} e + * @returns {boolean} + */ + onOptionSelect: function(e) { + var value, $target, $option, self = this; + + if (e.preventDefault) { + e.preventDefault(); + e.stopPropagation(); + } + + $target = $(e.currentTarget); + if ($target.hasClass('create')) { + self.createItem(null, function() { + if (self.settings.closeAfterSelect) { + self.close(); + } + }); + } else { + value = $target.attr('data-value'); + if (typeof value !== 'undefined') { + self.lastQuery = null; + self.setTextboxValue(''); + self.addItem(value); + if (self.settings.closeAfterSelect) { + self.close(); + } else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) { + self.setActiveOption(self.getOption(value)); + } + } + } + }, + + /** + * Triggered when the user clicks on an item + * that has been selected. + * + * @param {object} e + * @returns {boolean} + */ + onItemSelect: function(e) { + var self = this; + + if (self.isLocked) return; + if (self.settings.mode === 'multi') { + e.preventDefault(); + self.setActiveItem(e.currentTarget, e); + } + }, + + /** + * Invokes the provided method that provides + * results to a callback---which are then added + * as options to the control. + * + * @param {function} fn + */ + load: function(fn) { + var self = this; + var $wrapper = self.$wrapper.addClass(self.settings.loadingClass); + + self.loading++; + fn.apply(self, [function(results) { + self.loading = Math.max(self.loading - 1, 0); + if (results && results.length) { + self.addOption(results); + self.refreshOptions(self.isFocused && !self.isInputHidden); + } + if (!self.loading) { + $wrapper.removeClass(self.settings.loadingClass); + } + self.trigger('load', results); + }]); + }, + + /** + * Sets the input field of the control to the specified value. + * + * @param {string} value + */ + setTextboxValue: function(value) { + var $input = this.$control_input; + var changed = $input.val() !== value; + if (changed) { + $input.val(value).triggerHandler('update'); + this.lastValue = value; + } + }, + + /** + * Returns the value of the control. If multiple items + * can be selected (e.g. or + * element to reflect the current state. + */ + updateOriginalInput: function(opts) { + var i, n, options, label, self = this; + opts = opts || {}; + + if (self.tagType === TAG_SELECT) { + options = []; + for (i = 0, n = self.items.length; i < n; i++) { + label = self.options[self.items[i]][self.settings.labelField] || ''; + options.push(''); + } + if (!options.length && !this.$input.attr('multiple')) { + options.push(''); + } + self.$input.html(options.join('')); + } else { + self.$input.val(self.getValue()); + self.$input.attr('value',self.$input.val()); + } + + if (self.isSetup) { + if (!opts.silent) { + self.trigger('change', self.$input.val()); + } + } + }, + + /** + * Shows/hide the input placeholder depending + * on if there items in the list already. + */ + updatePlaceholder: function() { + if (!this.settings.placeholder) return; + var $input = this.$control_input; + + if (this.items.length) { + $input.removeAttr('placeholder'); + } else { + $input.attr('placeholder', this.settings.placeholder); + } + $input.triggerHandler('update', {force: true}); + }, + + /** + * Shows the autocomplete dropdown containing + * the available options. + */ + open: function() { + var self = this; + + if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return; + self.focus(); + self.isOpen = true; + self.refreshState(); + self.$dropdown.css({visibility: 'hidden', display: 'block'}); + self.positionDropdown(); + self.$dropdown.css({visibility: 'visible'}); + self.trigger('dropdown_open', self.$dropdown); + }, + + /** + * Closes the autocomplete dropdown menu. + */ + close: function() { + var self = this; + var trigger = self.isOpen; + + if (self.settings.mode === 'single' && self.items.length) { + self.hideInput(); + } + + self.isOpen = false; + self.$dropdown.hide(); + self.setActiveOption(null); + self.refreshState(); + + if (trigger) self.trigger('dropdown_close', self.$dropdown); + }, + + /** + * Calculates and applies the appropriate + * position of the dropdown. + */ + positionDropdown: function() { + var $control = this.$control; + var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position(); + offset.top += $control.outerHeight(true); + + this.$dropdown.css({ + width : $control.outerWidth(), + top : offset.top, + left : offset.left + }); + }, + + /** + * Resets / clears all selected items + * from the control. + * + * @param {boolean} silent + */ + clear: function(silent) { + var self = this; + + if (!self.items.length) return; + self.$control.children(':not(input)').remove(); + self.items = []; + self.lastQuery = null; + self.setCaret(0); + self.setActiveItem(null); + self.updatePlaceholder(); + self.updateOriginalInput({silent: silent}); + self.refreshState(); + self.showInput(); + self.trigger('clear'); + }, + + /** + * A helper method for inserting an element + * at the current caret position. + * + * @param {object} $el + */ + insertAtCaret: function($el) { + var caret = Math.min(this.caretPos, this.items.length); + if (caret === 0) { + this.$control.prepend($el); + } else { + $(this.$control[0].childNodes[caret]).before($el); + } + this.setCaret(caret + 1); + }, + + /** + * Removes the current selected item(s). + * + * @param {object} e (optional) + * @returns {boolean} + */ + deleteSelection: function(e) { + var i, n, direction, selection, values, caret, option_select, $option_select, $tail; + var self = this; + + direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1; + selection = getSelection(self.$control_input[0]); + + if (self.$activeOption && !self.settings.hideSelected) { + option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value'); + } + + // determine items that will be removed + values = []; + + if (self.$activeItems.length) { + $tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first')); + caret = self.$control.children(':not(input)').index($tail); + if (direction > 0) { caret++; } + + for (i = 0, n = self.$activeItems.length; i < n; i++) { + values.push($(self.$activeItems[i]).attr('data-value')); + } + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) { + if (direction < 0 && selection.start === 0 && selection.length === 0) { + values.push(self.items[self.caretPos - 1]); + } else if (direction > 0 && selection.start === self.$control_input.val().length) { + values.push(self.items[self.caretPos]); + } + } + + // allow the callback to abort + if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete.apply(self, [values]) === false)) { + return false; + } + + // perform removal + if (typeof caret !== 'undefined') { + self.setCaret(caret); + } + while (values.length) { + self.removeItem(values.pop()); + } + + self.showInput(); + self.positionDropdown(); + self.refreshOptions(true); + + // select previous option + if (option_select) { + $option_select = self.getOption(option_select); + if ($option_select.length) { + self.setActiveOption($option_select); + } + } + + return true; + }, + + /** + * Selects the previous / next item (depending + * on the `direction` argument). + * + * > 0 - right + * < 0 - left + * + * @param {int} direction + * @param {object} e (optional) + */ + advanceSelection: function(direction, e) { + var tail, selection, idx, valueLength, cursorAtEdge, $tail; + var self = this; + + if (direction === 0) return; + if (self.rtl) direction *= -1; + + tail = direction > 0 ? 'last' : 'first'; + selection = getSelection(self.$control_input[0]); + + if (self.isFocused && !self.isInputHidden) { + valueLength = self.$control_input.val().length; + cursorAtEdge = direction < 0 + ? selection.start === 0 && selection.length === 0 + : selection.start === valueLength; + + if (cursorAtEdge && !valueLength) { + self.advanceCaret(direction, e); + } + } else { + $tail = self.$control.children('.active:' + tail); + if ($tail.length) { + idx = self.$control.children(':not(input)').index($tail); + self.setActiveItem(null); + self.setCaret(direction > 0 ? idx + 1 : idx); + } + } + }, + + /** + * Moves the caret left / right. + * + * @param {int} direction + * @param {object} e (optional) + */ + advanceCaret: function(direction, e) { + var self = this, fn, $adj; + + if (direction === 0) return; + + fn = direction > 0 ? 'next' : 'prev'; + if (self.isShiftDown) { + $adj = self.$control_input[fn](); + if ($adj.length) { + self.hideInput(); + self.setActiveItem($adj); + e && e.preventDefault(); + } + } else { + self.setCaret(self.caretPos + direction); + } + }, + + /** + * Moves the caret to the specified index. + * + * @param {int} i + */ + setCaret: function(i) { + var self = this; + + if (self.settings.mode === 'single') { + i = self.items.length; + } else { + i = Math.max(0, Math.min(self.items.length, i)); + } + + if(!self.isPending) { + // the input must be moved by leaving it in place and moving the + // siblings, due to the fact that focus cannot be restored once lost + // on mobile webkit devices + var j, n, fn, $children, $child; + $children = self.$control.children(':not(input)'); + for (j = 0, n = $children.length; j < n; j++) { + $child = $($children[j]).detach(); + if (j < i) { + self.$control_input.before($child); + } else { + self.$control.append($child); + } + } + } + + self.caretPos = i; + }, + + /** + * Disables user input on the control. Used while + * items are being asynchronously created. + */ + lock: function() { + this.close(); + this.isLocked = true; + this.refreshState(); + }, + + /** + * Re-enables user input on the control. + */ + unlock: function() { + this.isLocked = false; + this.refreshState(); + }, + + /** + * Disables user input on the control completely. + * While disabled, it cannot receive focus. + */ + disable: function() { + var self = this; + self.$input.prop('disabled', true); + self.$control_input.prop('disabled', true).prop('tabindex', -1); + self.isDisabled = true; + self.lock(); + }, + + /** + * Enables the control so that it can respond + * to focus and user input. + */ + enable: function() { + var self = this; + self.$input.prop('disabled', false); + self.$control_input.prop('disabled', false).prop('tabindex', self.tabIndex); + self.isDisabled = false; + self.unlock(); + }, + + /** + * Completely destroys the control and + * unbinds all event listeners so that it can + * be garbage collected. + */ + destroy: function() { + var self = this; + var eventNS = self.eventNS; + var revertSettings = self.revertSettings; + + self.trigger('destroy'); + self.off(); + self.$wrapper.remove(); + self.$dropdown.remove(); + + self.$input + .html('') + .append(revertSettings.$children) + .removeAttr('tabindex') + .removeClass('selectized') + .attr({tabindex: revertSettings.tabindex}) + .show(); + + self.$control_input.removeData('grow'); + self.$input.removeData('selectize'); + + $(window).off(eventNS); + $(document).off(eventNS); + $(document.body).off(eventNS); + + delete self.$input[0].selectize; + }, + + /** + * A helper method for rendering "item" and + * "option" templates, given the data. + * + * @param {string} templateName + * @param {object} data + * @returns {string} + */ + render: function(templateName, data) { + var value, id, label; + var html = ''; + var cache = false; + var self = this; + var regex_tag = /^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i; + + if (templateName === 'option' || templateName === 'item') { + value = hash_key(data[self.settings.valueField]); + cache = !!value; + } + + // pull markup from cache if it exists + if (cache) { + if (!isset(self.renderCache[templateName])) { + self.renderCache[templateName] = {}; + } + if (self.renderCache[templateName].hasOwnProperty(value)) { + return self.renderCache[templateName][value]; + } + } + + // render markup + html = self.settings.render[templateName].apply(this, [data, escape_html]); + + // add mandatory attributes + if (templateName === 'option' || templateName === 'option_create') { + html = html.replace(regex_tag, '<$1 data-selectable'); + } + if (templateName === 'optgroup') { + id = data[self.settings.optgroupValueField] || ''; + html = html.replace(regex_tag, '<$1 data-group="' + escape_replace(escape_html(id)) + '"'); + } + if (templateName === 'option' || templateName === 'item') { + html = html.replace(regex_tag, '<$1 data-value="' + escape_replace(escape_html(value || '')) + '"'); + } + + // update cache + if (cache) { + self.renderCache[templateName][value] = html; + } + + return html; + }, + + /** + * Clears the render cache for a template. If + * no template is given, clears all render + * caches. + * + * @param {string} templateName + */ + clearCache: function(templateName) { + var self = this; + if (typeof templateName === 'undefined') { + self.renderCache = {}; + } else { + delete self.renderCache[templateName]; + } + }, + + /** + * Determines whether or not to display the + * create item prompt, given a user input. + * + * @param {string} input + * @return {boolean} + */ + canCreate: function(input) { + var self = this; + if (!self.settings.create) return false; + var filter = self.settings.createFilter; + return input.length + && (typeof filter !== 'function' || filter.apply(self, [input])) + && (typeof filter !== 'string' || new RegExp(filter).test(input)) + && (!(filter instanceof RegExp) || filter.test(input)); + } + + }); + + + Selectize.count = 0; + Selectize.defaults = { + options: [], + optgroups: [], + + plugins: [], + delimiter: ',', + splitOn: null, // regexp or string for splitting up values from a paste command + persist: true, + diacritics: true, + create: false, + createOnBlur: false, + createFilter: null, + highlight: true, + openOnFocus: true, + maxOptions: 1000, + maxItems: null, + hideSelected: null, + addPrecedence: false, + selectOnTab: false, + preload: false, + allowEmptyOption: false, + closeAfterSelect: false, + + scrollDuration: 60, + loadThrottle: 300, + loadingClass: 'loading', + + dataAttr: 'data-data', + optgroupField: 'optgroup', + valueField: 'value', + labelField: 'text', + optgroupLabelField: 'label', + optgroupValueField: 'value', + lockOptgroupOrder: false, + + sortField: '$order', + searchField: ['text'], + searchConjunction: 'and', + + mode: null, + wrapperClass: 'selectize-control', + inputClass: 'selectize-input', + dropdownClass: 'selectize-dropdown', + dropdownContentClass: 'selectize-dropdown-content', + + dropdownParent: null, + + copyClassesToDropdown: true, + + /* + load : null, // function(query, callback) { ... } + score : null, // function(search) { ... } + onInitialize : null, // function() { ... } + onChange : null, // function(value) { ... } + onItemAdd : null, // function(value, $item) { ... } + onItemRemove : null, // function(value) { ... } + onClear : null, // function() { ... } + onOptionAdd : null, // function(value, data) { ... } + onOptionRemove : null, // function(value) { ... } + onOptionClear : null, // function() { ... } + onOptionGroupAdd : null, // function(id, data) { ... } + onOptionGroupRemove : null, // function(id) { ... } + onOptionGroupClear : null, // function() { ... } + onDropdownOpen : null, // function($dropdown) { ... } + onDropdownClose : null, // function($dropdown) { ... } + onType : null, // function(str) { ... } + onDelete : null, // function(values) { ... } + */ + + render: { + /* + item: null, + optgroup: null, + optgroup_header: null, + option: null, + option_create: null + */ + } + }; + + + $.fn.selectize = function(settings_user) { + var defaults = $.fn.selectize.defaults; + var settings = $.extend({}, defaults, settings_user); + var attr_data = settings.dataAttr; + var field_label = settings.labelField; + var field_value = settings.valueField; + var field_optgroup = settings.optgroupField; + var field_optgroup_label = settings.optgroupLabelField; + var field_optgroup_value = settings.optgroupValueField; + + var optionsMap = {}; + + /** + * Initializes selectize from a element. + * + * @param {object} $input + * @param {object} settings_element + */ + var init_textbox = function($input, settings_element) { + var i, n, values, option; + + var data_raw = $input.attr(attr_data); + + if (!data_raw) { + var value = $.trim($input.val() || ''); + if (!settings.allowEmptyOption && !value.length) return; + values = value.split(settings.delimiter); + for (i = 0, n = values.length; i < n; i++) { + option = {}; + option[field_label] = values[i]; + option[field_value] = values[i]; + settings_element.options.push(option); + } + settings_element.items = values; + } else { + settings_element.options = JSON.parse(data_raw); + for (i = 0, n = settings_element.options.length; i < n; i++) { + settings_element.items.push(settings_element.options[i][field_value]); + } + } + }; + + /** + * Initializes selectize from a