nya-bs-select.js 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560
  1. /**
  2. * nya-bootstrap-select v2.0.10
  3. * Copyright 2014 Nyasoft
  4. * Licensed under MIT license
  5. */
  6. (function(){
  7. 'use strict';
  8. var uid = 0;
  9. function nextUid() {
  10. return ++uid;
  11. }
  12. /**
  13. * Checks if `obj` is a window object.
  14. *
  15. * @private
  16. * @param {*} obj Object to check
  17. * @returns {boolean} True if `obj` is a window obj.
  18. */
  19. function isWindow(obj) {
  20. return obj && obj.window === obj;
  21. }
  22. /**
  23. * @ngdoc function
  24. * @name angular.isString
  25. * @module ng
  26. * @kind function
  27. *
  28. * @description
  29. * Determines if a reference is a `String`.
  30. *
  31. * @param {*} value Reference to check.
  32. * @returns {boolean} True if `value` is a `String`.
  33. */
  34. function isString(value){return typeof value === 'string';}
  35. /**
  36. * @param {*} obj
  37. * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments,
  38. * String ...)
  39. */
  40. function isArrayLike(obj) {
  41. if (obj == null || isWindow(obj)) {
  42. return false;
  43. }
  44. var length = obj.length;
  45. if (obj.nodeType === 1 && length) {
  46. return true;
  47. }
  48. return isString(obj) || Array.isArray(obj) || length === 0 ||
  49. typeof length === 'number' && length > 0 && (length - 1) in obj;
  50. }
  51. /**
  52. * Creates a new object without a prototype. This object is useful for lookup without having to
  53. * guard against prototypically inherited properties via hasOwnProperty.
  54. *
  55. * Related micro-benchmarks:
  56. * - http://jsperf.com/object-create2
  57. * - http://jsperf.com/proto-map-lookup/2
  58. * - http://jsperf.com/for-in-vs-object-keys2
  59. *
  60. * @returns {Object}
  61. */
  62. function createMap() {
  63. return Object.create(null);
  64. }
  65. /**
  66. * Computes a hash of an 'obj'.
  67. * Hash of a:
  68. * string is string
  69. * number is number as string
  70. * object is either result of calling $$hashKey function on the object or uniquely generated id,
  71. * that is also assigned to the $$hashKey property of the object.
  72. *
  73. * @param obj
  74. * @returns {string} hash string such that the same input will have the same hash string.
  75. * The resulting string key is in 'type:hashKey' format.
  76. */
  77. function hashKey(obj, nextUidFn) {
  78. var objType = typeof obj,
  79. key;
  80. if (objType == 'function' || (objType == 'object' && obj !== null)) {
  81. if (typeof (key = obj.$$hashKey) == 'function') {
  82. // must invoke on object to keep the right this
  83. key = obj.$$hashKey();
  84. } else if (key === undefined) {
  85. key = obj.$$hashKey = (nextUidFn || nextUid)();
  86. }
  87. } else {
  88. key = obj;
  89. }
  90. return objType + ':' + key;
  91. }
  92. //TODO: use with caution. if an property of element in array doesn't exist in group, the resultArray may lose some element.
  93. function sortByGroup(array ,group, property) {
  94. var unknownGroup = [],
  95. i, j,
  96. resultArray = [];
  97. for(i = 0; i < group.length; i++) {
  98. for(j = 0; j < array.length;j ++) {
  99. if(!array[j][property]) {
  100. unknownGroup.push(array[j]);
  101. } else if(array[j][property] === group[i]) {
  102. resultArray.push(array[j]);
  103. }
  104. }
  105. }
  106. resultArray = resultArray.concat(unknownGroup);
  107. return resultArray;
  108. }
  109. /**
  110. * Return the DOM siblings between the first and last node in the given array.
  111. * @param {Array} array like object
  112. * @returns {jqLite} jqLite collection containing the nodes
  113. */
  114. function getBlockNodes(nodes) {
  115. // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
  116. // collection, otherwise update the original collection.
  117. var node = nodes[0];
  118. var endNode = nodes[nodes.length - 1];
  119. var blockNodes = [node];
  120. do {
  121. node = node.nextSibling;
  122. if (!node) break;
  123. blockNodes.push(node);
  124. } while (node !== endNode);
  125. return angular.element(blockNodes);
  126. }
  127. var getBlockStart = function(block) {
  128. return block.clone[0];
  129. };
  130. var getBlockEnd = function(block) {
  131. return block.clone[block.clone.length - 1];
  132. };
  133. var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength, group) {
  134. // TODO(perf): generate setters to shave off ~40ms or 1-1.5%
  135. scope[valueIdentifier] = value;
  136. if (keyIdentifier) scope[keyIdentifier] = key;
  137. scope.$index = index;
  138. scope.$first = (index === 0);
  139. scope.$last = (index === (arrayLength - 1));
  140. scope.$middle = !(scope.$first || scope.$last);
  141. // jshint bitwise: false
  142. scope.$odd = !(scope.$even = (index&1) === 0);
  143. // jshint bitwise: true
  144. if(group) {
  145. scope.$group = group;
  146. }
  147. };
  148. var contains = function(array, element) {
  149. var length = array.length,
  150. i;
  151. if(length === 0) {
  152. return false;
  153. }
  154. for(i = 0;i < length; i++) {
  155. if(deepEquals(element, array[i])) {
  156. return true;
  157. }
  158. }
  159. return false;
  160. };
  161. var indexOf = function(array, element) {
  162. var length = array.length,
  163. i;
  164. if(length === 0) {
  165. return -1;
  166. }
  167. for(i = 0; i < length; i++) {
  168. if(deepEquals(element, array[i])) {
  169. return i;
  170. }
  171. }
  172. return -1;
  173. };
  174. /**
  175. * filter the event target for the nya-bs-option element.
  176. * Use this method with event delegate. (attach a event handler on an parent element and listen the special children elements)
  177. * @param target event.target node
  178. * @param parent {object} the parent, where the event handler attached.
  179. * @param selector {string}|{object} a class or DOM element
  180. * @return the filtered target or null if no element satisfied the selector.
  181. */
  182. var filterTarget = function(target, parent, selector) {
  183. var elem = target,
  184. className, type = typeof selector;
  185. if(target == parent) {
  186. return null;
  187. } else {
  188. do {
  189. if(type === 'string') {
  190. className = ' ' + elem.className + ' ';
  191. if(elem.nodeType === 1 && className.replace(/[\t\r\n\f]/g, ' ').indexOf(selector) >= 0) {
  192. return elem;
  193. }
  194. } else {
  195. if(elem == selector) {
  196. return elem;
  197. }
  198. }
  199. } while((elem = elem.parentNode) && elem != parent && elem.nodeType !== 9);
  200. return null;
  201. }
  202. };
  203. var getClassList = function(element) {
  204. var classList,
  205. className = element.className.replace(/[\t\r\n\f]/g, ' ').trim();
  206. classList = className.split(' ');
  207. for(var i = 0; i < classList.length; i++) {
  208. if(/\s+/.test(classList[i])) {
  209. classList.splice(i, 1);
  210. i--;
  211. }
  212. }
  213. return classList;
  214. };
  215. // work with node element
  216. var hasClass = function(element, className) {
  217. var classList = getClassList(element);
  218. return classList.indexOf(className) !== -1;
  219. };
  220. // query children by class(one or more)
  221. var queryChildren = function(element, classList) {
  222. var children = element.children(),
  223. length = children.length,
  224. child,
  225. valid,
  226. classes;
  227. if(length > 0) {
  228. for(var i = 0; i < length; i++) {
  229. child = children.eq(i);
  230. valid = true;
  231. classes = getClassList(child[0]);
  232. if(classes.length > 0) {
  233. for(var j = 0; j < classList.length; j++) {
  234. if(classes.indexOf(classList[j]) === -1) {
  235. valid = false;
  236. break;
  237. }
  238. }
  239. }
  240. if(valid) {
  241. return child;
  242. }
  243. }
  244. }
  245. return [];
  246. };
  247. /**
  248. * Current support only drill down one level.
  249. * case insensitive
  250. * @param element
  251. * @param keyword
  252. */
  253. var hasKeyword = function(element, keyword) {
  254. var childElements,
  255. index, length;
  256. if(element.text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
  257. return true;
  258. } else {
  259. childElements = element.children();
  260. length = childElements.length;
  261. for(index = 0; index < length; index++) {
  262. if(childElements.eq(index).text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
  263. return true;
  264. }
  265. }
  266. return false;
  267. }
  268. };
  269. function sibling( cur, dir ) {
  270. while ( (cur = cur[dir]) && cur.nodeType !== 1) {}
  271. return cur;
  272. }
  273. // map global property to local variable.
  274. var jqLite = angular.element;
  275. var deepEquals = angular.equals;
  276. var deepCopy = angular.copy;
  277. var extend = angular.extend;
  278. var nyaBsSelect = angular.module('nya.bootstrap.select', []);
  279. /**
  280. * A service for configuration. the configuration is shared globally.
  281. */
  282. nyaBsSelect.provider('nyaBsConfig', function() {
  283. var locale = null;
  284. // default localized text. cannot be modified.
  285. var defaultText = {
  286. 'en-us': {
  287. defaultNoneSelection: 'Nothing selected',
  288. noSearchResult: 'NO SEARCH RESULT',
  289. numberItemSelected: '%d item selected'
  290. }
  291. };
  292. // localized text which actually being used.
  293. var interfaceText = deepCopy(defaultText);
  294. /**
  295. * Merge with default localized text.
  296. * @param localeId a string formatted as languageId-countryId
  297. * @param obj localized text object.
  298. */
  299. this.setLocalizedText = function(localeId, obj) {
  300. if(!localeId) {
  301. throw new Error('localeId must be a string formatted as languageId-countryId');
  302. }
  303. if(!interfaceText[localeId]) {
  304. interfaceText[localeId] = {};
  305. }
  306. interfaceText[localeId] = extend(interfaceText[localeId], obj);
  307. };
  308. /**
  309. * Force to use a special locale id. if localeId is null. reset to user-agent locale.
  310. * @param localeId a string formatted as languageId-countryId
  311. */
  312. this.useLocale = function(localeId) {
  313. locale = localeId;
  314. };
  315. /**
  316. * get the localized text according current locale or forced locale
  317. * @returns localizedText
  318. */
  319. this.$get = ['$locale', function($locale){
  320. var localizedText;
  321. if(locale) {
  322. localizedText = interfaceText[locale];
  323. } else {
  324. localizedText = interfaceText[$locale.id];
  325. }
  326. if(!localizedText) {
  327. localizedText = defaultText['en-us'];
  328. }
  329. return localizedText;
  330. }];
  331. });
  332. nyaBsSelect.controller('nyaBsSelectCtrl', function(){
  333. var self = this;
  334. // keyIdentifier and valueIdentifier are set by nyaBsOption directive
  335. // used by nyaBsSelect directive to retrieve key and value from each nyaBsOption's child scope.
  336. self.keyIdentifier = null;
  337. self.valueIdentifier = null;
  338. self.isMultiple = false;
  339. // Should be override by nyaBsSelect directive and called by nyaBsOption directive when collection is changed.
  340. self.onCollectionChange = function(){};
  341. // for debug
  342. self.setId = function(id) {
  343. self.id = id || 'id#' + Math.floor(Math.random() * 10000);
  344. };
  345. });
  346. nyaBsSelect.directive('nyaBsSelect', ['$parse', '$document', '$timeout', 'nyaBsConfig', function ($parse, $document, $timeout, nyaBsConfig) {
  347. var DEFAULT_NONE_SELECTION = 'Nothing selected';
  348. var DROPDOWN_TOGGLE = '<button class="btn btn-default dropdown-toggle" type="button">' +
  349. '<span class="pull-left filter-option"></span>' +
  350. '&nbsp;' +
  351. '<span class="caret"></span>' +
  352. '</button>';
  353. var DROPDOWN_CONTAINER = '<div class="dropdown-menu open"></div>';
  354. var SEARCH_BOX = '<div class="bs-searchbox">' +
  355. '<input type="text" class="form-control">' +
  356. '</div>';
  357. var DROPDOWN_MENU = '<ul class="dropdown-menu inner"></ul>';
  358. var NO_SEARCH_RESULT = '<li class="no-search-result"><span>NO SEARCH RESULT</span></li>';
  359. return {
  360. restrict: 'ECA',
  361. require: ['ngModel', 'nyaBsSelect'],
  362. controller: 'nyaBsSelectCtrl',
  363. compile: function nyaBsSelectCompile (tElement, tAttrs){
  364. tElement.addClass('btn-group');
  365. var getDefaultNoneSelectionContent = function() {
  366. // text node or jqLite element.
  367. var content;
  368. if(tAttrs.titleTpl) {
  369. // use title-tpl attribute value.
  370. content = jqLite(tAttrs.titleTpl);
  371. } else if(tAttrs.title) {
  372. // use title attribute value.
  373. content = document.createTextNode(tAttrs.title);
  374. } else if(localizedText.defaultNoneSelectionTpl){
  375. // use localized text template.
  376. content = jqLite(localizedText.defaultNoneSelectionTpl);
  377. } else if(localizedText.defaultNoneSelection) {
  378. // use localized text.
  379. content = document.createTextNode(localizedText.defaultNoneSelection);
  380. } else {
  381. // use default.
  382. content = document.createTextNode(DEFAULT_NONE_SELECTION);
  383. }
  384. return content;
  385. };
  386. var options = tElement.children(),
  387. dropdownToggle = jqLite(DROPDOWN_TOGGLE),
  388. dropdownContainer = jqLite(DROPDOWN_CONTAINER),
  389. dropdownMenu = jqLite(DROPDOWN_MENU),
  390. searchBox,
  391. noSearchResult,
  392. classList,
  393. length,
  394. index,
  395. liElement,
  396. localizedText = nyaBsConfig;
  397. classList = getClassList(tElement[0]);
  398. classList.forEach(function(className) {
  399. if(/btn-(?:primary|info|success|warning|danger|inverse)/.test(className)) {
  400. tElement.removeClass(className);
  401. dropdownToggle.removeClass('btn-default');
  402. dropdownToggle.addClass(className);
  403. }
  404. //if(/btn-(?:lg|sm|xs)/.test(className)) {
  405. // tElement.removeClass(className);
  406. // dropdownToggle.addClass(className);
  407. //}
  408. if(className === 'form-control') {
  409. dropdownToggle.addClass(className);
  410. }
  411. });
  412. dropdownMenu.append(options);
  413. // add tabindex to children anchor elements if not present.
  414. // tabindex attribute will give an anchor element ability to be get focused.
  415. length = options.length;
  416. for(index = 0; index < length; index++) {
  417. liElement = options.eq(index);
  418. if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
  419. liElement.find('a').attr('tabindex', '0');
  420. }
  421. }
  422. if(tAttrs.liveSearch === 'true') {
  423. searchBox = jqLite(SEARCH_BOX);
  424. // set localized text
  425. if(localizedText.noSearchResultTpl) {
  426. NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResultTpl);
  427. } else if(localizedText.noSearchResult) {
  428. NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResult);
  429. }
  430. noSearchResult = jqLite(NO_SEARCH_RESULT);
  431. dropdownContainer.append(searchBox);
  432. dropdownMenu.append(noSearchResult);
  433. }
  434. // set default none selection text
  435. dropdownToggle.children().eq(0).append(getDefaultNoneSelectionContent());
  436. dropdownContainer.append(dropdownMenu);
  437. tElement.append(dropdownToggle);
  438. tElement.append(dropdownContainer);
  439. return function nyaBsSelectLink ($scope, $element, $attrs, ctrls) {
  440. var ngCtrl = ctrls[0],
  441. nyaBsSelectCtrl = ctrls[1],
  442. liHeight,
  443. isDisabled = false,
  444. previousTabIndex,
  445. valueExpFn,
  446. valueExpGetter = $parse(nyaBsSelectCtrl.valueExp),
  447. isMultiple = typeof $attrs.multiple !== 'undefined';
  448. // find element from current $element root. because the compiled element may be detached from DOM tree by ng-if or ng-switch.
  449. var dropdownToggle = queryChildren($element, ['dropdown-toggle']),
  450. dropdownContainer = dropdownToggle.next(),
  451. dropdownMenu = queryChildren(dropdownContainer, ['dropdown-menu', 'inner']),
  452. searchBox = queryChildren(dropdownContainer, ['bs-searchbox']),
  453. noSearchResult = queryChildren(dropdownMenu, ['no-search-result']);
  454. if(nyaBsSelectCtrl.valueExp) {
  455. valueExpFn = function(scope, locals) {
  456. return valueExpGetter(scope, locals);
  457. };
  458. }
  459. // for debug
  460. nyaBsSelectCtrl.setId($element.attr('id'));
  461. if (isMultiple) {
  462. nyaBsSelectCtrl.isMultiple = true;
  463. // required validator
  464. ngCtrl.$isEmpty = function(value) {
  465. return !value || value.length === 0;
  466. };
  467. }
  468. if(typeof $attrs.disabled !== 'undefined') {
  469. $scope.$watch($attrs.disabled, function(disabled){
  470. if(!!disabled) {
  471. dropdownToggle.addClass('disabled');
  472. previousTabIndex = dropdownToggle.attr('tabindex');
  473. dropdownToggle.attr('tabindex', '-1');
  474. isDisabled = true;
  475. } else {
  476. dropdownToggle.removeClass('disabled');
  477. if(previousTabIndex) {
  478. dropdownToggle.attr('tabindex', previousTabIndex);
  479. } else {
  480. dropdownToggle.removeAttr('tabindex');
  481. }
  482. isDisabled = false;
  483. }
  484. });
  485. }
  486. /**
  487. * Do some check on modelValue. remove no existing value
  488. * @param values
  489. */
  490. nyaBsSelectCtrl.onCollectionChange = function (values) {
  491. var valuesForSelect = [],
  492. index,
  493. length,
  494. modelValue = ngCtrl.$modelValue;
  495. if(!modelValue) {
  496. return;
  497. }
  498. if(!values || values.length === 0) {
  499. if(isMultiple) {
  500. modelValue = [];
  501. } else {
  502. modelValue = null;
  503. }
  504. } else {
  505. if(valueExpFn) {
  506. for(index = 0; index < values.length; index++) {
  507. valuesForSelect.push(valueExpFn($scope, values[index]));
  508. }
  509. } else {
  510. for(index = 0; index < values.length; index++) {
  511. if(nyaBsSelectCtrl.valueIdentifier) {
  512. valuesForSelect.push(values[index][nyaBsSelectCtrl.valueIdentifier]);
  513. } else if(nyaBsSelectCtrl.keyIdentifier) {
  514. valuesForSelect.push(values[index][nyaBsSelectCtrl.keyIdentifier]);
  515. }
  516. }
  517. }
  518. if(isMultiple) {
  519. length = modelValue.length;
  520. for(index = 0; index < modelValue.length; index++) {
  521. if(!contains(valuesForSelect, modelValue[index])) {
  522. modelValue.splice(index, 1);
  523. index--;
  524. }
  525. }
  526. if(length !== modelValue.length) {
  527. // modelValue changed.
  528. // Due to ngModelController compare reference with the old modelValue, we must set an new array instead of modifying the old one.
  529. // See: https://github.com/angular/angular.js/issues/1751
  530. modelValue = deepCopy(modelValue);
  531. }
  532. } else {
  533. if(!contains(valuesForSelect, modelValue)) {
  534. modelValue = valuesForSelect[0];
  535. }
  536. }
  537. }
  538. ngCtrl.$setViewValue(modelValue);
  539. updateButtonContent();
  540. };
  541. // view --> model
  542. dropdownMenu.on('click', function menuEventHandler (event) {
  543. if(isDisabled) {
  544. return;
  545. }
  546. if(jqLite(event.target).hasClass('dropdown-header')) {
  547. return;
  548. }
  549. var nyaBsOptionNode = filterTarget(event.target, dropdownMenu[0], 'nya-bs-option'),
  550. nyaBsOption;
  551. if(nyaBsOptionNode !== null) {
  552. nyaBsOption = jqLite(nyaBsOptionNode);
  553. if(nyaBsOption.hasClass('disabled')) {
  554. return;
  555. }
  556. selectOption(nyaBsOption);
  557. }
  558. });
  559. // if click the outside of dropdown menu, close the dropdown menu
  560. $document.on('click', function(event) {
  561. if(filterTarget(event.target, $element.parent()[0], $element[0]) === null) {
  562. if($element.hasClass('open')) {
  563. $element.triggerHandler('blur');
  564. }
  565. $element.removeClass('open');
  566. }
  567. });
  568. dropdownToggle.on('blur', function() {
  569. if(!$element.hasClass('open')) {
  570. $element.triggerHandler('blur');
  571. }
  572. });
  573. dropdownToggle.on('click', function() {
  574. var nyaBsOptionNode;
  575. $element.toggleClass('open');
  576. if($element.hasClass('open') && typeof liHeight === 'undefined') {
  577. calcMenuSize();
  578. }
  579. if($attrs.liveSearch === 'true' && $element.hasClass('open')) {
  580. searchBox.children().eq(0)[0].focus();
  581. nyaBsOptionNode = findFocus(true);
  582. if(nyaBsOptionNode) {
  583. dropdownMenu.children().removeClass('active');
  584. jqLite(nyaBsOptionNode).addClass('active');
  585. }
  586. } else if($element.hasClass('open')) {
  587. nyaBsOptionNode = findFocus(true);
  588. if(nyaBsOptionNode) {
  589. setFocus(nyaBsOptionNode);
  590. }
  591. }
  592. });
  593. // live search
  594. if($attrs.liveSearch === 'true') {
  595. searchBox.children().on('input', function(){
  596. var searchKeyword = searchBox.children().val(),
  597. found = 0,
  598. options = dropdownMenu.children(),
  599. length = options.length,
  600. index,
  601. option,
  602. nyaBsOptionNode;
  603. if(searchKeyword) {
  604. for(index = 0; index < length; index++) {
  605. option = options.eq(index);
  606. if(option.hasClass('nya-bs-option')) {
  607. if(!hasKeyword(option.find('a'), searchKeyword)) {
  608. option.addClass('not-match');
  609. } else {
  610. option.removeClass('not-match');
  611. found++;
  612. }
  613. }
  614. }
  615. if(found === 0) {
  616. noSearchResult.addClass('show');
  617. } else {
  618. noSearchResult.removeClass('show');
  619. }
  620. } else {
  621. for(index = 0; index < length; index++) {
  622. option = options.eq(index);
  623. if(option.hasClass('nya-bs-option')) {
  624. option.removeClass('not-match');
  625. }
  626. }
  627. noSearchResult.removeClass('show');
  628. }
  629. nyaBsOptionNode = findFocus(true);
  630. if(nyaBsOptionNode) {
  631. options.removeClass('active');
  632. jqLite(nyaBsOptionNode).addClass('active');
  633. }
  634. });
  635. }
  636. // model --> view
  637. ngCtrl.$render = function() {
  638. var modelValue = ngCtrl.$modelValue,
  639. index,
  640. bsOptionElements = dropdownMenu.children(),
  641. length = bsOptionElements.length,
  642. value;
  643. if(typeof modelValue === 'undefined') {
  644. // if modelValue is undefined. uncheck all option
  645. for(index = 0; index < length; index++) {
  646. if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
  647. bsOptionElements.eq(index).removeClass('selected');
  648. }
  649. }
  650. } else {
  651. for(index = 0; index < length; index++) {
  652. if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
  653. value = getOptionValue(bsOptionElements.eq(index));
  654. if(isMultiple) {
  655. if(contains(modelValue, value)) {
  656. bsOptionElements.eq(index).addClass('selected');
  657. } else {
  658. bsOptionElements.eq(index).removeClass('selected');
  659. }
  660. } else {
  661. if(deepEquals(modelValue, value)) {
  662. bsOptionElements.eq(index).addClass('selected');
  663. } else {
  664. bsOptionElements.eq(index).removeClass('selected');
  665. }
  666. }
  667. }
  668. }
  669. }
  670. updateButtonContent();
  671. };
  672. // simple keyboard support
  673. $element.on('keydown', function(event){
  674. var keyCode = event.keyCode;
  675. if(keyCode !== 27 && keyCode !== 13 && keyCode !== 38 && keyCode !== 40) {
  676. // we only handle special keys. don't waste time to traverse the dom tree.
  677. return;
  678. }
  679. // prevent a click event to be fired.
  680. event.preventDefault();
  681. if(isDisabled) {
  682. event.stopPropagation();
  683. return;
  684. }
  685. var toggleButton = filterTarget(event.target, $element[0], dropdownToggle[0]),
  686. menuContainer,
  687. searchBoxContainer,
  688. liElement,
  689. nyaBsOptionNode;
  690. if($attrs.liveSearch === 'true') {
  691. searchBoxContainer = filterTarget(event.target, $element[0], searchBox[0]);
  692. } else {
  693. menuContainer = filterTarget(event.target, $element[0], dropdownContainer[0])
  694. }
  695. if(toggleButton) {
  696. // press enter to active dropdown
  697. if((keyCode === 13 || keyCode === 38 || keyCode === 40) && !$element.hasClass('open')) {
  698. event.stopPropagation();
  699. $element.addClass('open');
  700. // calculate menu size
  701. if(typeof liHeight === 'undefined') {
  702. calcMenuSize();
  703. }
  704. // if live search enabled. give focus to search box.
  705. if($attrs.liveSearch === 'true') {
  706. searchBox.children().eq(0)[0].focus();
  707. // find the focusable node but we will use active
  708. nyaBsOptionNode = findFocus(true);
  709. if(nyaBsOptionNode) {
  710. // remove previous active state
  711. dropdownMenu.children().removeClass('active');
  712. // set active to first focusable element
  713. jqLite(nyaBsOptionNode).addClass('active');
  714. }
  715. } else {
  716. // otherwise, give focus to first menu item.
  717. nyaBsOptionNode = findFocus(true);
  718. if(nyaBsOptionNode) {
  719. setFocus(nyaBsOptionNode);
  720. }
  721. }
  722. }
  723. // press enter or escape to de-active dropdown
  724. //if((keyCode === 13 || keyCode === 27) && $element.hasClass('open')) {
  725. // $element.removeClass('open');
  726. // event.stopPropagation();
  727. //}
  728. } else if(menuContainer) {
  729. if(keyCode === 27) {
  730. // escape pressed
  731. dropdownToggle[0].focus();
  732. if($element.hasClass('open')) {
  733. $element.triggerHandler('blur');
  734. }
  735. $element.removeClass('open');
  736. event.stopPropagation();
  737. } else if(keyCode === 38) {
  738. event.stopPropagation();
  739. // up arrow key
  740. nyaBsOptionNode = findNextFocus(event.target.parentNode, 'previousSibling');
  741. if(nyaBsOptionNode) {
  742. setFocus(nyaBsOptionNode);
  743. } else {
  744. nyaBsOptionNode = findFocus(false);
  745. if(nyaBsOptionNode) {
  746. setFocus(nyaBsOptionNode);
  747. }
  748. }
  749. } else if(keyCode === 40) {
  750. event.stopPropagation();
  751. // down arrow key
  752. nyaBsOptionNode = findNextFocus(event.target.parentNode, 'nextSibling');
  753. if(nyaBsOptionNode) {
  754. setFocus(nyaBsOptionNode);
  755. } else {
  756. nyaBsOptionNode = findFocus(true);
  757. if(nyaBsOptionNode) {
  758. setFocus(nyaBsOptionNode);
  759. }
  760. }
  761. } else if(keyCode === 13) {
  762. event.stopPropagation();
  763. // enter pressed
  764. liElement = jqLite(event.target.parentNode);
  765. if(liElement.hasClass('nya-bs-option')) {
  766. selectOption(liElement);
  767. if(!isMultiple) {
  768. dropdownToggle[0].focus();
  769. }
  770. }
  771. }
  772. } else if(searchBoxContainer) {
  773. if(keyCode === 27) {
  774. dropdownToggle[0].focus();
  775. $element.removeClass('open');
  776. event.stopPropagation();
  777. } else if(keyCode === 38) {
  778. // up
  779. event.stopPropagation();
  780. liElement = findActive();
  781. if(liElement) {
  782. nyaBsOptionNode = findNextFocus(liElement[0], 'previousSibling');
  783. if(nyaBsOptionNode) {
  784. liElement.removeClass('active');
  785. jqLite(nyaBsOptionNode).addClass('active');
  786. } else {
  787. nyaBsOptionNode = findFocus(false);
  788. if(nyaBsOptionNode) {
  789. liElement.removeClass('active');
  790. jqLite(nyaBsOptionNode).addClass('active');
  791. }
  792. }
  793. }
  794. } else if(keyCode === 40) {
  795. // down
  796. event.stopPropagation();
  797. liElement = findActive();
  798. if(liElement) {
  799. nyaBsOptionNode = findNextFocus(liElement[0], 'nextSibling');
  800. if(nyaBsOptionNode) {
  801. liElement.removeClass('active');
  802. jqLite(nyaBsOptionNode).addClass('active');
  803. } else {
  804. nyaBsOptionNode = findFocus(true);
  805. if(nyaBsOptionNode) {
  806. liElement.removeClass('active');
  807. jqLite(nyaBsOptionNode).addClass('active');
  808. }
  809. }
  810. }
  811. } else if(keyCode === 13) {
  812. // select an option.
  813. liElement = findActive();
  814. if(liElement) {
  815. selectOption(liElement);
  816. if(!isMultiple) {
  817. dropdownToggle[0].focus();
  818. }
  819. }
  820. }
  821. }
  822. });
  823. function findActive() {
  824. var list = dropdownMenu.children(),
  825. i, liElement,
  826. length = list.length;
  827. for(i = 0; i < length; i++) {
  828. liElement = list.eq(i);
  829. if(liElement.hasClass('active') && liElement.hasClass('nya-bs-option')) {
  830. return liElement;
  831. }
  832. }
  833. return null;
  834. }
  835. /**
  836. * setFocus on a nya-bs-option element. it actually set focus on its child anchor element.
  837. * @param elem a nya-bs-option element.
  838. */
  839. function setFocus(elem) {
  840. var childList = elem.childNodes,
  841. length = childList.length,
  842. child;
  843. for(var i = 0; i < length; i++) {
  844. child = childList[i];
  845. if(child.nodeType === 1 && child.tagName.toLowerCase() === 'a') {
  846. child.focus();
  847. break;
  848. }
  849. }
  850. }
  851. function findFocus(fromFirst) {
  852. var firstLiElement;
  853. if(fromFirst) {
  854. firstLiElement = dropdownMenu.children().eq(0);
  855. } else {
  856. firstLiElement = dropdownMenu.children().eq(dropdownMenu.children().length - 1);
  857. }
  858. // focus on selected element
  859. for(var i = 0; i < dropdownMenu.children().length; i++) {
  860. if(dropdownMenu.children().eq(i).hasClass('selected')) {
  861. return dropdownMenu.children().eq(i)[0];
  862. }
  863. }
  864. if(firstLiElement.hasClass('nya-bs-option') && !firstLiElement.hasClass('disabled') && !firstLiElement.hasClass('not-match')) {
  865. return firstLiElement[0];
  866. } else {
  867. if(fromFirst) {
  868. return findNextFocus(firstLiElement[0], 'nextSibling');
  869. } else {
  870. return findNextFocus(firstLiElement[0], 'previousSibling');
  871. }
  872. }
  873. }
  874. /**
  875. * find next focusable element on direction
  876. * @param from the element traversed from
  877. * @param direction can be 'nextSibling' or 'previousSibling'
  878. * @returns the element if found, otherwise return null.
  879. */
  880. function findNextFocus(from, direction) {
  881. if(from && !hasClass(from, 'nya-bs-option')) {
  882. return;
  883. }
  884. var next = from;
  885. while ((next = sibling(next, direction)) && next.nodeType) {
  886. if(hasClass(next,'nya-bs-option') && !hasClass(next, 'disabled') && !hasClass(next, 'not-match')) {
  887. return next
  888. }
  889. }
  890. return null;
  891. }
  892. /**
  893. * select an option represented by nyaBsOption argument. Get the option's value and update model.
  894. * if isMultiple = true, doesn't close dropdown menu. otherwise close the menu.
  895. * @param nyaBsOption the jqLite wrapped `nya-bs-option` element.
  896. */
  897. function selectOption(nyaBsOption) {
  898. var value,
  899. viewValue,
  900. modelValue = ngCtrl.$modelValue,
  901. index;
  902. // if user specify the value attribute. we should use the value attribute
  903. // otherwise, use the valueIdentifier specified field in target scope
  904. value = getOptionValue(nyaBsOption);
  905. if(typeof value !== 'undefined') {
  906. if(isMultiple) {
  907. // make a deep copy enforce ngModelController to call its $render method.
  908. // See: https://github.com/angular/angular.js/issues/1751
  909. viewValue = Array.isArray(modelValue) ? deepCopy(modelValue) : [];
  910. index = indexOf(viewValue, value);
  911. if(index === -1) {
  912. // check element
  913. viewValue.push(value);
  914. nyaBsOption.addClass('selected');
  915. } else {
  916. // uncheck element
  917. viewValue.splice(index, 1);
  918. nyaBsOption.removeClass('selected');
  919. }
  920. } else {
  921. dropdownMenu.children().removeClass('selected');
  922. viewValue = value;
  923. nyaBsOption.addClass('selected');
  924. }
  925. }
  926. // update view value regardless
  927. ngCtrl.$setViewValue(viewValue);
  928. $scope.$digest();
  929. if(!isMultiple) {
  930. // in single selection mode. close the dropdown menu
  931. if($element.hasClass('open')) {
  932. $element.triggerHandler('blur');
  933. }
  934. $element.removeClass('open');
  935. }
  936. updateButtonContent();
  937. }
  938. /**
  939. * get a value of current nyaBsOption. according to different setting.
  940. * - if `nya-bs-option` directive is used to populate options and a `value` attribute is specified. use expression of the attribute value.
  941. * - if `nya-bs-option` directive is used to populate options and no other settings, use the valueIdentifier or keyIdentifier to retrieve value from scope of current nyaBsOption.
  942. * - if `nya-bs-option` class is used on static options. use literal value of the `value` attribute.
  943. * @param nyaBsOption a jqLite wrapped `nya-bs-option` element
  944. */
  945. function getOptionValue(nyaBsOption) {
  946. var scopeOfOption;
  947. if(valueExpFn) {
  948. scopeOfOption = nyaBsOption.scope();
  949. return valueExpFn(scopeOfOption);
  950. } else {
  951. if(nyaBsSelectCtrl.valueIdentifier || nyaBsSelectCtrl.keyIdentifier) {
  952. scopeOfOption = nyaBsOption.scope();
  953. return scopeOfOption[nyaBsSelectCtrl.valueIdentifier] || scopeOfOption[nyaBsSelectCtrl.keyIdentifier];
  954. } else {
  955. return nyaBsOption.attr('value');
  956. }
  957. }
  958. }
  959. function getOptionText(nyaBsOption) {
  960. var item = nyaBsOption.find('a');
  961. if(item.children().length === 0 || item.children().eq(0).hasClass('check-mark')) {
  962. // if the first child is check-mark or has no children, means the option text is text node
  963. return item[0].firstChild.cloneNode(false);
  964. } else {
  965. // otherwise we clone the first element of the item
  966. return item.children().eq(0)[0].cloneNode(true);
  967. }
  968. }
  969. function updateButtonContent() {
  970. var modelValue = ngCtrl.$modelValue;
  971. $element.triggerHandler('change');
  972. var filterOption = dropdownToggle.children().eq(0);
  973. if(typeof modelValue === 'undefined') {
  974. /**
  975. * Select empty option when model is undefined.
  976. */
  977. filterOption.empty();
  978. filterOption.append(getDefaultNoneSelectionContent());
  979. return;
  980. }
  981. if(isMultiple && modelValue.length === 0) {
  982. filterOption.empty();
  983. filterOption.append(getDefaultNoneSelectionContent());
  984. } else {
  985. $timeout(function() {
  986. var bsOptionElements = dropdownMenu.children(),
  987. value,
  988. nyaBsOption,
  989. index,
  990. length = bsOptionElements.length,
  991. optionTitle,
  992. selection = [],
  993. match,
  994. count;
  995. if(isMultiple && $attrs.selectedTextFormat === 'count') {
  996. count = 1;
  997. } else if(isMultiple && $attrs.selectedTextFormat && (match = $attrs.selectedTextFormat.match(/\s*count\s*>\s*(\d+)\s*/))) {
  998. count = parseInt(match[1], 10);
  999. }
  1000. // data-selected-text-format="count" or data-selected-text-format="count>x"
  1001. if((typeof count !== 'undefined') && modelValue.length > count) {
  1002. filterOption.empty();
  1003. if(localizedText.numberItemSelectedTpl) {
  1004. filterOption.append(jqLite(localizedText.numberItemSelectedTpl.replace('%d', modelValue.length)));
  1005. } else if(localizedText.numberItemSelected) {
  1006. filterOption.append(document.createTextNode(localizedText.numberItemSelected.replace('%d', modelValue.length)));
  1007. } else {
  1008. filterOption.append(document.createTextNode(modelValue.length + ' items selected'));
  1009. }
  1010. return;
  1011. }
  1012. // data-selected-text-format="values" or the number of selected items is less than count
  1013. for(index = 0; index < length; index++) {
  1014. nyaBsOption = bsOptionElements.eq(index);
  1015. if(nyaBsOption.hasClass('nya-bs-option')) {
  1016. value = getOptionValue(nyaBsOption);
  1017. if(isMultiple) {
  1018. if(Array.isArray(modelValue) && contains(modelValue, value)) {
  1019. // if option has an title attribute. use the title value as content show in button.
  1020. // otherwise get very first child element.
  1021. optionTitle = nyaBsOption.attr('title');
  1022. if(optionTitle) {
  1023. selection.push(document.createTextNode(optionTitle));
  1024. } else {
  1025. selection.push(getOptionText(nyaBsOption));
  1026. }
  1027. }
  1028. } else {
  1029. if(deepEquals(modelValue, value)) {
  1030. optionTitle = nyaBsOption.attr('title');
  1031. if(optionTitle) {
  1032. selection.push(document.createTextNode(optionTitle));
  1033. } else {
  1034. selection.push(getOptionText(nyaBsOption));
  1035. }
  1036. }
  1037. }
  1038. }
  1039. }
  1040. if(selection.length === 0) {
  1041. filterOption.empty();
  1042. filterOption.append(getDefaultNoneSelectionContent());
  1043. } else if(selection.length === 1) {
  1044. // either single or multiple selection will show the only selected content.
  1045. filterOption.empty();
  1046. filterOption.append(selection[0]);
  1047. } else {
  1048. filterOption.empty();
  1049. for(index = 0; index < selection.length; index++) {
  1050. filterOption.append(selection[index]);
  1051. if(index < selection.length -1) {
  1052. filterOption.append(document.createTextNode(', '));
  1053. }
  1054. }
  1055. }
  1056. });
  1057. }
  1058. }
  1059. // will called only once.
  1060. function calcMenuSize(){
  1061. var liElements = dropdownMenu.find('li'),
  1062. length = liElements.length,
  1063. liElement,
  1064. i;
  1065. for(i = 0; i < length; i++) {
  1066. liElement = liElements.eq(i);
  1067. if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
  1068. liHeight = liElement[0].clientHeight;
  1069. break;
  1070. }
  1071. }
  1072. if(/\d+/.test($attrs.size)) {
  1073. var dropdownSize = parseInt($attrs.size, 10);
  1074. dropdownMenu.css('max-height', (dropdownSize * liHeight) + 'px');
  1075. dropdownMenu.css('overflow-y', 'auto');
  1076. }
  1077. }
  1078. };
  1079. }
  1080. };
  1081. }]);
  1082. nyaBsSelect.directive('nyaBsOption', ['$parse', function($parse){
  1083. //00000011111111111111100000000022222222222222200000003333333333333330000000000000004444444444000000000000000000055555555550000000000000000000006666666666000000
  1084. var BS_OPTION_REGEX = /^\s*(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/;
  1085. return {
  1086. restrict: 'A',
  1087. transclude: 'element',
  1088. priority: 1000,
  1089. terminal: true,
  1090. require: ['^nyaBsSelect', '^ngModel'],
  1091. compile: function nyaBsOptionCompile (tElement, tAttrs) {
  1092. var expression = tAttrs.nyaBsOption;
  1093. var nyaBsOptionEndComment = document.createComment(' end nyaBsOption: ' + expression + ' ');
  1094. var match = expression.match(BS_OPTION_REGEX);
  1095. if(!match) {
  1096. throw new Error('invalid expression');
  1097. }
  1098. // we want to keep our expression comprehensible so we don't use 'select as label for value in collection' expression.
  1099. var valueExp = tAttrs.value,
  1100. valueExpGetter = valueExp ? $parse(valueExp) : null;
  1101. var valueIdentifier = match[3] || match[1],
  1102. keyIdentifier = match[2],
  1103. collectionExp = match[4],
  1104. groupByExpGetter = match[5] ? $parse(match[5]) : null,
  1105. trackByExp = match[6];
  1106. var trackByIdArrayFn,
  1107. trackByIdObjFn,
  1108. trackByIdExpFn,
  1109. trackByExpGetter;
  1110. var hashFnLocals = {$id: hashKey};
  1111. var groupByFn, locals = {};
  1112. if(trackByExp) {
  1113. trackByExpGetter = $parse(trackByExp);
  1114. } else {
  1115. trackByIdArrayFn = function(key, value) {
  1116. return hashKey(value);
  1117. };
  1118. trackByIdObjFn = function(key) {
  1119. return key;
  1120. };
  1121. }
  1122. return function nyaBsOptionLink($scope, $element, $attr, ctrls, $transclude) {
  1123. var nyaBsSelectCtrl = ctrls[0],
  1124. ngCtrl = ctrls[1],
  1125. valueExpFn,
  1126. valueExpLocals = {};
  1127. if(trackByExpGetter) {
  1128. trackByIdExpFn = function(key, value, index) {
  1129. // assign key, value, and $index to the locals so that they can be used in hash functions
  1130. if (keyIdentifier) {
  1131. hashFnLocals[keyIdentifier] = key;
  1132. }
  1133. hashFnLocals[valueIdentifier] = value;
  1134. hashFnLocals.$index = index;
  1135. return trackByExpGetter($scope, hashFnLocals);
  1136. };
  1137. }
  1138. if(groupByExpGetter) {
  1139. groupByFn = function(key, value) {
  1140. if(keyIdentifier) {
  1141. locals[keyIdentifier] = key;
  1142. }
  1143. locals[valueIdentifier] = value;
  1144. return groupByExpGetter($scope, locals);
  1145. }
  1146. }
  1147. // set keyIdentifier and valueIdentifier property of nyaBsSelectCtrl
  1148. if(keyIdentifier) {
  1149. nyaBsSelectCtrl.keyIdentifier = keyIdentifier;
  1150. }
  1151. if(valueIdentifier) {
  1152. nyaBsSelectCtrl.valueIdentifier = valueIdentifier;
  1153. }
  1154. if(valueExpGetter) {
  1155. nyaBsSelectCtrl.valueExp = valueExp;
  1156. valueExpFn = function(key, value) {
  1157. if(keyIdentifier) {
  1158. valueExpLocals[keyIdentifier] = key;
  1159. }
  1160. valueExpLocals[valueIdentifier] = value;
  1161. return valueExpGetter($scope, valueExpLocals);
  1162. }
  1163. }
  1164. // Store a list of elements from previous run. This is a hash where key is the item from the
  1165. // iterator, and the value is objects with following properties.
  1166. // - scope: bound scope
  1167. // - element: previous element.
  1168. // - index: position
  1169. //
  1170. // We are using no-proto object so that we don't need to guard against inherited props via
  1171. // hasOwnProperty.
  1172. var lastBlockMap = createMap();
  1173. // deepWatch will impact performance. use with caution.
  1174. if($attr.deepWatch === 'true') {
  1175. $scope.$watch(collectionExp, nyaBsOptionAction, true);
  1176. } else {
  1177. $scope.$watchCollection(collectionExp, nyaBsOptionAction);
  1178. }
  1179. function nyaBsOptionAction(collection) {
  1180. var index,
  1181. previousNode = $element[0], // node that cloned nodes should be inserted after
  1182. // initialized to the comment node anchor
  1183. key, value,
  1184. trackById,
  1185. trackByIdFn,
  1186. collectionKeys,
  1187. collectionLength,
  1188. // Same as lastBlockMap but it has the current state. It will become the
  1189. // lastBlockMap on the next iteration.
  1190. nextBlockMap = createMap(),
  1191. nextBlockOrder,
  1192. block,
  1193. groupName,
  1194. nextNode,
  1195. group,
  1196. lastGroup,
  1197. values = [],
  1198. valueObj; // the collection value
  1199. if(groupByFn) {
  1200. group = [];
  1201. }
  1202. if(isArrayLike(collection)) {
  1203. collectionKeys = collection;
  1204. trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
  1205. } else {
  1206. trackByIdFn = trackByIdExpFn || trackByIdObjFn;
  1207. // if object, extract keys, sort them and use to determine order of iteration over obj props
  1208. collectionKeys = [];
  1209. for (var itemKey in collection) {
  1210. if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') {
  1211. collectionKeys.push(itemKey);
  1212. }
  1213. }
  1214. collectionKeys.sort();
  1215. }
  1216. collectionLength = collectionKeys.length;
  1217. nextBlockOrder = new Array(collectionLength);
  1218. for(index = 0; index < collectionLength; index++) {
  1219. key = (collection === collectionKeys) ? index : collectionKeys[index];
  1220. value = collection[key];
  1221. trackById = trackByIdFn(key, value, index);
  1222. // copy the value with scope like structure to notify the select directive.
  1223. valueObj = {};
  1224. if(keyIdentifier) {
  1225. valueObj[keyIdentifier] = key;
  1226. }
  1227. valueObj[valueIdentifier] = value;
  1228. values.push(valueObj);
  1229. if(groupByFn) {
  1230. groupName = groupByFn(key, value);
  1231. if(group.indexOf(groupName) === -1 && groupName) {
  1232. group.push(groupName);
  1233. }
  1234. }
  1235. if(lastBlockMap[trackById]) {
  1236. // found previously seen block
  1237. block = lastBlockMap[trackById];
  1238. delete lastBlockMap[trackById];
  1239. // must update block here because some data we stored may change.
  1240. if(groupByFn) {
  1241. block.group = groupName;
  1242. }
  1243. block.key = key;
  1244. block.value = value;
  1245. nextBlockMap[trackById] = block;
  1246. nextBlockOrder[index] = block;
  1247. } else if(nextBlockMap[trackById]) {
  1248. //if collision detected. restore lastBlockMap and throw an error
  1249. nextBlockOrder.forEach(function(block) {
  1250. if(block && block.scope) {
  1251. lastBlockMap[block.id] = block;
  1252. }
  1253. });
  1254. throw new Error("Duplicates in a select are not allowed. Use 'track by' expression to specify unique keys.");
  1255. } else {
  1256. // new never before seen block
  1257. nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined, key: key, value: value};
  1258. nextBlockMap[trackById] = true;
  1259. if(groupName) {
  1260. nextBlockOrder[index].group = groupName;
  1261. }
  1262. }
  1263. }
  1264. // only resort nextBlockOrder when group found
  1265. if(group && group.length > 0) {
  1266. nextBlockOrder = sortByGroup(nextBlockOrder, group, 'group');
  1267. }
  1268. // remove DOM nodes
  1269. for( var blockKey in lastBlockMap) {
  1270. block = lastBlockMap[blockKey];
  1271. getBlockNodes(block.clone).remove();
  1272. block.scope.$destroy();
  1273. }
  1274. for(index = 0; index < collectionLength; index++) {
  1275. block = nextBlockOrder[index];
  1276. if(block.scope) {
  1277. // if we have already seen this object, then we need to reuse the
  1278. // associated scope/element
  1279. nextNode = previousNode;
  1280. if(getBlockStart(block) != nextNode) {
  1281. jqLite(previousNode).after(block.clone);
  1282. }
  1283. previousNode = getBlockEnd(block);
  1284. updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
  1285. } else {
  1286. $transclude(function nyaBsOptionTransclude(clone, scope) {
  1287. block.scope = scope;
  1288. var endNode = nyaBsOptionEndComment.cloneNode(false);
  1289. clone[clone.length++] = endNode;
  1290. jqLite(previousNode).after(clone);
  1291. // add nya-bs-option class
  1292. clone.addClass('nya-bs-option');
  1293. // for newly created item we need to ensure its selected status from the model value.
  1294. if(valueExpFn) {
  1295. value = valueExpFn(block.key, block.value);
  1296. } else {
  1297. value = block.value || block.key;
  1298. }
  1299. if(nyaBsSelectCtrl.isMultiple) {
  1300. if(Array.isArray(ngCtrl.$modelValue) && contains(ngCtrl.$modelValue, value)) {
  1301. clone.addClass('selected');
  1302. }
  1303. } else {
  1304. if(deepEquals(value, ngCtrl.$modelValue)) {
  1305. clone.addClass('selected');
  1306. }
  1307. }
  1308. previousNode = endNode;
  1309. // Note: We only need the first/last node of the cloned nodes.
  1310. // However, we need to keep the reference to the jqlite wrapper as it might be changed later
  1311. // by a directive with templateUrl when its template arrives.
  1312. block.clone = clone;
  1313. nextBlockMap[block.id] = block;
  1314. updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
  1315. });
  1316. }
  1317. // we need to mark the first item of a group
  1318. if(group) {
  1319. if(!lastGroup || lastGroup !== block.group) {
  1320. block.clone.addClass('first-in-group');
  1321. } else {
  1322. block.clone.removeClass('first-in-group');
  1323. }
  1324. lastGroup = block.group;
  1325. // add special class for indent
  1326. block.clone.addClass('group-item');
  1327. }
  1328. }
  1329. lastBlockMap = nextBlockMap;
  1330. nyaBsSelectCtrl.onCollectionChange(values);
  1331. }
  1332. };
  1333. }
  1334. }
  1335. }]);
  1336. })();