results.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. define([
  2. 'jquery',
  3. './utils'
  4. ], function ($, Utils) {
  5. function Results ($element, options, dataAdapter) {
  6. this.$element = $element;
  7. this.data = dataAdapter;
  8. this.options = options;
  9. Results.__super__.constructor.call(this);
  10. }
  11. Utils.Extend(Results, Utils.Observable);
  12. Results.prototype.render = function () {
  13. var $results = $(
  14. '<ul class="select2-results__options" role="listbox"></ul>'
  15. );
  16. if (this.options.get('multiple')) {
  17. $results.attr('aria-multiselectable', 'true');
  18. }
  19. this.$results = $results;
  20. return $results;
  21. };
  22. Results.prototype.clear = function () {
  23. this.$results.empty();
  24. };
  25. Results.prototype.displayMessage = function (params) {
  26. var escapeMarkup = this.options.get('escapeMarkup');
  27. this.clear();
  28. this.hideLoading();
  29. var $message = $(
  30. '<li role="alert" aria-live="assertive"' +
  31. ' class="select2-results__option"></li>'
  32. );
  33. var message = this.options.get('translations').get(params.message);
  34. $message.append(
  35. escapeMarkup(
  36. message(params.args)
  37. )
  38. );
  39. $message[0].className += ' select2-results__message';
  40. this.$results.append($message);
  41. };
  42. Results.prototype.hideMessages = function () {
  43. this.$results.find('.select2-results__message').remove();
  44. };
  45. Results.prototype.append = function (data) {
  46. this.hideLoading();
  47. var $options = [];
  48. if (data.results == null || data.results.length === 0) {
  49. if (this.$results.children().length === 0) {
  50. this.trigger('results:message', {
  51. message: 'noResults'
  52. });
  53. }
  54. return;
  55. }
  56. data.results = this.sort(data.results);
  57. for (var d = 0; d < data.results.length; d++) {
  58. var item = data.results[d];
  59. var $option = this.option(item);
  60. $options.push($option);
  61. }
  62. this.$results.append($options);
  63. };
  64. Results.prototype.position = function ($results, $dropdown) {
  65. var $resultsContainer = $dropdown.find('.select2-results');
  66. $resultsContainer.append($results);
  67. };
  68. Results.prototype.sort = function (data) {
  69. var sorter = this.options.get('sorter');
  70. return sorter(data);
  71. };
  72. Results.prototype.highlightFirstItem = function () {
  73. var $options = this.$results
  74. .find('.select2-results__option--selectable');
  75. var $selected = $options.filter('.select2-results__option--selected');
  76. // Check if there are any selected options
  77. if ($selected.length > 0) {
  78. // If there are selected options, highlight the first
  79. $selected.first().trigger('mouseenter');
  80. } else {
  81. // If there are no selected options, highlight the first option
  82. // in the dropdown
  83. $options.first().trigger('mouseenter');
  84. }
  85. this.ensureHighlightVisible();
  86. };
  87. Results.prototype.setClasses = function () {
  88. var self = this;
  89. this.data.current(function (selected) {
  90. var selectedIds = selected.map(function (s) {
  91. return s.id.toString();
  92. });
  93. var $options = self.$results
  94. .find('.select2-results__option--selectable');
  95. $options.each(function () {
  96. var $option = $(this);
  97. var item = Utils.GetData(this, 'data');
  98. // id needs to be converted to a string when comparing
  99. var id = '' + item.id;
  100. if ((item.element != null && item.element.selected) ||
  101. (item.element == null && selectedIds.indexOf(id) > -1)) {
  102. this.classList.add('select2-results__option--selected');
  103. $option.attr('aria-selected', 'true');
  104. } else {
  105. this.classList.remove('select2-results__option--selected');
  106. $option.attr('aria-selected', 'false');
  107. }
  108. });
  109. });
  110. };
  111. Results.prototype.showLoading = function (params) {
  112. this.hideLoading();
  113. var loadingMore = this.options.get('translations').get('searching');
  114. var loading = {
  115. disabled: true,
  116. loading: true,
  117. text: loadingMore(params)
  118. };
  119. var $loading = this.option(loading);
  120. $loading.className += ' loading-results';
  121. this.$results.prepend($loading);
  122. };
  123. Results.prototype.hideLoading = function () {
  124. this.$results.find('.loading-results').remove();
  125. };
  126. Results.prototype.option = function (data) {
  127. var option = document.createElement('li');
  128. option.classList.add('select2-results__option');
  129. option.classList.add('select2-results__option--selectable');
  130. var attrs = {
  131. 'role': 'option'
  132. };
  133. var matches = window.Element.prototype.matches ||
  134. window.Element.prototype.msMatchesSelector ||
  135. window.Element.prototype.webkitMatchesSelector;
  136. if ((data.element != null && matches.call(data.element, ':disabled')) ||
  137. (data.element == null && data.disabled)) {
  138. attrs['aria-disabled'] = 'true';
  139. option.classList.remove('select2-results__option--selectable');
  140. option.classList.add('select2-results__option--disabled');
  141. }
  142. if (data.id == null) {
  143. option.classList.remove('select2-results__option--selectable');
  144. }
  145. if (data._resultId != null) {
  146. option.id = data._resultId;
  147. }
  148. if (data.title) {
  149. option.title = data.title;
  150. }
  151. if (data.children) {
  152. attrs.role = 'group';
  153. attrs['aria-label'] = data.text;
  154. option.classList.remove('select2-results__option--selectable');
  155. option.classList.add('select2-results__option--group');
  156. }
  157. for (var attr in attrs) {
  158. var val = attrs[attr];
  159. option.setAttribute(attr, val);
  160. }
  161. if (data.children) {
  162. var $option = $(option);
  163. var label = document.createElement('strong');
  164. label.className = 'select2-results__group';
  165. this.template(data, label);
  166. var $children = [];
  167. for (var c = 0; c < data.children.length; c++) {
  168. var child = data.children[c];
  169. var $child = this.option(child);
  170. $children.push($child);
  171. }
  172. var $childrenContainer = $('<ul></ul>', {
  173. 'class': 'select2-results__options select2-results__options--nested',
  174. 'role': 'none'
  175. });
  176. $childrenContainer.append($children);
  177. $option.append(label);
  178. $option.append($childrenContainer);
  179. } else {
  180. this.template(data, option);
  181. }
  182. Utils.StoreData(option, 'data', data);
  183. return option;
  184. };
  185. Results.prototype.bind = function (container, $container) {
  186. var self = this;
  187. var id = container.id + '-results';
  188. this.$results.attr('id', id);
  189. container.on('results:all', function (params) {
  190. self.clear();
  191. self.append(params.data);
  192. if (container.isOpen()) {
  193. self.setClasses();
  194. self.highlightFirstItem();
  195. }
  196. });
  197. container.on('results:append', function (params) {
  198. self.append(params.data);
  199. if (container.isOpen()) {
  200. self.setClasses();
  201. }
  202. });
  203. container.on('query', function (params) {
  204. self.hideMessages();
  205. self.showLoading(params);
  206. });
  207. container.on('select', function () {
  208. if (!container.isOpen()) {
  209. return;
  210. }
  211. self.setClasses();
  212. if (self.options.get('scrollAfterSelect')) {
  213. self.highlightFirstItem();
  214. }
  215. });
  216. container.on('unselect', function () {
  217. if (!container.isOpen()) {
  218. return;
  219. }
  220. self.setClasses();
  221. if (self.options.get('scrollAfterSelect')) {
  222. self.highlightFirstItem();
  223. }
  224. });
  225. container.on('open', function () {
  226. // When the dropdown is open, aria-expended="true"
  227. self.$results.attr('aria-expanded', 'true');
  228. self.$results.attr('aria-hidden', 'false');
  229. self.setClasses();
  230. self.ensureHighlightVisible();
  231. });
  232. container.on('close', function () {
  233. // When the dropdown is closed, aria-expended="false"
  234. self.$results.attr('aria-expanded', 'false');
  235. self.$results.attr('aria-hidden', 'true');
  236. self.$results.removeAttr('aria-activedescendant');
  237. });
  238. container.on('results:toggle', function () {
  239. var $highlighted = self.getHighlightedResults();
  240. if ($highlighted.length === 0) {
  241. return;
  242. }
  243. $highlighted.trigger('mouseup');
  244. });
  245. container.on('results:select', function () {
  246. var $highlighted = self.getHighlightedResults();
  247. if ($highlighted.length === 0) {
  248. return;
  249. }
  250. var data = Utils.GetData($highlighted[0], 'data');
  251. if ($highlighted.hasClass('select2-results__option--selected')) {
  252. self.trigger('close', {});
  253. } else {
  254. self.trigger('select', {
  255. data: data
  256. });
  257. }
  258. });
  259. container.on('results:previous', function () {
  260. var $highlighted = self.getHighlightedResults();
  261. var $options = self.$results.find('.select2-results__option--selectable');
  262. var currentIndex = $options.index($highlighted);
  263. // If we are already at the top, don't move further
  264. // If no options, currentIndex will be -1
  265. if (currentIndex <= 0) {
  266. return;
  267. }
  268. var nextIndex = currentIndex - 1;
  269. // If none are highlighted, highlight the first
  270. if ($highlighted.length === 0) {
  271. nextIndex = 0;
  272. }
  273. var $next = $options.eq(nextIndex);
  274. $next.trigger('mouseenter');
  275. var currentOffset = self.$results.offset().top;
  276. var nextTop = $next.offset().top;
  277. var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset);
  278. if (nextIndex === 0) {
  279. self.$results.scrollTop(0);
  280. } else if (nextTop - currentOffset < 0) {
  281. self.$results.scrollTop(nextOffset);
  282. }
  283. });
  284. container.on('results:next', function () {
  285. var $highlighted = self.getHighlightedResults();
  286. var $options = self.$results.find('.select2-results__option--selectable');
  287. var currentIndex = $options.index($highlighted);
  288. var nextIndex = currentIndex + 1;
  289. // If we are at the last option, stay there
  290. if (nextIndex >= $options.length) {
  291. return;
  292. }
  293. var $next = $options.eq(nextIndex);
  294. $next.trigger('mouseenter');
  295. var currentOffset = self.$results.offset().top +
  296. self.$results.outerHeight(false);
  297. var nextBottom = $next.offset().top + $next.outerHeight(false);
  298. var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset;
  299. if (nextIndex === 0) {
  300. self.$results.scrollTop(0);
  301. } else if (nextBottom > currentOffset) {
  302. self.$results.scrollTop(nextOffset);
  303. }
  304. });
  305. container.on('results:focus', function (params) {
  306. params.element[0].classList.add('select2-results__option--highlighted');
  307. params.element[0].setAttribute('aria-selected', 'true');
  308. });
  309. container.on('results:message', function (params) {
  310. self.displayMessage(params);
  311. });
  312. if ($.fn.mousewheel) {
  313. this.$results.on('mousewheel', function (e) {
  314. var top = self.$results.scrollTop();
  315. var bottom = self.$results.get(0).scrollHeight - top + e.deltaY;
  316. var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0;
  317. var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height();
  318. if (isAtTop) {
  319. self.$results.scrollTop(0);
  320. e.preventDefault();
  321. e.stopPropagation();
  322. } else if (isAtBottom) {
  323. self.$results.scrollTop(
  324. self.$results.get(0).scrollHeight - self.$results.height()
  325. );
  326. e.preventDefault();
  327. e.stopPropagation();
  328. }
  329. });
  330. }
  331. this.$results.on('mouseup', '.select2-results__option--selectable',
  332. function (evt) {
  333. var $this = $(this);
  334. var data = Utils.GetData(this, 'data');
  335. if ($this.hasClass('select2-results__option--selected')) {
  336. if (self.options.get('multiple')) {
  337. self.trigger('unselect', {
  338. originalEvent: evt,
  339. data: data
  340. });
  341. } else {
  342. self.trigger('close', {
  343. originalEvent: evt,
  344. data: data
  345. });
  346. }
  347. return;
  348. }
  349. self.trigger('select', {
  350. originalEvent: evt,
  351. data: data
  352. });
  353. });
  354. this.$results.on('mouseenter', '.select2-results__option--selectable',
  355. function (evt) {
  356. var data = Utils.GetData(this, 'data');
  357. self.getHighlightedResults()
  358. .removeClass('select2-results__option--highlighted')
  359. .attr('aria-selected', 'false');
  360. self.trigger('results:focus', {
  361. data: data,
  362. element: $(this)
  363. });
  364. });
  365. };
  366. Results.prototype.getHighlightedResults = function () {
  367. var $highlighted = this.$results
  368. .find('.select2-results__option--highlighted');
  369. return $highlighted;
  370. };
  371. Results.prototype.destroy = function () {
  372. this.$results.remove();
  373. };
  374. Results.prototype.ensureHighlightVisible = function () {
  375. var $highlighted = this.getHighlightedResults();
  376. if ($highlighted.length === 0) {
  377. return;
  378. }
  379. var $options = this.$results.find('.select2-results__option--selectable');
  380. var currentIndex = $options.index($highlighted);
  381. var currentOffset = this.$results.offset().top;
  382. var nextTop = $highlighted.offset().top;
  383. var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset);
  384. var offsetDelta = nextTop - currentOffset;
  385. nextOffset -= $highlighted.outerHeight(false) * 2;
  386. if (currentIndex <= 2) {
  387. this.$results.scrollTop(0);
  388. } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) {
  389. this.$results.scrollTop(nextOffset);
  390. }
  391. };
  392. Results.prototype.template = function (result, container) {
  393. var template = this.options.get('templateResult');
  394. var escapeMarkup = this.options.get('escapeMarkup');
  395. var content = template(result, container);
  396. if (content == null) {
  397. container.style.display = 'none';
  398. } else if (typeof content === 'string') {
  399. container.innerHTML = escapeMarkup(content);
  400. } else {
  401. $(container).append(content);
  402. }
  403. };
  404. return Results;
  405. });