completer.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /*!
  2. * Completer v0.1.3
  3. * https://github.com/fengyuanchen/completer
  4. *
  5. * Copyright (c) 2014-2016 Fengyuan Chen
  6. * Released under the MIT license
  7. *
  8. * Date: 2016-06-13T12:43:37.946Z
  9. */
  10. (function (factory) {
  11. if (typeof define === 'function' && define.amd) {
  12. // AMD. Register as anonymous module.
  13. define(['jquery'], factory);
  14. } else if (typeof exports === 'object') {
  15. // Node / CommonJS
  16. factory(require('jquery'));
  17. } else {
  18. // Browser globals.
  19. factory(jQuery);
  20. }
  21. })(function ($) {
  22. 'use strict';
  23. var $window = $(window);
  24. var $document = $(document);
  25. var NAMESPACE = 'completer';
  26. var EVENT_RESIZE = 'resize';
  27. var EVENT_MOUSE_DOWN = 'mousedown';
  28. function Completer(element, options) {
  29. this.$element = $(element);
  30. this.options = $.extend({}, Completer.DEFAULTS, $.isPlainObject(options) && options);
  31. this.init();
  32. }
  33. function espace(s) {
  34. return s.replace(/([\.\$\^\{\[\(\|\)\*\+\?\\])/g, '\\$1');
  35. }
  36. function toRegexp (s) {
  37. if (typeof s === 'string' && s !== '') {
  38. s = espace(s);
  39. return new RegExp(s + '+[^' + s + ']*$', 'i');
  40. }
  41. return null;
  42. }
  43. function toArray(s) {
  44. if (typeof s === 'string') {
  45. s = s.replace(/[\{\}\[\]"']+/g, '').split(/\s*,+\s*/);
  46. }
  47. s = $.map(s, function (n) {
  48. return typeof n !== 'string' ? n.toString() : n;
  49. });
  50. return s;
  51. }
  52. Completer.prototype = {
  53. constructor: Completer,
  54. init: function () {
  55. var options = this.options,
  56. data = toArray(options.source);
  57. if (data.length > 0) {
  58. this.data = data;
  59. this.regexp = toRegexp(options.separator);
  60. this.$completer = $(options.template);
  61. this.$completer.hide().appendTo('body');
  62. this.place();
  63. this.$element.attr('autocomplete', 'off').on({
  64. focus: $.proxy(this.enable, this),
  65. blur: $.proxy(this.disable, this)
  66. });
  67. if (this.$element.is(':focus')) {
  68. this.enable();
  69. }
  70. }
  71. },
  72. enable: function () {
  73. if (!this.active) {
  74. this.active = true;
  75. this.$element.on({
  76. keydown: $.proxy(this.keydown, this),
  77. keyup: $.proxy(this.keyup, this)
  78. });
  79. this.$completer.on({
  80. mousedown: $.proxy(this.mousedown, this),
  81. mouseover: $.proxy(this.mouseover, this)
  82. });
  83. }
  84. },
  85. disable: function () {
  86. if (this.active) {
  87. this.active = false;
  88. this.$element.off({
  89. keydown: this.keydown,
  90. keyup: this.keyup
  91. });
  92. this.$completer.off({
  93. mousedown: this.mousedown,
  94. mouseover: this.mouseover
  95. });
  96. }
  97. },
  98. attach: function (val) {
  99. var options = this.options;
  100. var separator = options.separator;
  101. var regexp = this.regexp;
  102. var part = regexp ? val.match(regexp) : null;
  103. var matched = [];
  104. var all = [];
  105. var that = this;
  106. var reg;
  107. var item;
  108. if (part) {
  109. part = part[0];
  110. val = val.replace(regexp, '');
  111. reg = new RegExp('^' + espace(part), 'i');
  112. }
  113. $.each(this.data, function (i, n) {
  114. n = separator + n;
  115. item = that.template(val + n);
  116. if (reg && reg.test(n)) {
  117. matched.push(item);
  118. } else {
  119. all.push(item);
  120. }
  121. });
  122. matched = matched.length ? matched.sort() : all;
  123. if (options.position === 'top') {
  124. matched = matched.reverse();
  125. }
  126. this.fill(matched.join(''));
  127. },
  128. suggest: function (val) {
  129. var reg = new RegExp(espace(val), 'i');
  130. var that = this;
  131. var matched = [];
  132. $.each(this.data, function (i, n) {
  133. if (reg.test(n)) {
  134. matched.push(n);
  135. }
  136. });
  137. matched.sort(function (a, b) {
  138. return a.indexOf(val) - b.indexOf(val);
  139. });
  140. $.each(matched, function (i, n) {
  141. matched[i] = that.template(n);
  142. });
  143. this.fill(matched.join(''));
  144. },
  145. template: function (text) {
  146. var tag = this.options.itemTag;
  147. return ('<' + tag + '>' + text + '</' + tag + '>');
  148. },
  149. fill: function (html) {
  150. var filter;
  151. this.$completer.empty();
  152. if (html) {
  153. filter = this.options.position === 'top' ? ':last' : ':first';
  154. this.$completer.html(html);
  155. this.$completer.children(filter).addClass(this.options.selectedClass);
  156. this.show();
  157. } else {
  158. this.hide();
  159. }
  160. },
  161. complete: function () {
  162. var options = this.options;
  163. var val = options.filter(this.$element.val()).toString();
  164. if (val === '') {
  165. this.hide();
  166. return;
  167. }
  168. if (options.suggest) {
  169. this.suggest(val);
  170. } else {
  171. this.attach(val);
  172. }
  173. },
  174. keydown: function (e) {
  175. var keyCode = e.keyCode || e.which || e.charCode;
  176. if (keyCode === 13) {
  177. e.stopPropagation();
  178. e.preventDefault();
  179. }
  180. },
  181. keyup: function (e) {
  182. var keyCode = e.keyCode || e.which || e.charCode;
  183. if (keyCode === 13 || keyCode === 38 || keyCode === 40) {
  184. this.toggle(keyCode);
  185. } else {
  186. this.complete();
  187. }
  188. },
  189. mouseover: function (e) {
  190. var options = this.options;
  191. var selectedClass = options.selectedClass,
  192. $target = $(e.target);
  193. if ($target.is(options.itemTag)) {
  194. $target.addClass(selectedClass).siblings().removeClass(selectedClass);
  195. }
  196. },
  197. mousedown: function (e) {
  198. e.stopPropagation();
  199. e.preventDefault();
  200. this.setValue($(e.target).text());
  201. },
  202. setValue: function (val) {
  203. this.$element.val(val);
  204. this.options.complete();
  205. this.hide();
  206. },
  207. toggle: function (keyCode) {
  208. var selectedClass = this.options.selectedClass;
  209. var $selected = this.$completer.find('.' + selectedClass);
  210. switch (keyCode) {
  211. // Down
  212. case 40:
  213. $selected.removeClass(selectedClass);
  214. $selected = $selected.next();
  215. break;
  216. // Up
  217. case 38:
  218. $selected.removeClass(selectedClass);
  219. $selected = $selected.prev();
  220. break;
  221. // Enter
  222. case 13:
  223. this.setValue($selected.text());
  224. break;
  225. // No default
  226. }
  227. if ($selected.length === 0) {
  228. $selected = this.$completer.children(keyCode === 40 ? ':first' : ':last');
  229. }
  230. $selected.addClass(selectedClass);
  231. },
  232. place: function () {
  233. var $element = this.$element;
  234. var offset = $element.offset();
  235. var left = offset.left;
  236. var top = offset.top;
  237. var height = $element.outerHeight();
  238. var width = $element.outerWidth();
  239. var styles = {
  240. minWidth: width,
  241. zIndex: this.options.zIndex
  242. };
  243. switch (this.options.position) {
  244. case 'right':
  245. styles.left = left + width;
  246. styles.top = top;
  247. break;
  248. case 'left':
  249. styles.right = $window.innerWidth() - left;
  250. styles.top = top;
  251. break;
  252. case 'top':
  253. styles.left = left;
  254. styles.bottom = $window.innerHeight() - top;
  255. break;
  256. // case 'bottom':
  257. default:
  258. styles.left = left;
  259. styles.top = top + height;
  260. }
  261. this.$completer.css(styles);
  262. },
  263. show: function () {
  264. this.$completer.show();
  265. $window.on(EVENT_RESIZE, $.proxy(this.place, this));
  266. $document.on(EVENT_MOUSE_DOWN, $.proxy(this.hide, this));
  267. },
  268. hide: function () {
  269. this.$completer.hide();
  270. $window.off(EVENT_RESIZE, this.place);
  271. $document.off(EVENT_MOUSE_DOWN, this.hide);
  272. },
  273. destroy: function () {
  274. var $this = this.$element;
  275. this.hide();
  276. this.disable();
  277. $this.off({
  278. focus: this.enable,
  279. blur: this.disable
  280. });
  281. $this.removeData(NAMESPACE);
  282. }
  283. };
  284. Completer.DEFAULTS = {
  285. itemTag: 'li',
  286. position: 'bottom', // or 'right'
  287. source: [],
  288. selectedClass: 'completer-selected',
  289. separator: '',
  290. suggest: false,
  291. template: '<ul class="completer-container"></ul>',
  292. zIndex: 1,
  293. complete: $.noop,
  294. filter: function (val) {
  295. return val;
  296. }
  297. };
  298. Completer.setDefaults = function (options) {
  299. $.extend(Completer.DEFAULTS, options);
  300. };
  301. // Save the other completer
  302. Completer.other = $.fn.completer;
  303. // Register as jQuery plugin
  304. $.fn.completer = function (option) {
  305. var args = [].slice.call(arguments, 1);
  306. var result;
  307. this.each(function () {
  308. var $this = $(this);
  309. var data = $this.data(NAMESPACE);
  310. var options;
  311. var fn;
  312. if (!data) {
  313. if (/destroy/.test(option)) {
  314. return;
  315. }
  316. options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
  317. $this.data(NAMESPACE, (data = new Completer(this, options)));
  318. }
  319. if (typeof option === 'string' && $.isFunction(fn = data[option])) {
  320. result = fn.apply(data, args);
  321. }
  322. });
  323. return typeof result !== 'undefined' ? result : this;
  324. };
  325. $.fn.completer.Constructor = Completer;
  326. $.fn.completer.setDefaults = Completer.setDefaults;
  327. // No conflict
  328. $.fn.completer.noConflict = function () {
  329. $.fn.completer = Completer.other;
  330. return this;
  331. };
  332. $(function () {
  333. $('[data-toggle="completer"]').completer();
  334. });
  335. });