mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
607 lines
24 KiB
JavaScript
607 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ng.directive:select
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* HTML `SELECT` element with angular data-binding.
|
|
*
|
|
* # `ngOptions`
|
|
*
|
|
* Optionally `ngOptions` attribute can be used to dynamically generate a list of `<option>`
|
|
* elements for a `<select>` element using an array or an object obtained by evaluating the
|
|
* `ngOptions` expression.
|
|
*˝˝
|
|
* When an item in the select menu is select, the value of array element or object property
|
|
* represented by the selected option will be bound to the model identified by the `ngModel`
|
|
* directive of the parent select element.
|
|
*
|
|
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
|
|
* be nested into the `<select>` element. This element will then represent `null` or "not selected"
|
|
* option. See example below for demonstration.
|
|
*
|
|
* Note: `ngOptions` provides iterator facility for `<option>` element which should be used instead
|
|
* of {@link ng.directive:ngRepeat ngRepeat} when you want the
|
|
* `select` model to be bound to a non-string value. This is because an option element can currently
|
|
* be bound to string values only.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required The control is considered valid only if value is entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {comprehension_expression=} ngOptions in one of the following forms:
|
|
*
|
|
* * for array data sources:
|
|
* * `label` **`for`** `value` **`in`** `array`
|
|
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
|
|
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
|
|
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
|
|
* * for object data sources:
|
|
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
|
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
|
* * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
|
|
* * `select` **`as`** `label` **`group by`** `group`
|
|
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
|
|
*
|
|
* Where:
|
|
*
|
|
* * `array` / `object`: an expression which evaluates to an array / object to iterate over.
|
|
* * `value`: local variable which will refer to each item in the `array` or each property value
|
|
* of `object` during iteration.
|
|
* * `key`: local variable which will refer to a property name in `object` during iteration.
|
|
* * `label`: The result of this expression will be the label for `<option>` element. The
|
|
* `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
|
|
* * `select`: The result of this expression will be bound to the model of the parent `<select>`
|
|
* element. If not specified, `select` expression will default to `value`.
|
|
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
|
|
* DOM element.
|
|
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
|
|
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
|
|
* `value` variable (e.g. `value.propertyName`).
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function MyCntrl($scope) {
|
|
$scope.colors = [
|
|
{name:'black', shade:'dark'},
|
|
{name:'white', shade:'light'},
|
|
{name:'red', shade:'dark'},
|
|
{name:'blue', shade:'dark'},
|
|
{name:'yellow', shade:'light'}
|
|
];
|
|
$scope.color = $scope.colors[2]; // red
|
|
}
|
|
</script>
|
|
<div ng-controller="MyCntrl">
|
|
<ul>
|
|
<li ng-repeat="color in colors">
|
|
Name: <input ng-model="color.name">
|
|
[<a href ng-click="colors.splice($index, 1)">X</a>]
|
|
</li>
|
|
<li>
|
|
[<a href ng-click="colors.push({})">add</a>]
|
|
</li>
|
|
</ul>
|
|
<hr/>
|
|
Color (null not allowed):
|
|
<select ng-model="color" ng-options="c.name for c in colors"></select><br>
|
|
|
|
Color (null allowed):
|
|
<span class="nullable">
|
|
<select ng-model="color" ng-options="c.name for c in colors">
|
|
<option value="">-- chose color --</option>
|
|
</select>
|
|
</span><br/>
|
|
|
|
Color grouped by shade:
|
|
<select ng-model="color" ng-options="c.name group by c.shade for c in colors">
|
|
</select><br/>
|
|
|
|
|
|
Select <a href ng-click="color={name:'not in list'}">bogus</a>.<br>
|
|
<hr/>
|
|
Currently selected: {{ {selected_color:color} }}
|
|
<div style="border:solid 1px black; height:20px"
|
|
ng-style="{'background-color':color.name}">
|
|
</div>
|
|
</div>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should check ng-options', function() {
|
|
expect(binding('{selected_color:color}')).toMatch('red');
|
|
select('color').option('0');
|
|
expect(binding('{selected_color:color}')).toMatch('black');
|
|
using('.nullable').select('color').option('');
|
|
expect(binding('{selected_color:color}')).toMatch('null');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
|
|
var ngOptionsDirective = valueFn({ terminal: true });
|
|
var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|
//0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000007777000000000000000000088888
|
|
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/,
|
|
nullModelCtrl = {$setViewValue: noop};
|
|
|
|
return {
|
|
restrict: 'E',
|
|
require: ['select', '?ngModel'],
|
|
controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
|
|
var self = this,
|
|
optionsMap = {},
|
|
ngModelCtrl = nullModelCtrl,
|
|
nullOption,
|
|
unknownOption;
|
|
|
|
|
|
self.databound = $attrs.ngModel;
|
|
|
|
|
|
self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
|
|
ngModelCtrl = ngModelCtrl_;
|
|
nullOption = nullOption_;
|
|
unknownOption = unknownOption_;
|
|
}
|
|
|
|
|
|
self.addOption = function(value) {
|
|
optionsMap[value] = true;
|
|
|
|
if (ngModelCtrl.$viewValue == value) {
|
|
$element.val(value);
|
|
if (unknownOption.parent()) unknownOption.remove();
|
|
}
|
|
};
|
|
|
|
|
|
self.removeOption = function(value) {
|
|
if (this.hasOption(value)) {
|
|
delete optionsMap[value];
|
|
if (ngModelCtrl.$viewValue == value) {
|
|
this.renderUnknownOption(value);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
self.renderUnknownOption = function(val) {
|
|
var unknownVal = '? ' + hashKey(val) + ' ?';
|
|
unknownOption.val(unknownVal);
|
|
$element.prepend(unknownOption);
|
|
$element.val(unknownVal);
|
|
unknownOption.prop('selected', true); // needed for IE
|
|
}
|
|
|
|
|
|
self.hasOption = function(value) {
|
|
return optionsMap.hasOwnProperty(value);
|
|
}
|
|
|
|
$scope.$on('$destroy', function() {
|
|
// disable unknown option so that we don't do work when the whole select is being destroyed
|
|
self.renderUnknownOption = noop;
|
|
});
|
|
}],
|
|
|
|
link: function(scope, element, attr, ctrls) {
|
|
// if ngModel is not defined, we don't need to do anything
|
|
if (!ctrls[1]) return;
|
|
|
|
var selectCtrl = ctrls[0],
|
|
ngModelCtrl = ctrls[1],
|
|
multiple = attr.multiple,
|
|
optionsExp = attr.ngOptions,
|
|
nullOption = false, // if false, user will not be able to select it (used by ngOptions)
|
|
emptyOption,
|
|
// we can't just jqLite('<option>') since jqLite is not smart enough
|
|
// to create it in <select> and IE barfs otherwise.
|
|
optionTemplate = jqLite(document.createElement('option')),
|
|
optGroupTemplate =jqLite(document.createElement('optgroup')),
|
|
unknownOption = optionTemplate.clone();
|
|
|
|
// find "null" option
|
|
for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
|
|
if (children[i].value == '') {
|
|
emptyOption = nullOption = children.eq(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
|
|
|
|
// required validator
|
|
if (multiple && (attr.required || attr.ngRequired)) {
|
|
var requiredValidator = function(value) {
|
|
ngModelCtrl.$setValidity('required', !attr.required || (value && value.length));
|
|
return value;
|
|
};
|
|
|
|
ngModelCtrl.$parsers.push(requiredValidator);
|
|
ngModelCtrl.$formatters.unshift(requiredValidator);
|
|
|
|
attr.$observe('required', function() {
|
|
requiredValidator(ngModelCtrl.$viewValue);
|
|
});
|
|
}
|
|
|
|
if (optionsExp) Options(scope, element, ngModelCtrl);
|
|
else if (multiple) Multiple(scope, element, ngModelCtrl);
|
|
else Single(scope, element, ngModelCtrl, selectCtrl);
|
|
|
|
|
|
////////////////////////////
|
|
|
|
|
|
|
|
function Single(scope, selectElement, ngModelCtrl, selectCtrl) {
|
|
ngModelCtrl.$render = function() {
|
|
var viewValue = ngModelCtrl.$viewValue;
|
|
|
|
if (selectCtrl.hasOption(viewValue)) {
|
|
if (unknownOption.parent()) unknownOption.remove();
|
|
selectElement.val(viewValue);
|
|
if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
|
|
} else {
|
|
if (isUndefined(viewValue) && emptyOption) {
|
|
selectElement.val('');
|
|
} else {
|
|
selectCtrl.renderUnknownOption(viewValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
selectElement.bind('change', function() {
|
|
scope.$apply(function() {
|
|
if (unknownOption.parent()) unknownOption.remove();
|
|
ngModelCtrl.$setViewValue(selectElement.val());
|
|
});
|
|
});
|
|
}
|
|
|
|
function Multiple(scope, selectElement, ctrl) {
|
|
var lastView;
|
|
ctrl.$render = function() {
|
|
var items = new HashMap(ctrl.$viewValue);
|
|
forEach(selectElement.find('option'), function(option) {
|
|
option.selected = isDefined(items.get(option.value));
|
|
});
|
|
};
|
|
|
|
// we have to do it on each watch since ngModel watches reference, but
|
|
// we need to work of an array, so we need to see if anything was inserted/removed
|
|
scope.$watch(function selectMultipleWatch() {
|
|
if (!equals(lastView, ctrl.$viewValue)) {
|
|
lastView = copy(ctrl.$viewValue);
|
|
ctrl.$render();
|
|
}
|
|
});
|
|
|
|
selectElement.bind('change', function() {
|
|
scope.$apply(function() {
|
|
var array = [];
|
|
forEach(selectElement.find('option'), function(option) {
|
|
if (option.selected) {
|
|
array.push(option.value);
|
|
}
|
|
});
|
|
ctrl.$setViewValue(array);
|
|
});
|
|
});
|
|
}
|
|
|
|
function Options(scope, selectElement, ctrl) {
|
|
var match;
|
|
|
|
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
|
|
throw ngError(9,
|
|
"ngOptions error! Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}",
|
|
optionsExp, startingTag(selectElement));
|
|
}
|
|
|
|
var displayFn = $parse(match[2] || match[1]),
|
|
valueName = match[4] || match[6],
|
|
keyName = match[5],
|
|
groupByFn = $parse(match[3] || ''),
|
|
valueFn = $parse(match[2] ? match[1] : valueName),
|
|
valuesFn = $parse(match[7]),
|
|
track = match[8],
|
|
trackFn = track ? $parse(match[8]) : null,
|
|
// This is an array of array of existing option groups in DOM. We try to reuse these if possible
|
|
// optionGroupsCache[0] is the options with no option group
|
|
// optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
|
|
optionGroupsCache = [[{element: selectElement, label:''}]];
|
|
|
|
if (nullOption) {
|
|
// compile the element since there might be bindings in it
|
|
$compile(nullOption)(scope);
|
|
|
|
// remove the class, which is added automatically because we recompile the element and it
|
|
// becomes the compilation root
|
|
nullOption.removeClass('ng-scope');
|
|
|
|
// we need to remove it before calling selectElement.html('') because otherwise IE will
|
|
// remove the label from the element. wtf?
|
|
nullOption.remove();
|
|
}
|
|
|
|
// clear contents, we'll add what's needed based on the model
|
|
selectElement.html('');
|
|
|
|
selectElement.bind('change', function() {
|
|
scope.$apply(function() {
|
|
var optionGroup,
|
|
collection = valuesFn(scope) || [],
|
|
locals = {},
|
|
key, value, optionElement, index, groupIndex, length, groupLength;
|
|
|
|
if (multiple) {
|
|
value = [];
|
|
for (groupIndex = 0, groupLength = optionGroupsCache.length;
|
|
groupIndex < groupLength;
|
|
groupIndex++) {
|
|
// list of options for that group. (first item has the parent)
|
|
optionGroup = optionGroupsCache[groupIndex];
|
|
|
|
for(index = 1, length = optionGroup.length; index < length; index++) {
|
|
if ((optionElement = optionGroup[index].element)[0].selected) {
|
|
key = optionElement.val();
|
|
if (keyName) locals[keyName] = key;
|
|
if (trackFn) {
|
|
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
|
|
locals[valueName] = collection[trackIndex];
|
|
if (trackFn(scope, locals) == key) break;
|
|
}
|
|
} else {
|
|
locals[valueName] = collection[key];
|
|
}
|
|
value.push(valueFn(scope, locals));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
key = selectElement.val();
|
|
if (key == '?') {
|
|
value = undefined;
|
|
} else if (key == ''){
|
|
value = null;
|
|
} else {
|
|
if (trackFn) {
|
|
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
|
|
locals[valueName] = collection[trackIndex];
|
|
if (trackFn(scope, locals) == key) {
|
|
value = valueFn(scope, locals);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
locals[valueName] = collection[key];
|
|
if (keyName) locals[keyName] = key;
|
|
value = valueFn(scope, locals);
|
|
}
|
|
}
|
|
}
|
|
ctrl.$setViewValue(value);
|
|
});
|
|
});
|
|
|
|
ctrl.$render = render;
|
|
|
|
// TODO(vojta): can't we optimize this ?
|
|
scope.$watch(render);
|
|
|
|
function render() {
|
|
var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
|
|
optionGroupNames = [''],
|
|
optionGroupName,
|
|
optionGroup,
|
|
option,
|
|
existingParent, existingOptions, existingOption,
|
|
modelValue = ctrl.$modelValue,
|
|
values = valuesFn(scope) || [],
|
|
keys = keyName ? sortedKeys(values) : values,
|
|
groupLength, length,
|
|
groupIndex, index,
|
|
locals = {},
|
|
selected,
|
|
selectedSet = false, // nothing is selected yet
|
|
lastElement,
|
|
element,
|
|
label;
|
|
|
|
if (multiple) {
|
|
if (trackFn && isArray(modelValue)) {
|
|
selectedSet = new HashMap([]);
|
|
for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) {
|
|
locals[valueName] = modelValue[trackIndex];
|
|
selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]);
|
|
}
|
|
} else {
|
|
selectedSet = new HashMap(modelValue);
|
|
}
|
|
}
|
|
|
|
// We now build up the list of options we need (we merge later)
|
|
for (index = 0; length = keys.length, index < length; index++) {
|
|
locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index];
|
|
optionGroupName = groupByFn(scope, locals) || '';
|
|
if (!(optionGroup = optionGroups[optionGroupName])) {
|
|
optionGroup = optionGroups[optionGroupName] = [];
|
|
optionGroupNames.push(optionGroupName);
|
|
}
|
|
if (multiple) {
|
|
selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) != undefined;
|
|
} else {
|
|
if (trackFn) {
|
|
var modelCast = {};
|
|
modelCast[valueName] = modelValue;
|
|
selected = trackFn(scope, modelCast) === trackFn(scope, locals);
|
|
} else {
|
|
selected = modelValue === valueFn(scope, locals);
|
|
}
|
|
selectedSet = selectedSet || selected; // see if at least one item is selected
|
|
}
|
|
label = displayFn(scope, locals); // what will be seen by the user
|
|
label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values
|
|
optionGroup.push({
|
|
id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), // either the index into array or key from object
|
|
label: label,
|
|
selected: selected // determine if we should be selected
|
|
});
|
|
}
|
|
if (!multiple) {
|
|
if (nullOption || modelValue === null) {
|
|
// insert null option if we have a placeholder, or the model is null
|
|
optionGroups[''].unshift({id:'', label:'', selected:!selectedSet});
|
|
} else if (!selectedSet) {
|
|
// option could not be found, we have to insert the undefined item
|
|
optionGroups[''].unshift({id:'?', label:'', selected:true});
|
|
}
|
|
}
|
|
|
|
// Now we need to update the list of DOM nodes to match the optionGroups we computed above
|
|
for (groupIndex = 0, groupLength = optionGroupNames.length;
|
|
groupIndex < groupLength;
|
|
groupIndex++) {
|
|
// current option group name or '' if no group
|
|
optionGroupName = optionGroupNames[groupIndex];
|
|
|
|
// list of options for that group. (first item has the parent)
|
|
optionGroup = optionGroups[optionGroupName];
|
|
|
|
if (optionGroupsCache.length <= groupIndex) {
|
|
// we need to grow the optionGroups
|
|
existingParent = {
|
|
element: optGroupTemplate.clone().attr('label', optionGroupName),
|
|
label: optionGroup.label
|
|
};
|
|
existingOptions = [existingParent];
|
|
optionGroupsCache.push(existingOptions);
|
|
selectElement.append(existingParent.element);
|
|
} else {
|
|
existingOptions = optionGroupsCache[groupIndex];
|
|
existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
|
|
|
|
// update the OPTGROUP label if not the same.
|
|
if (existingParent.label != optionGroupName) {
|
|
existingParent.element.attr('label', existingParent.label = optionGroupName);
|
|
}
|
|
}
|
|
|
|
lastElement = null; // start at the beginning
|
|
for(index = 0, length = optionGroup.length; index < length; index++) {
|
|
option = optionGroup[index];
|
|
if ((existingOption = existingOptions[index+1])) {
|
|
// reuse elements
|
|
lastElement = existingOption.element;
|
|
if (existingOption.label !== option.label) {
|
|
lastElement.text(existingOption.label = option.label);
|
|
}
|
|
if (existingOption.id !== option.id) {
|
|
lastElement.val(existingOption.id = option.id);
|
|
}
|
|
// lastElement.prop('selected') provided by jQuery has side-effects
|
|
if (lastElement[0].selected !== option.selected) {
|
|
lastElement.prop('selected', (existingOption.selected = option.selected));
|
|
}
|
|
} else {
|
|
// grow elements
|
|
|
|
// if it's a null option
|
|
if (option.id === '' && nullOption) {
|
|
// put back the pre-compiled element
|
|
element = nullOption;
|
|
} else {
|
|
// jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
|
|
// in this version of jQuery on some browser the .text() returns a string
|
|
// rather then the element.
|
|
(element = optionTemplate.clone())
|
|
.val(option.id)
|
|
.attr('selected', option.selected)
|
|
.text(option.label);
|
|
}
|
|
|
|
existingOptions.push(existingOption = {
|
|
element: element,
|
|
label: option.label,
|
|
id: option.id,
|
|
selected: option.selected
|
|
});
|
|
if (lastElement) {
|
|
lastElement.after(element);
|
|
} else {
|
|
existingParent.element.append(element);
|
|
}
|
|
lastElement = element;
|
|
}
|
|
}
|
|
// remove any excessive OPTIONs in a group
|
|
index++; // increment since the existingOptions[0] is parent element not OPTION
|
|
while(existingOptions.length > index) {
|
|
existingOptions.pop().element.remove();
|
|
}
|
|
}
|
|
// remove any excessive OPTGROUPs from select
|
|
while(optionGroupsCache.length > groupIndex) {
|
|
optionGroupsCache.pop()[0].element.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
var optionDirective = ['$interpolate', function($interpolate) {
|
|
var nullSelectCtrl = {
|
|
addOption: noop,
|
|
removeOption: noop
|
|
};
|
|
|
|
return {
|
|
restrict: 'E',
|
|
priority: 100,
|
|
compile: function(element, attr) {
|
|
if (isUndefined(attr.value)) {
|
|
var interpolateFn = $interpolate(element.text(), true);
|
|
if (!interpolateFn) {
|
|
attr.$set('value', element.text());
|
|
}
|
|
}
|
|
|
|
return function (scope, element, attr) {
|
|
var selectCtrlName = '$selectController',
|
|
parent = element.parent(),
|
|
selectCtrl = parent.data(selectCtrlName) ||
|
|
parent.parent().data(selectCtrlName); // in case we are in optgroup
|
|
|
|
if (selectCtrl && selectCtrl.databound) {
|
|
// For some reason Opera defaults to true and if not overridden this messes up the repeater.
|
|
// We don't want the view to drive the initialization of the model anyway.
|
|
element.prop('selected', false);
|
|
} else {
|
|
selectCtrl = nullSelectCtrl;
|
|
}
|
|
|
|
if (interpolateFn) {
|
|
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
|
|
attr.$set('value', newVal);
|
|
if (newVal !== oldVal) selectCtrl.removeOption(oldVal);
|
|
selectCtrl.addOption(newVal);
|
|
});
|
|
} else {
|
|
selectCtrl.addOption(attr.value);
|
|
}
|
|
|
|
element.bind('$destroy', function() {
|
|
selectCtrl.removeOption(attr.value);
|
|
});
|
|
};
|
|
}
|
|
}
|
|
}];
|