1723 lines
No EOL
57 KiB
JavaScript
1723 lines
No EOL
57 KiB
JavaScript
/**
|
|
* nya-bootstrap-select v2.1.6
|
|
* Copyright 2014 Nyasoft
|
|
* Licensed under MIT license
|
|
*/
|
|
(function(){
|
|
'use strict';
|
|
|
|
|
|
var uid = 0;
|
|
|
|
function nextUid() {
|
|
return ++uid;
|
|
}
|
|
|
|
/**
|
|
* Checks if `obj` is a window object.
|
|
*
|
|
* @private
|
|
* @param {*} obj Object to check
|
|
* @returns {boolean} True if `obj` is a window obj.
|
|
*/
|
|
function isWindow(obj) {
|
|
return obj && obj.window === obj;
|
|
}
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name angular.isString
|
|
* @module ng
|
|
* @kind function
|
|
*
|
|
* @description
|
|
* Determines if a reference is a `String`.
|
|
*
|
|
* @param {*} value Reference to check.
|
|
* @returns {boolean} True if `value` is a `String`.
|
|
*/
|
|
function isString(value){return typeof value === 'string';}
|
|
|
|
/**
|
|
* @param {*} obj
|
|
* @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments,
|
|
* String ...)
|
|
*/
|
|
function isArrayLike(obj) {
|
|
if (obj == null || isWindow(obj)) {
|
|
return false;
|
|
}
|
|
|
|
var length = obj.length;
|
|
|
|
if (obj.nodeType === 1 && length) {
|
|
return true;
|
|
}
|
|
|
|
return isString(obj) || Array.isArray(obj) || length === 0 ||
|
|
typeof length === 'number' && length > 0 && (length - 1) in obj;
|
|
}
|
|
|
|
/**
|
|
* Creates a new object without a prototype. This object is useful for lookup without having to
|
|
* guard against prototypically inherited properties via hasOwnProperty.
|
|
*
|
|
* Related micro-benchmarks:
|
|
* - http://jsperf.com/object-create2
|
|
* - http://jsperf.com/proto-map-lookup/2
|
|
* - http://jsperf.com/for-in-vs-object-keys2
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
function createMap() {
|
|
return Object.create(null);
|
|
}
|
|
|
|
/**
|
|
* Computes a hash of an 'obj'.
|
|
* Hash of a:
|
|
* string is string
|
|
* number is number as string
|
|
* object is either result of calling $$hashKey function on the object or uniquely generated id,
|
|
* that is also assigned to the $$hashKey property of the object.
|
|
*
|
|
* @param obj
|
|
* @returns {string} hash string such that the same input will have the same hash string.
|
|
* The resulting string key is in 'type:hashKey' format.
|
|
*/
|
|
function hashKey(obj, nextUidFn) {
|
|
var objType = typeof obj,
|
|
key;
|
|
|
|
if (objType == 'function' || (objType == 'object' && obj !== null)) {
|
|
if (typeof (key = obj.$$hashKey) == 'function') {
|
|
// must invoke on object to keep the right this
|
|
key = obj.$$hashKey();
|
|
} else if (key === undefined) {
|
|
key = obj.$$hashKey = (nextUidFn || nextUid)();
|
|
}
|
|
} else {
|
|
key = obj;
|
|
}
|
|
|
|
return objType + ':' + key;
|
|
}
|
|
|
|
//TODO: use with caution. if an property of element in array doesn't exist in group, the resultArray may lose some element.
|
|
function sortByGroup(array ,group, property) {
|
|
var unknownGroup = [],
|
|
i, j,
|
|
resultArray = [];
|
|
for(i = 0; i < group.length; i++) {
|
|
for(j = 0; j < array.length;j ++) {
|
|
if(!array[j][property]) {
|
|
unknownGroup.push(array[j]);
|
|
} else if(array[j][property] === group[i]) {
|
|
resultArray.push(array[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
resultArray = resultArray.concat(unknownGroup);
|
|
|
|
return resultArray;
|
|
}
|
|
|
|
/**
|
|
* Return the DOM siblings between the first and last node in the given array.
|
|
* @param {Array} array like object
|
|
* @returns {jqLite} jqLite collection containing the nodes
|
|
*/
|
|
function getBlockNodes(nodes) {
|
|
// TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
|
|
// collection, otherwise update the original collection.
|
|
var node = nodes[0];
|
|
var endNode = nodes[nodes.length - 1];
|
|
var blockNodes = [node];
|
|
|
|
do {
|
|
node = node.nextSibling;
|
|
if (!node) break;
|
|
blockNodes.push(node);
|
|
} while (node !== endNode);
|
|
|
|
return angular.element(blockNodes);
|
|
}
|
|
|
|
var getBlockStart = function(block) {
|
|
return block.clone[0];
|
|
};
|
|
|
|
var getBlockEnd = function(block) {
|
|
return block.clone[block.clone.length - 1];
|
|
};
|
|
|
|
var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength, group) {
|
|
// TODO(perf): generate setters to shave off ~40ms or 1-1.5%
|
|
scope[valueIdentifier] = value;
|
|
if (keyIdentifier) scope[keyIdentifier] = key;
|
|
scope.$index = index;
|
|
scope.$first = (index === 0);
|
|
scope.$last = (index === (arrayLength - 1));
|
|
scope.$middle = !(scope.$first || scope.$last);
|
|
// jshint bitwise: false
|
|
scope.$odd = !(scope.$even = (index&1) === 0);
|
|
// jshint bitwise: true
|
|
|
|
if(group) {
|
|
scope.$group = group;
|
|
}
|
|
};
|
|
|
|
var setElementIsolateScope = function(element, scope) {
|
|
element.data('isolateScope', scope);
|
|
};
|
|
|
|
var contains = function(array, element) {
|
|
var length = array.length,
|
|
i;
|
|
if(length === 0) {
|
|
return false;
|
|
}
|
|
for(i = 0;i < length; i++) {
|
|
if(deepEquals(element, array[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
var indexOf = function(array, element) {
|
|
var length = array.length,
|
|
i;
|
|
if(length === 0) {
|
|
return -1;
|
|
}
|
|
for(i = 0; i < length; i++) {
|
|
if(deepEquals(element, array[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* filter the event target for the nya-bs-option element.
|
|
* Use this method with event delegate. (attach a event handler on an parent element and listen the special children elements)
|
|
* @param target event.target node
|
|
* @param parent {object} the parent, where the event handler attached.
|
|
* @param selector {string}|{object} a class or DOM element
|
|
* @return the filtered target or null if no element satisfied the selector.
|
|
*/
|
|
var filterTarget = function(target, parent, selector) {
|
|
var elem = target,
|
|
className, type = typeof selector;
|
|
|
|
if(target == parent) {
|
|
return null;
|
|
} else {
|
|
do {
|
|
if(type === 'string') {
|
|
className = ' ' + elem.className + ' ';
|
|
if(elem.nodeType === 1 && className.replace(/[\t\r\n\f]/g, ' ').indexOf(selector) >= 0) {
|
|
return elem;
|
|
}
|
|
} else {
|
|
if(elem == selector) {
|
|
return elem;
|
|
}
|
|
}
|
|
|
|
} while((elem = elem.parentNode) && elem != parent && elem.nodeType !== 9);
|
|
|
|
return null;
|
|
}
|
|
|
|
};
|
|
|
|
var getClassList = function(element) {
|
|
var classList,
|
|
className = element.className.replace(/[\t\r\n\f]/g, ' ').trim();
|
|
classList = className.split(' ');
|
|
for(var i = 0; i < classList.length; i++) {
|
|
if(/\s+/.test(classList[i])) {
|
|
classList.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
return classList;
|
|
|
|
};
|
|
|
|
// work with node element
|
|
var hasClass = function(element, className) {
|
|
var classList = getClassList(element);
|
|
return classList.indexOf(className) !== -1;
|
|
};
|
|
|
|
// query children by class(one or more)
|
|
var queryChildren = function(element, classList) {
|
|
var children = element.children(),
|
|
length = children.length,
|
|
child,
|
|
valid,
|
|
classes;
|
|
if(length > 0) {
|
|
for(var i = 0; i < length; i++) {
|
|
child = children.eq(i);
|
|
valid = true;
|
|
classes = getClassList(child[0]);
|
|
if(classes.length > 0) {
|
|
for(var j = 0; j < classList.length; j++) {
|
|
if(classes.indexOf(classList[j]) === -1) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(valid) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
};
|
|
|
|
/**
|
|
* Current support only drill down one level.
|
|
* case insensitive
|
|
* @param element
|
|
* @param keyword
|
|
*/
|
|
var hasKeyword = function(element, keyword) {
|
|
var childElements,
|
|
index, length;
|
|
if(element.text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
|
|
return true;
|
|
} else {
|
|
childElements = element.children();
|
|
length = childElements.length;
|
|
for(index = 0; index < length; index++) {
|
|
if(childElements.eq(index).text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
function sibling( cur, dir ) {
|
|
while ( (cur = cur[dir]) && cur.nodeType !== 1) {}
|
|
return cur;
|
|
}
|
|
|
|
|
|
// map global property to local variable.
|
|
var jqLite = angular.element;
|
|
|
|
var deepEquals = angular.equals;
|
|
|
|
var deepCopy = angular.copy;
|
|
|
|
var extend = angular.extend;
|
|
|
|
var nyaBsSelect = angular.module('nya.bootstrap.select', []);
|
|
|
|
/**
|
|
* A service for configuration. the configuration is shared globally.
|
|
* Testing ci build --jpmckearin
|
|
*/
|
|
nyaBsSelect.provider('nyaBsConfig', function() {
|
|
|
|
var locale = null;
|
|
|
|
// default localized text. cannot be modified.
|
|
var defaultText = {
|
|
'en-us': {
|
|
defaultNoneSelection: 'Nothing selected',
|
|
noSearchResult: 'NO SEARCH RESULT',
|
|
numberItemSelected: '%d items selected',
|
|
selectAll: 'Select All',
|
|
deselectAll: 'Deselect All'
|
|
}
|
|
};
|
|
|
|
// localized text which actually being used.
|
|
var interfaceText = deepCopy(defaultText);
|
|
|
|
/**
|
|
* Merge with default localized text.
|
|
* @param localeId a string formatted as languageId-countryId
|
|
* @param obj localized text object.
|
|
*/
|
|
this.setLocalizedText = function(localeId, obj) {
|
|
if(!localeId) {
|
|
throw new Error('localeId must be a string formatted as languageId-countryId');
|
|
}
|
|
if(!interfaceText[localeId]) {
|
|
interfaceText[localeId] = {};
|
|
}
|
|
interfaceText[localeId] = extend(interfaceText[localeId], obj);
|
|
};
|
|
|
|
/**
|
|
* Force to use a special locale id. if localeId is null. reset to user-agent locale.
|
|
* @param localeId a string formatted as languageId-countryId
|
|
*/
|
|
this.useLocale = function(localeId) {
|
|
locale = localeId;
|
|
};
|
|
|
|
/**
|
|
* get the localized text according current locale or forced locale
|
|
* @returns localizedText
|
|
*/
|
|
this.$get = ['$locale', function($locale){
|
|
var localizedText;
|
|
if(locale) {
|
|
localizedText = interfaceText[locale];
|
|
} else {
|
|
localizedText = interfaceText[$locale.id];
|
|
}
|
|
if(!localizedText) {
|
|
localizedText = defaultText['en-us'];
|
|
}
|
|
return localizedText;
|
|
}];
|
|
|
|
});
|
|
|
|
|
|
nyaBsSelect.controller('nyaBsSelectCtrl', function(){
|
|
|
|
var self = this;
|
|
|
|
// keyIdentifier and valueIdentifier are set by nyaBsOption directive
|
|
// used by nyaBsSelect directive to retrieve key and value from each nyaBsOption's child scope.
|
|
self.keyIdentifier = null;
|
|
self.valueIdentifier = null;
|
|
|
|
self.isMultiple = false;
|
|
|
|
// Should be override by nyaBsSelect directive and called by nyaBsOption directive when collection is changed.
|
|
self.onCollectionChange = function(){};
|
|
|
|
// for debug
|
|
self.setId = function(id) {
|
|
self.id = id || 'id#' + Math.floor(Math.random() * 10000);
|
|
};
|
|
|
|
});
|
|
nyaBsSelect.directive('nyaBsSelect', ['$parse', '$document', '$timeout', '$compile', 'nyaBsConfig', function ($parse, $document, $timeout, $compile, nyaBsConfig) {
|
|
|
|
var DEFAULT_NONE_SELECTION = 'Nothing selected';
|
|
|
|
var DROPDOWN_TOGGLE = '<button class="btn btn-default dropdown-toggle" type="button">' +
|
|
'<span class="pull-left filter-option"></span>' +
|
|
'<span class="pull-left special-title"></span>' +
|
|
' ' +
|
|
'<span class="caret"></span>' +
|
|
'</button>';
|
|
|
|
var DROPDOWN_CONTAINER = '<div class="dropdown-menu open"></div>';
|
|
|
|
var SEARCH_BOX = '<div class="bs-searchbox">' +
|
|
'<input type="text" class="form-control">' +
|
|
'</div>';
|
|
|
|
var DROPDOWN_MENU = '<ul class="dropdown-menu inner"></ul>';
|
|
|
|
var NO_SEARCH_RESULT = '<li class="no-search-result"><span>NO SEARCH RESULT</span></li>';
|
|
|
|
var ACTIONS_BOX = '<div class="bs-actionsbox">' +
|
|
'<div class="btn-group btn-group-sm btn-block">' +
|
|
'<button class="actions-btn bs-select-all btn btn-default">SELECT ALL</button>' +
|
|
'<button class="actions-btn bs-deselect-all btn btn-default">DESELECT ALL</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
return {
|
|
restrict: 'ECA',
|
|
require: ['ngModel', 'nyaBsSelect'],
|
|
controller: 'nyaBsSelectCtrl',
|
|
compile: function nyaBsSelectCompile (tElement, tAttrs){
|
|
|
|
|
|
tElement.addClass('btn-group');
|
|
|
|
|
|
/**
|
|
* get the default text when nothing is selected. can be template
|
|
* @param scope, if provided, will try to compile template with given scope, will not attempt to compile the pure text.
|
|
* @returns {*}
|
|
*/
|
|
var getDefaultNoneSelectionContent = function(scope) {
|
|
// text node or jqLite element.
|
|
var content;
|
|
|
|
if(tAttrs.titleTpl) {
|
|
// use title-tpl attribute value.
|
|
content = jqLite(tAttrs.titleTpl);
|
|
} else if(tAttrs.title) {
|
|
// use title attribute value.
|
|
content = document.createTextNode(tAttrs.title);
|
|
} else if(localizedText.defaultNoneSelectionTpl){
|
|
// use localized text template.
|
|
content = jqLite(localizedText.defaultNoneSelectionTpl);
|
|
} else if(localizedText.defaultNoneSelection) {
|
|
// use localized text.
|
|
content = document.createTextNode(localizedText.defaultNoneSelection);
|
|
} else {
|
|
// use default.
|
|
content = document.createTextNode(DEFAULT_NONE_SELECTION);
|
|
}
|
|
|
|
if(scope && (tAttrs.titleTpl || localizedText.defaultNoneSelectionTpl)) {
|
|
|
|
return $compile(content)(scope);
|
|
}
|
|
|
|
return content;
|
|
};
|
|
|
|
var options = tElement.children(),
|
|
dropdownToggle = jqLite(DROPDOWN_TOGGLE),
|
|
dropdownContainer = jqLite(DROPDOWN_CONTAINER),
|
|
dropdownMenu = jqLite(DROPDOWN_MENU),
|
|
searchBox,
|
|
noSearchResult,
|
|
actionsBox,
|
|
classList,
|
|
length,
|
|
index,
|
|
liElement,
|
|
localizedText = nyaBsConfig,
|
|
isMultiple = typeof tAttrs.multiple !== 'undefined',
|
|
nyaBsOptionValue;
|
|
|
|
classList = getClassList(tElement[0]);
|
|
classList.forEach(function(className) {
|
|
if(/btn-(?:primary|info|success|warning|danger|inverse)/.test(className)) {
|
|
tElement.removeClass(className);
|
|
dropdownToggle.removeClass('btn-default');
|
|
dropdownToggle.addClass(className);
|
|
}
|
|
|
|
if(/btn-(?:lg|sm|xs)/.test(className)) {
|
|
tElement.removeClass(className);
|
|
dropdownToggle.addClass(className);
|
|
}
|
|
|
|
if(className === 'form-control') {
|
|
dropdownToggle.addClass(className);
|
|
}
|
|
});
|
|
|
|
dropdownMenu.append(options);
|
|
|
|
// add tabindex to children anchor elements if not present.
|
|
// tabindex attribute will give an anchor element ability to be get focused.
|
|
length = options.length;
|
|
for(index = 0; index < length; index++) {
|
|
liElement = options.eq(index);
|
|
if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
|
|
liElement.find('a').attr('tabindex', '0');
|
|
// In order to be compatible with old version, we should copy value of value attribute into data-value attribute.
|
|
// For the reason we use data-value instead, see http://nya.io/AngularJS/Beware-Of-Using-value-Attribute-On-list-element/
|
|
nyaBsOptionValue = liElement.attr('value');
|
|
if(angular.isString(nyaBsOptionValue) && nyaBsOptionValue !== '') {
|
|
liElement.attr('data-value', nyaBsOptionValue);
|
|
liElement.removeAttr('value');
|
|
}
|
|
}
|
|
}
|
|
|
|
if(tAttrs.liveSearch === 'true') {
|
|
searchBox = jqLite(SEARCH_BOX);
|
|
|
|
if(tAttrs.noSearchTitle) {
|
|
NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', tAttrs.noSearchTitle);
|
|
} else if (tAttrs.noSearchTitleTpl) {
|
|
NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', tAttrs.noSearchTitleTpl);
|
|
}else {
|
|
// set localized text
|
|
if(localizedText.noSearchResultTpl) {
|
|
NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResultTpl);
|
|
} else if(localizedText.noSearchResult) {
|
|
NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResult);
|
|
}
|
|
}
|
|
|
|
noSearchResult = jqLite(NO_SEARCH_RESULT);
|
|
dropdownContainer.append(searchBox);
|
|
dropdownMenu.append(noSearchResult);
|
|
}
|
|
|
|
if (tAttrs.actionsBox === 'true' && isMultiple) {
|
|
// set localizedText
|
|
if (localizedText.selectAllTpl) {
|
|
ACTIONS_BOX = ACTIONS_BOX.replace('SELECT ALL', localizedText.selectAllTpl);
|
|
} else if (localizedText.selectAll) {
|
|
ACTIONS_BOX = ACTIONS_BOX.replace('SELECT ALL', localizedText.selectAll);
|
|
}
|
|
|
|
if (localizedText.deselectAllTpl) {
|
|
ACTIONS_BOX = ACTIONS_BOX.replace('DESELECT ALL', localizedText.deselectAllTpl);
|
|
} else if (localizedText.selectAll) {
|
|
ACTIONS_BOX = ACTIONS_BOX.replace('DESELECT ALL', localizedText.deselectAll);
|
|
}
|
|
|
|
actionsBox = jqLite(ACTIONS_BOX);
|
|
dropdownContainer.append(actionsBox);
|
|
}
|
|
|
|
// set default none selection text
|
|
jqLite(dropdownToggle[0].querySelector('.special-title')).append(getDefaultNoneSelectionContent());
|
|
|
|
dropdownContainer.append(dropdownMenu);
|
|
|
|
tElement.append(dropdownToggle);
|
|
tElement.append(dropdownContainer);
|
|
|
|
return function nyaBsSelectLink ($scope, $element, $attrs, ctrls) {
|
|
|
|
var ngCtrl = ctrls[0],
|
|
nyaBsSelectCtrl = ctrls[1],
|
|
liHeight,
|
|
isDisabled = false,
|
|
previousTabIndex,
|
|
valueExpFn,
|
|
valueExpGetter = $parse(nyaBsSelectCtrl.valueExp),
|
|
isMultiple = typeof $attrs.multiple !== 'undefined';
|
|
|
|
// find element from current $element root. because the compiled element may be detached from DOM tree by ng-if or ng-switch.
|
|
var dropdownToggle = jqLite($element[0].querySelector('.dropdown-toggle')),
|
|
dropdownContainer = dropdownToggle.next(),
|
|
dropdownMenu = jqLite(dropdownContainer[0].querySelector('.dropdown-menu.inner')),
|
|
searchBox = jqLite(dropdownContainer[0].querySelector('.bs-searchbox')),
|
|
noSearchResult = jqLite(dropdownMenu[0].querySelector('.no-search-result')),
|
|
actionsBox = jqLite(dropdownContainer[0].querySelector('.bs-actionsbox'));
|
|
|
|
if(nyaBsSelectCtrl.valueExp) {
|
|
valueExpFn = function(scope, locals) {
|
|
return valueExpGetter(scope, locals);
|
|
};
|
|
}
|
|
|
|
// for debug
|
|
nyaBsSelectCtrl.setId($element.attr('id'));
|
|
|
|
if (isMultiple) {
|
|
nyaBsSelectCtrl.isMultiple = true;
|
|
|
|
// required validator
|
|
ngCtrl.$isEmpty = function(value) {
|
|
return !value || value.length === 0;
|
|
};
|
|
}
|
|
if(typeof $attrs.disabled !== 'undefined') {
|
|
$scope.$watch($attrs.disabled, function(disabled){
|
|
if(disabled) {
|
|
dropdownToggle.addClass('disabled');
|
|
dropdownToggle.attr('disabled', 'disabled');
|
|
previousTabIndex = dropdownToggle.attr('tabindex');
|
|
dropdownToggle.attr('tabindex', '-1');
|
|
isDisabled = true;
|
|
} else {
|
|
dropdownToggle.removeClass('disabled');
|
|
dropdownToggle.removeAttr('disabled');
|
|
if(previousTabIndex) {
|
|
dropdownToggle.attr('tabindex', previousTabIndex);
|
|
} else {
|
|
dropdownToggle.removeAttr('tabindex');
|
|
}
|
|
isDisabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Do some check on modelValue. remove no existing value
|
|
* @param values
|
|
* @param deepWatched
|
|
*/
|
|
nyaBsSelectCtrl.onCollectionChange = function (values, deepWatched) {
|
|
var valuesForSelect = [],
|
|
index,
|
|
modelValueChanged = false,
|
|
// Due to ngModelController compare reference with the old modelValue, we must set an new array instead of modifying the old one.
|
|
// See: https://github.com/angular/angular.js/issues/1751
|
|
modelValue = deepCopy(ngCtrl.$modelValue);
|
|
|
|
if(!modelValue) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Behavior change, since 2.1.0, we don't want to reset model to null or empty array when options' collection is not prepared.
|
|
*/
|
|
if(Array.isArray(values) && values.length > 0) {
|
|
if(valueExpFn) {
|
|
for(index = 0; index < values.length; index++) {
|
|
valuesForSelect.push(valueExpFn($scope, values[index]));
|
|
}
|
|
} else {
|
|
for(index = 0; index < values.length; index++) {
|
|
if(nyaBsSelectCtrl.valueIdentifier) {
|
|
valuesForSelect.push(values[index][nyaBsSelectCtrl.valueIdentifier]);
|
|
} else if(nyaBsSelectCtrl.keyIdentifier) {
|
|
valuesForSelect.push(values[index][nyaBsSelectCtrl.keyIdentifier]);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if(isMultiple) {
|
|
for(index = 0; index < modelValue.length; index++) {
|
|
if(!contains(valuesForSelect, modelValue[index])) {
|
|
modelValueChanged = true;
|
|
modelValue.splice(index, 1);
|
|
index--;
|
|
}
|
|
}
|
|
|
|
if(modelValueChanged) {
|
|
// modelValue changed.
|
|
|
|
ngCtrl.$setViewValue(modelValue);
|
|
|
|
updateButtonContent();
|
|
}
|
|
|
|
} else {
|
|
if(!contains(valuesForSelect, modelValue)) {
|
|
modelValue = valuesForSelect[0];
|
|
|
|
ngCtrl.$setViewValue(modelValue);
|
|
|
|
updateButtonContent();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* if we set deep-watch="true" on nyaBsOption directive,
|
|
* we need to refresh dropdown button content whenever a change happened in collection.
|
|
*/
|
|
if(deepWatched) {
|
|
|
|
updateButtonContent();
|
|
}
|
|
|
|
};
|
|
|
|
// view --> model
|
|
|
|
dropdownMenu.on('click', function menuEventHandler (event) {
|
|
if(isDisabled) {
|
|
return;
|
|
}
|
|
|
|
if(jqLite(event.target).hasClass('dropdown-header')) {
|
|
return;
|
|
}
|
|
var nyaBsOptionNode = filterTarget(event.target, dropdownMenu[0], 'nya-bs-option'),
|
|
nyaBsOption;
|
|
|
|
if(nyaBsOptionNode !== null) {
|
|
nyaBsOption = jqLite(nyaBsOptionNode);
|
|
if(nyaBsOption.hasClass('disabled')) {
|
|
return;
|
|
}
|
|
selectOption(nyaBsOption);
|
|
}
|
|
});
|
|
|
|
// if click the outside of dropdown menu, close the dropdown menu
|
|
var outClick = function(event) {
|
|
if(filterTarget(event.target, $element.parent()[0], $element[0]) === null) {
|
|
if($element.hasClass('open')) {
|
|
$element.triggerHandler('blur');
|
|
}
|
|
$element.removeClass('open');
|
|
}
|
|
};
|
|
$document.on('click', outClick);
|
|
|
|
|
|
|
|
dropdownToggle.on('blur', function() {
|
|
if(!$element.hasClass('open')) {
|
|
$element.triggerHandler('blur');
|
|
}
|
|
});
|
|
dropdownToggle.on('click', function() {
|
|
var nyaBsOptionNode;
|
|
$element.toggleClass('open');
|
|
if($element.hasClass('open') && typeof liHeight === 'undefined') {
|
|
calcMenuSize();
|
|
}
|
|
if($attrs.liveSearch === 'true' && $element.hasClass('open')) {
|
|
searchBox.children().eq(0)[0].focus();
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
dropdownMenu.children().removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
}
|
|
} else if($element.hasClass('open')) {
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
}
|
|
}
|
|
});
|
|
|
|
// actions box
|
|
if ($attrs.actionsBox === 'true' && isMultiple) {
|
|
actionsBox.find('button').eq(0).on('click', function () {
|
|
setAllOptions(true);
|
|
});
|
|
actionsBox.find('button').eq(1).on('click', function () {
|
|
setAllOptions(false);
|
|
});
|
|
}
|
|
|
|
|
|
// live search
|
|
if($attrs.liveSearch === 'true') {
|
|
searchBox.children().on('input', function(){
|
|
|
|
var searchKeyword = searchBox.children().val(),
|
|
found = 0,
|
|
options = dropdownMenu.children(),
|
|
length = options.length,
|
|
index,
|
|
option,
|
|
nyaBsOptionNode;
|
|
|
|
if(searchKeyword) {
|
|
for(index = 0; index < length; index++) {
|
|
option = options.eq(index);
|
|
if(option.hasClass('nya-bs-option')) {
|
|
if(!hasKeyword(option.find('a'), searchKeyword)) {
|
|
option.addClass('not-match');
|
|
} else {
|
|
option.removeClass('not-match');
|
|
found++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(found === 0) {
|
|
noSearchResult.addClass('show');
|
|
} else {
|
|
noSearchResult.removeClass('show');
|
|
}
|
|
} else {
|
|
for(index = 0; index < length; index++) {
|
|
option = options.eq(index);
|
|
if(option.hasClass('nya-bs-option')) {
|
|
option.removeClass('not-match');
|
|
}
|
|
}
|
|
noSearchResult.removeClass('show');
|
|
}
|
|
|
|
nyaBsOptionNode = findFocus(true);
|
|
|
|
if(nyaBsOptionNode) {
|
|
options.removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
|
|
// model --> view
|
|
|
|
ngCtrl.$render = function() {
|
|
var modelValue = ngCtrl.$modelValue,
|
|
index,
|
|
bsOptionElements = dropdownMenu.children(),
|
|
length = bsOptionElements.length,
|
|
value;
|
|
if(typeof modelValue === 'undefined') {
|
|
// if modelValue is undefined. uncheck all option
|
|
for(index = 0; index < length; index++) {
|
|
if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
|
|
bsOptionElements.eq(index).removeClass('selected');
|
|
}
|
|
}
|
|
} else {
|
|
for(index = 0; index < length; index++) {
|
|
if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
|
|
|
|
value = getOptionValue(bsOptionElements.eq(index));
|
|
if(isMultiple) {
|
|
if(contains(modelValue, value)) {
|
|
bsOptionElements.eq(index).addClass('selected');
|
|
} else {
|
|
bsOptionElements.eq(index).removeClass('selected');
|
|
}
|
|
} else {
|
|
if(deepEquals(modelValue, value)) {
|
|
bsOptionElements.eq(index).addClass('selected');
|
|
} else {
|
|
bsOptionElements.eq(index).removeClass('selected');
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
//console.log(nyaBsSelectCtrl.id + ' render end');
|
|
updateButtonContent();
|
|
};
|
|
|
|
// simple keyboard support
|
|
$element.on('keydown', function(event){
|
|
var keyCode = event.keyCode;
|
|
|
|
if(keyCode !== 27 && keyCode !== 13 && keyCode !== 38 && keyCode !== 40) {
|
|
// we only handle special keys. don't waste time to traverse the dom tree.
|
|
return;
|
|
}
|
|
|
|
// prevent a click event to be fired.
|
|
event.preventDefault();
|
|
if(isDisabled) {
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
var toggleButton = filterTarget(event.target, $element[0], dropdownToggle[0]),
|
|
menuContainer,
|
|
searchBoxContainer,
|
|
liElement,
|
|
nyaBsOptionNode;
|
|
|
|
if($attrs.liveSearch === 'true') {
|
|
searchBoxContainer = filterTarget(event.target, $element[0], searchBox[0]);
|
|
} else {
|
|
menuContainer = filterTarget(event.target, $element[0], dropdownContainer[0])
|
|
}
|
|
|
|
if(toggleButton) {
|
|
|
|
|
|
// press enter to active dropdown
|
|
if((keyCode === 13 || keyCode === 38 || keyCode === 40) && !$element.hasClass('open')) {
|
|
|
|
event.stopPropagation();
|
|
|
|
$element.addClass('open');
|
|
|
|
// calculate menu size
|
|
if(typeof liHeight === 'undefined') {
|
|
calcMenuSize();
|
|
}
|
|
|
|
// if live search enabled. give focus to search box.
|
|
if($attrs.liveSearch === 'true') {
|
|
searchBox.children().eq(0)[0].focus();
|
|
// find the focusable node but we will use active
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
// remove previous active state
|
|
dropdownMenu.children().removeClass('active');
|
|
// set active to first focusable element
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
}
|
|
} else {
|
|
// otherwise, give focus to first menu item.
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// press enter or escape to de-active dropdown
|
|
//if((keyCode === 13 || keyCode === 27) && $element.hasClass('open')) {
|
|
// $element.removeClass('open');
|
|
// event.stopPropagation();
|
|
//}
|
|
} else if(menuContainer) {
|
|
|
|
if(keyCode === 27) {
|
|
// escape pressed
|
|
dropdownToggle[0].focus();
|
|
if($element.hasClass('open')) {
|
|
$element.triggerHandler('blur');
|
|
}
|
|
$element.removeClass('open');
|
|
event.stopPropagation();
|
|
|
|
} else if(keyCode === 38) {
|
|
event.stopPropagation();
|
|
// up arrow key
|
|
nyaBsOptionNode = findNextFocus(event.target.parentNode, 'previousSibling');
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
} else {
|
|
nyaBsOptionNode = findFocus(false);
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
}
|
|
}
|
|
} else if(keyCode === 40) {
|
|
event.stopPropagation();
|
|
// down arrow key
|
|
nyaBsOptionNode = findNextFocus(event.target.parentNode, 'nextSibling');
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
} else {
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
setFocus(nyaBsOptionNode);
|
|
}
|
|
}
|
|
} else if(keyCode === 13) {
|
|
event.stopPropagation();
|
|
// enter pressed
|
|
liElement = jqLite(event.target.parentNode);
|
|
if(liElement.hasClass('nya-bs-option')) {
|
|
selectOption(liElement);
|
|
if(!isMultiple) {
|
|
dropdownToggle[0].focus();
|
|
}
|
|
}
|
|
}
|
|
} else if(searchBoxContainer) {
|
|
if(keyCode === 27) {
|
|
dropdownToggle[0].focus();
|
|
$element.removeClass('open');
|
|
event.stopPropagation();
|
|
} else if(keyCode === 38) {
|
|
// up
|
|
event.stopPropagation();
|
|
|
|
liElement = findActive();
|
|
if(liElement) {
|
|
nyaBsOptionNode = findNextFocus(liElement[0], 'previousSibling');
|
|
if(nyaBsOptionNode) {
|
|
liElement.removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
} else {
|
|
nyaBsOptionNode = findFocus(false);
|
|
if(nyaBsOptionNode) {
|
|
liElement.removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if(keyCode === 40) {
|
|
// down
|
|
event.stopPropagation();
|
|
|
|
liElement = findActive();
|
|
if(liElement) {
|
|
nyaBsOptionNode = findNextFocus(liElement[0], 'nextSibling');
|
|
if(nyaBsOptionNode) {
|
|
liElement.removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
} else {
|
|
nyaBsOptionNode = findFocus(true);
|
|
if(nyaBsOptionNode) {
|
|
liElement.removeClass('active');
|
|
jqLite(nyaBsOptionNode).addClass('active');
|
|
}
|
|
}
|
|
}
|
|
} else if(keyCode === 13) {
|
|
// select an option.
|
|
liElement = findActive();
|
|
if(liElement) {
|
|
selectOption(liElement);
|
|
if(!isMultiple) {
|
|
dropdownToggle[0].focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function findActive() {
|
|
var list = dropdownMenu.children(),
|
|
i, liElement,
|
|
length = list.length;
|
|
for(i = 0; i < length; i++) {
|
|
liElement = list.eq(i);
|
|
if(liElement.hasClass('active') && liElement.hasClass('nya-bs-option') && !liElement.hasClass('not-match')) {
|
|
return liElement;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* setFocus on a nya-bs-option element. it actually set focus on its child anchor element.
|
|
* @param elem a nya-bs-option element.
|
|
*/
|
|
function setFocus(elem) {
|
|
var childList = elem.childNodes,
|
|
length = childList.length,
|
|
child;
|
|
for(var i = 0; i < length; i++) {
|
|
child = childList[i];
|
|
if(child.nodeType === 1 && child.tagName.toLowerCase() === 'a') {
|
|
child.focus();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function findFocus(fromFirst) {
|
|
var firstLiElement;
|
|
if(fromFirst) {
|
|
firstLiElement = dropdownMenu.children().eq(0);
|
|
} else {
|
|
firstLiElement = dropdownMenu.children().eq(dropdownMenu.children().length - 1);
|
|
}
|
|
|
|
// focus on selected element
|
|
for(var i = 0; i < dropdownMenu.children().length; i++) {
|
|
var childElement = dropdownMenu.children().eq(i);
|
|
if (!childElement.hasClass('not-match') && childElement.hasClass('selected')) {
|
|
return dropdownMenu.children().eq(i)[0];
|
|
}
|
|
}
|
|
|
|
if(firstLiElement.hasClass('nya-bs-option') && !firstLiElement.hasClass('disabled') && !firstLiElement.hasClass('not-match')) {
|
|
return firstLiElement[0];
|
|
} else {
|
|
if(fromFirst) {
|
|
return findNextFocus(firstLiElement[0], 'nextSibling');
|
|
} else {
|
|
return findNextFocus(firstLiElement[0], 'previousSibling');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* find next focusable element on direction
|
|
* @param from the element traversed from
|
|
* @param direction can be 'nextSibling' or 'previousSibling'
|
|
* @returns the element if found, otherwise return null.
|
|
*/
|
|
function findNextFocus(from, direction) {
|
|
if(from && !hasClass(from, 'nya-bs-option')) {
|
|
return;
|
|
}
|
|
var next = from;
|
|
while ((next = sibling(next, direction)) && next.nodeType) {
|
|
if(hasClass(next,'nya-bs-option') && !hasClass(next, 'disabled') && !hasClass(next, 'not-match')) {
|
|
return next
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function setAllOptions(selectAll) {
|
|
if (!isMultiple || isDisabled)
|
|
return;
|
|
|
|
var liElements,
|
|
wv,
|
|
viewValue;
|
|
|
|
liElements = dropdownMenu[0].querySelectorAll('.nya-bs-option');
|
|
if (liElements.length > 0) {
|
|
wv = ngCtrl.$viewValue;
|
|
|
|
// make a deep copy enforce ngModelController to call its $render method.
|
|
// See: https://github.com/angular/angular.js/issues/1751
|
|
viewValue = Array.isArray(wv) ? deepCopy(wv) : [];
|
|
|
|
for (var i = 0; i < liElements.length; i++) {
|
|
var nyaBsOption = jqLite(liElements[i]);
|
|
if (nyaBsOption.hasClass('disabled'))
|
|
continue;
|
|
|
|
var value, index;
|
|
|
|
// if user specify the value attribute. we should use the value attribute
|
|
// otherwise, use the valueIdentifier specified field in target scope
|
|
value = getOptionValue(nyaBsOption);
|
|
|
|
if (typeof value !== 'undefined') {
|
|
index = indexOf(viewValue, value);
|
|
if (selectAll && index == -1) {
|
|
// check element
|
|
viewValue.push(value);
|
|
nyaBsOption.addClass('selected');
|
|
} else if (!selectAll && index != -1) {
|
|
// uncheck element
|
|
viewValue.splice(index, 1);
|
|
nyaBsOption.removeClass('selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
// update view value regardless
|
|
ngCtrl.$setViewValue(viewValue);
|
|
$scope.$digest();
|
|
|
|
updateButtonContent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* select an option represented by nyaBsOption argument. Get the option's value and update model.
|
|
* if isMultiple = true, doesn't close dropdown menu. otherwise close the menu.
|
|
* @param nyaBsOption the jqLite wrapped `nya-bs-option` element.
|
|
*/
|
|
function selectOption(nyaBsOption) {
|
|
var value,
|
|
viewValue,
|
|
wv = ngCtrl.$viewValue,
|
|
index;
|
|
// if user specify the value attribute. we should use the value attribute
|
|
// otherwise, use the valueIdentifier specified field in target scope
|
|
|
|
value = getOptionValue(nyaBsOption);
|
|
|
|
if(typeof value !== 'undefined') {
|
|
if(isMultiple) {
|
|
// make a deep copy enforce ngModelController to call its $render method.
|
|
// See: https://github.com/angular/angular.js/issues/1751
|
|
viewValue = Array.isArray(wv) ? deepCopy(wv) : [];
|
|
index = indexOf(viewValue, value);
|
|
if(index === -1) {
|
|
// check element
|
|
viewValue.push(value);
|
|
nyaBsOption.addClass('selected');
|
|
|
|
} else {
|
|
// uncheck element
|
|
viewValue.splice(index, 1);
|
|
nyaBsOption.removeClass('selected');
|
|
|
|
}
|
|
|
|
} else {
|
|
dropdownMenu.children().removeClass('selected');
|
|
viewValue = value;
|
|
nyaBsOption.addClass('selected');
|
|
|
|
}
|
|
}
|
|
// update view value regardless
|
|
ngCtrl.$setViewValue(viewValue);
|
|
$scope.$digest();
|
|
|
|
if(!isMultiple) {
|
|
// in single selection mode. close the dropdown menu
|
|
if($element.hasClass('open')) {
|
|
$element.triggerHandler('blur');
|
|
}
|
|
$element.removeClass('open');
|
|
dropdownToggle[0].focus();
|
|
}
|
|
updateButtonContent();
|
|
}
|
|
|
|
/**
|
|
* get a value of current nyaBsOption. according to different setting.
|
|
* - if `nya-bs-option` directive is used to populate options and a `value` attribute is specified. use expression of the attribute value.
|
|
* - 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.
|
|
* - if `nya-bs-option` class is used on static options. use literal value of the `value` attribute.
|
|
* @param nyaBsOption a jqLite wrapped `nya-bs-option` element
|
|
*/
|
|
function getOptionValue(nyaBsOption) {
|
|
var scopeOfOption;
|
|
if(valueExpFn) {
|
|
// here we use the scope bound by ourselves in the nya-bs-option.
|
|
scopeOfOption = nyaBsOption.data('isolateScope');
|
|
return valueExpFn(scopeOfOption);
|
|
} else {
|
|
if(nyaBsSelectCtrl.valueIdentifier || nyaBsSelectCtrl.keyIdentifier) {
|
|
scopeOfOption = nyaBsOption.data('isolateScope');
|
|
return scopeOfOption[nyaBsSelectCtrl.valueIdentifier] || scopeOfOption[nyaBsSelectCtrl.keyIdentifier];
|
|
} else {
|
|
return nyaBsOption.attr('data-value');
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function getOptionText(nyaBsOption) {
|
|
var item = nyaBsOption.find('a');
|
|
if(item.children().length === 0 || item.children().eq(0).hasClass('check-mark')) {
|
|
// if the first child is check-mark or has no children, means the option text is text node
|
|
return item[0].firstChild.cloneNode(false);
|
|
} else {
|
|
// otherwise we clone the first element of the item
|
|
return item.children().eq(0)[0].cloneNode(true);
|
|
}
|
|
}
|
|
|
|
function updateButtonContent() {
|
|
var viewValue = ngCtrl.$viewValue;
|
|
$element.triggerHandler('change');
|
|
|
|
var filterOption = jqLite(dropdownToggle[0].querySelector('.filter-option'));
|
|
var specialTitle = jqLite(dropdownToggle[0].querySelector('.special-title'));
|
|
if(typeof viewValue === 'undefined') {
|
|
/**
|
|
* Select empty option when model is undefined.
|
|
*/
|
|
dropdownToggle.addClass('show-special-title');
|
|
filterOption.empty();
|
|
return;
|
|
}
|
|
if(isMultiple && viewValue.length === 0) {
|
|
dropdownToggle.addClass('show-special-title');
|
|
filterOption.empty();
|
|
} else {
|
|
dropdownToggle.removeClass('show-special-title');
|
|
$timeout(function() {
|
|
|
|
var bsOptionElements = dropdownMenu.children(),
|
|
value,
|
|
nyaBsOption,
|
|
index,
|
|
length = bsOptionElements.length,
|
|
optionTitle,
|
|
selection = [],
|
|
match,
|
|
count;
|
|
|
|
if(isMultiple && $attrs.selectedTextFormat === 'count') {
|
|
count = 1;
|
|
} else if(isMultiple && $attrs.selectedTextFormat && (match = $attrs.selectedTextFormat.match(/\s*count\s*>\s*(\d+)\s*/))) {
|
|
count = parseInt(match[1], 10);
|
|
}
|
|
|
|
// data-selected-text-format="count" or data-selected-text-format="count>x"
|
|
if((typeof count !== 'undefined') && viewValue.length > count) {
|
|
filterOption.empty();
|
|
if(localizedText.numberItemSelectedTpl) {
|
|
filterOption.append(jqLite(localizedText.numberItemSelectedTpl.replace('%d', viewValue.length)));
|
|
} else if(localizedText.numberItemSelected) {
|
|
filterOption.append(document.createTextNode(localizedText.numberItemSelected.replace('%d', viewValue.length)));
|
|
} else {
|
|
filterOption.append(document.createTextNode(viewValue.length + ' items selected'));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// data-selected-text-format="values" or the number of selected items is less than count
|
|
for(index = 0; index < length; index++) {
|
|
nyaBsOption = bsOptionElements.eq(index);
|
|
if(nyaBsOption.hasClass('nya-bs-option')) {
|
|
|
|
value = getOptionValue(nyaBsOption);
|
|
|
|
if(isMultiple) {
|
|
if(Array.isArray(viewValue) && contains(viewValue, value)) {
|
|
// if option has an title attribute. use the title value as content show in button.
|
|
// otherwise get very first child element.
|
|
optionTitle = nyaBsOption.attr('title');
|
|
if(optionTitle) {
|
|
selection.push(document.createTextNode(optionTitle));
|
|
} else {
|
|
selection.push(getOptionText(nyaBsOption));
|
|
}
|
|
|
|
}
|
|
} else {
|
|
if(deepEquals(viewValue, value)) {
|
|
optionTitle = nyaBsOption.attr('title');
|
|
if(optionTitle) {
|
|
selection.push(document.createTextNode(optionTitle));
|
|
} else {
|
|
selection.push(getOptionText(nyaBsOption));
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if(selection.length === 0) {
|
|
filterOption.empty();
|
|
dropdownToggle.addClass('show-special-title');
|
|
} else if(selection.length === 1) {
|
|
dropdownToggle.removeClass('show-special-title');
|
|
// either single or multiple selection will show the only selected content.
|
|
filterOption.empty();
|
|
filterOption.append(selection[0]);
|
|
} else {
|
|
dropdownToggle.removeClass('show-special-title');
|
|
filterOption.empty();
|
|
for(index = 0; index < selection.length; index++) {
|
|
filterOption.append(selection[index]);
|
|
if(index < selection.length -1) {
|
|
filterOption.append(document.createTextNode(', '));
|
|
}
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
// will called only once.
|
|
function calcMenuSize(){
|
|
|
|
var liElements = dropdownMenu.find('li'),
|
|
length = liElements.length,
|
|
liElement,
|
|
i;
|
|
for(i = 0; i < length; i++) {
|
|
liElement = liElements.eq(i);
|
|
if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
|
|
liHeight = liElement[0].clientHeight;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(/\d+/.test($attrs.size)) {
|
|
var dropdownSize = parseInt($attrs.size, 10);
|
|
dropdownMenu.css('max-height', (dropdownSize * liHeight) + 'px');
|
|
dropdownMenu.css('overflow-y', 'auto');
|
|
}
|
|
|
|
}
|
|
|
|
$scope.$on('$destroy', function() {
|
|
dropdownMenu.off();
|
|
dropdownToggle.off();
|
|
if (searchBox.off) searchBox.off();
|
|
$document.off('click', outClick);
|
|
|
|
});
|
|
|
|
};
|
|
}
|
|
};
|
|
}]);
|
|
|
|
nyaBsSelect.directive('nyaBsOption', ['$parse', function($parse){
|
|
|
|
//00000011111111111111100000000022222222222222200000003333333333333330000000000000004444444444000000000000000000055555555550000000000000000000006666666666000000
|
|
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*$/;
|
|
|
|
return {
|
|
restrict: 'A',
|
|
transclude: 'element',
|
|
priority: 1000,
|
|
terminal: true,
|
|
require: ['^nyaBsSelect', '^ngModel'],
|
|
compile: function nyaBsOptionCompile (tElement, tAttrs) {
|
|
|
|
var expression = tAttrs.nyaBsOption;
|
|
var nyaBsOptionEndComment = document.createComment(' end nyaBsOption: ' + expression + ' ');
|
|
var match = expression.match(BS_OPTION_REGEX);
|
|
|
|
if(!match) {
|
|
throw new Error('invalid expression');
|
|
}
|
|
|
|
// we want to keep our expression comprehensible so we don't use 'select as label for value in collection' expression.
|
|
var valueExp = tAttrs.value,
|
|
valueExpGetter = valueExp ? $parse(valueExp) : null;
|
|
|
|
var valueIdentifier = match[3] || match[1],
|
|
keyIdentifier = match[2],
|
|
collectionExp = match[4],
|
|
groupByExpGetter = match[5] ? $parse(match[5]) : null,
|
|
trackByExp = match[6];
|
|
|
|
var trackByIdArrayFn,
|
|
trackByIdObjFn,
|
|
trackByIdExpFn,
|
|
trackByExpGetter;
|
|
var hashFnLocals = {$id: hashKey};
|
|
var groupByFn, locals = {};
|
|
|
|
if(trackByExp) {
|
|
trackByExpGetter = $parse(trackByExp);
|
|
} else {
|
|
trackByIdArrayFn = function(key, value) {
|
|
return hashKey(value);
|
|
};
|
|
trackByIdObjFn = function(key) {
|
|
return key;
|
|
};
|
|
}
|
|
return function nyaBsOptionLink($scope, $element, $attr, ctrls, $transclude) {
|
|
|
|
var nyaBsSelectCtrl = ctrls[0],
|
|
ngCtrl = ctrls[1],
|
|
valueExpFn,
|
|
deepWatched,
|
|
valueExpLocals = {};
|
|
|
|
if(trackByExpGetter) {
|
|
trackByIdExpFn = function(key, value, index) {
|
|
// assign key, value, and $index to the locals so that they can be used in hash functions
|
|
if (keyIdentifier) {
|
|
hashFnLocals[keyIdentifier] = key;
|
|
}
|
|
hashFnLocals[valueIdentifier] = value;
|
|
hashFnLocals.$index = index;
|
|
return trackByExpGetter($scope, hashFnLocals);
|
|
};
|
|
}
|
|
|
|
if(groupByExpGetter) {
|
|
groupByFn = function(key, value) {
|
|
if(keyIdentifier) {
|
|
locals[keyIdentifier] = key;
|
|
}
|
|
locals[valueIdentifier] = value;
|
|
return groupByExpGetter($scope, locals);
|
|
}
|
|
}
|
|
|
|
// set keyIdentifier and valueIdentifier property of nyaBsSelectCtrl
|
|
if(keyIdentifier) {
|
|
nyaBsSelectCtrl.keyIdentifier = keyIdentifier;
|
|
}
|
|
if(valueIdentifier) {
|
|
nyaBsSelectCtrl.valueIdentifier = valueIdentifier;
|
|
}
|
|
|
|
if(valueExpGetter) {
|
|
nyaBsSelectCtrl.valueExp = valueExp;
|
|
valueExpFn = function(key, value) {
|
|
if(keyIdentifier) {
|
|
valueExpLocals[keyIdentifier] = key;
|
|
}
|
|
valueExpLocals[valueIdentifier] = value;
|
|
return valueExpGetter($scope, valueExpLocals);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// Store a list of elements from previous run. This is a hash where key is the item from the
|
|
// iterator, and the value is objects with following properties.
|
|
// - scope: bound scope
|
|
// - element: previous element.
|
|
// - index: position
|
|
//
|
|
// We are using no-proto object so that we don't need to guard against inherited props via
|
|
// hasOwnProperty.
|
|
var lastBlockMap = createMap();
|
|
|
|
// deepWatch will impact performance. use with caution.
|
|
if($attr.deepWatch === 'true') {
|
|
deepWatched = true;
|
|
$scope.$watch(collectionExp, nyaBsOptionAction, true);
|
|
} else {
|
|
deepWatched = false;
|
|
$scope.$watchCollection(collectionExp, nyaBsOptionAction);
|
|
}
|
|
|
|
function nyaBsOptionAction(collection) {
|
|
var index,
|
|
|
|
previousNode = $element[0], // node that cloned nodes should be inserted after
|
|
// initialized to the comment node anchor
|
|
|
|
key, value,
|
|
trackById,
|
|
trackByIdFn,
|
|
collectionKeys,
|
|
collectionLength,
|
|
// Same as lastBlockMap but it has the current state. It will become the
|
|
// lastBlockMap on the next iteration.
|
|
nextBlockMap = createMap(),
|
|
nextBlockOrder,
|
|
block,
|
|
groupName,
|
|
nextNode,
|
|
group,
|
|
lastGroup,
|
|
|
|
removedClone, // removed clone node, should also remove isolateScope data as well
|
|
|
|
values = [],
|
|
valueObj; // the collection value
|
|
|
|
if(groupByFn) {
|
|
group = [];
|
|
}
|
|
|
|
if(isArrayLike(collection)) {
|
|
collectionKeys = collection;
|
|
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
|
|
} else {
|
|
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
|
|
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
|
collectionKeys = [];
|
|
for (var itemKey in collection) {
|
|
if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') {
|
|
collectionKeys.push(itemKey);
|
|
}
|
|
}
|
|
collectionKeys.sort();
|
|
}
|
|
collectionLength = collectionKeys.length;
|
|
nextBlockOrder = new Array(collectionLength);
|
|
|
|
for(index = 0; index < collectionLength; index++) {
|
|
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
|
value = collection[key];
|
|
trackById = trackByIdFn(key, value, index);
|
|
|
|
// copy the value with scope like structure to notify the select directive.
|
|
valueObj = {};
|
|
if(keyIdentifier) {
|
|
valueObj[keyIdentifier] = key;
|
|
}
|
|
|
|
valueObj[valueIdentifier] = value;
|
|
values.push(valueObj);
|
|
|
|
if(groupByFn) {
|
|
groupName = groupByFn(key, value);
|
|
if(group.indexOf(groupName) === -1 && groupName) {
|
|
group.push(groupName);
|
|
}
|
|
}
|
|
|
|
if(lastBlockMap[trackById]) {
|
|
// found previously seen block
|
|
block = lastBlockMap[trackById];
|
|
delete lastBlockMap[trackById];
|
|
|
|
// must update block here because some data we stored may change.
|
|
if(groupByFn) {
|
|
block.group = groupName;
|
|
}
|
|
block.key = key;
|
|
block.value = value;
|
|
|
|
nextBlockMap[trackById] = block;
|
|
nextBlockOrder[index] = block;
|
|
} else if(nextBlockMap[trackById]) {
|
|
//if collision detected. restore lastBlockMap and throw an error
|
|
nextBlockOrder.forEach(function(block) {
|
|
if(block && block.scope) {
|
|
lastBlockMap[block.id] = block;
|
|
}
|
|
});
|
|
throw new Error("Duplicates in a select are not allowed. Use 'track by' expression to specify unique keys.");
|
|
} else {
|
|
// new never before seen block
|
|
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined, key: key, value: value};
|
|
nextBlockMap[trackById] = true;
|
|
if(groupName) {
|
|
nextBlockOrder[index].group = groupName;
|
|
}
|
|
}
|
|
}
|
|
|
|
// only resort nextBlockOrder when group found
|
|
if(group && group.length > 0) {
|
|
|
|
nextBlockOrder = sortByGroup(nextBlockOrder, group, 'group');
|
|
}
|
|
|
|
// remove DOM nodes
|
|
for( var blockKey in lastBlockMap) {
|
|
block = lastBlockMap[blockKey];
|
|
removedClone = getBlockNodes(block.clone);
|
|
// remove the isolateScope data to detach scope from this clone
|
|
removedClone.removeData('isolateScope');
|
|
removedClone.remove();
|
|
block.scope.$destroy();
|
|
}
|
|
|
|
for(index = 0; index < collectionLength; index++) {
|
|
block = nextBlockOrder[index];
|
|
if(block.scope) {
|
|
// if we have already seen this object, then we need to reuse the
|
|
// associated scope/element
|
|
|
|
nextNode = previousNode;
|
|
if(getBlockStart(block) != nextNode) {
|
|
jqLite(previousNode).after(block.clone);
|
|
}
|
|
previousNode = getBlockEnd(block);
|
|
|
|
updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
|
|
} else {
|
|
$transclude(function nyaBsOptionTransclude(clone, scope) {
|
|
// in case of the debugInfoEnable is set to false, we have to bind the scope to the clone node.
|
|
setElementIsolateScope(clone, scope);
|
|
|
|
block.scope = scope;
|
|
|
|
var endNode = nyaBsOptionEndComment.cloneNode(false);
|
|
clone[clone.length++] = endNode;
|
|
|
|
jqLite(previousNode).after(clone);
|
|
|
|
// add nya-bs-option class
|
|
clone.addClass('nya-bs-option');
|
|
|
|
// for newly created item we need to ensure its selected status from the model value.
|
|
if(valueExpFn) {
|
|
value = valueExpFn(block.key, block.value);
|
|
} else {
|
|
value = block.value || block.key;
|
|
}
|
|
|
|
if(nyaBsSelectCtrl.isMultiple) {
|
|
if(Array.isArray(ngCtrl.$modelValue) && contains(ngCtrl.$modelValue, value)) {
|
|
clone.addClass('selected');
|
|
}
|
|
} else {
|
|
if(deepEquals(value, ngCtrl.$modelValue)) {
|
|
clone.addClass('selected');
|
|
}
|
|
}
|
|
|
|
previousNode = endNode;
|
|
// Note: We only need the first/last node of the cloned nodes.
|
|
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
|
|
// by a directive with templateUrl when its template arrives.
|
|
block.clone = clone;
|
|
nextBlockMap[block.id] = block;
|
|
updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
|
|
});
|
|
|
|
}
|
|
|
|
// we need to mark the first item of a group
|
|
if(group) {
|
|
if(!lastGroup || lastGroup !== block.group) {
|
|
block.clone.addClass('first-in-group');
|
|
} else {
|
|
block.clone.removeClass('first-in-group');
|
|
}
|
|
|
|
lastGroup = block.group;
|
|
|
|
// add special class for indent
|
|
block.clone.addClass('group-item');
|
|
}
|
|
}
|
|
|
|
lastBlockMap = nextBlockMap;
|
|
|
|
nyaBsSelectCtrl.onCollectionChange(values, deepWatched);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}]);
|
|
|
|
|
|
})(); |