Skip to content

Commit

Permalink
add select as, new item mode
Browse files Browse the repository at this point in the history
  • Loading branch information
o.istomin committed Jan 21, 2015
1 parent 75e2c39 commit 2ce5b8a
Show file tree
Hide file tree
Showing 11 changed files with 696 additions and 238 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
<a name="0.1.5"></a>
# 0.1.5

## Features

- select `as` support

- **oi-multiselect-options:**
- saveTrigger
- newItem
- newItemModel
- newItemFn

<a name="0.1.4"></a>
# 0.1.4

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
## Features

* API compatible with [Angular select](http://docs.angularjs.org/api/ng/directive/select)
* without jQuery and other dependencies
* Angular 1.2+ without jQuery and other dependencies
* search options by substring (including the search query to the server)
* use [Bootstrap](http://getbootstrap.com) styles (but you can use own styles)
* 16 KB minified
* 17 KB minified

## Demo

Expand Down Expand Up @@ -57,7 +57,7 @@ Use `oi-multiselect` directive:
## Attributes
* `ng-options` — see: [Angular select](http://docs.angularjs.org/api/ng/directive/select)
* `ng-options="item for item in shopArrShort | limitTo: 3"` — filter input list
* `ng-options="item for item in shopArrFn($query)"` — generate input list (expects array/object or promise)
* `ng-options="item for item in shopArrFn($query, $querySelectAs)"` — generate input list (expects array/object or promise)
* `ng-model` — chosen item/items
* `ng-disabled` — specifies that a drop-down list should be disabled
* `multiple` — specifies that multiple options can be selected at once
Expand All @@ -70,4 +70,8 @@ Use `oi-multiselect` directive:
* `searchFilter` — filter name for items in search field
* `dropdownFilter` — filter name for items in dropdown
* `listFilter` — filter name for items order in dropdown
* `saveLastQuery` — function which get `lastQuery` and `removedValue` and return string for input after element was removed (default: '').
* `saveLastQuery` — function which get `lastQuery` and `removedValue` and return string for input after element was removed (default: '')
* `saveTrigger` — Trigger on which element is stored in the model. May be `enter`, `slash`, `tab`, `blur` (default: `enter`, `slash`)
* `newItem` — Mode of adding new items from query (default: false). May be `autocomplete` (priority save matches), `prompt` (priority save new item)
* `newItemModel` — New items model (default: model = query). `$query` value from model will be changed to query string.
* `newItemFn` — function which get query and return new item object or promise
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "https://github.com/tamtakoe"
},
"name": "oi.multiselect",
"version": "0.1.4",
"version": "0.1.5",
"main": ["./dist/multiselect-tpls.js"],
"dependencies": {
"angular": ">=1.2"
Expand Down
251 changes: 175 additions & 76 deletions dist/multiselect-tpls.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/multiselect-tpls.min.js

Large diffs are not rendered by default.

251 changes: 175 additions & 76 deletions dist/multiselect.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/multiselect.min.js

Large diffs are not rendered by default.

186 changes: 113 additions & 73 deletions src/multiselect/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,45 @@ angular.module('oi.multiselect')
throw new Error("Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'");
}

var displayName = match[2] || match[1],
valueName = match[4] || match[6],
groupByName = match[3] || '',
trackByName = match[8] || displayName,
valueMatches = match[7].match(VALUES_REGEXP);

var valuesName = valueMatches[1],
filteredValuesName = valuesName + (valueMatches[3] || ''),
valuesFnName = valuesName + (valueMatches[2] || '');

var displayFn = $parse(displayName),
var selectAsName = / as /.test(match[0]) && match[1], //item.modelValue
displayName = match[2] || match[1], //item.label
valueName = match[4] || match[6], //item
groupByName = match[3] || '', //item.groupName
trackByName = match[8] || displayName, //item.id
valueMatches = match[7].match(VALUES_REGEXP); //collection

var valuesName = valueMatches[1], //collection
filteredValuesName = valuesName + (valueMatches[3] || ''), //collection | filter
valuesFnName = valuesName + (valueMatches[2] || ''); //collection()

var selectAsFn = selectAsName && $parse(selectAsName),
displayFn = $parse(displayName),
groupByFn = $parse(groupByName),
filteredValuesFn = $parse(filteredValuesName),
valuesFn = $parse(valuesFnName),
trackByFn = $parse(trackByName);

var locals = {},
timeoutPromise,
var timeoutPromise,
lastQuery;

var multiple = angular.isDefined(attrs.multiple),
multipleLimit = Number(attrs.multipleLimit),
placeholderFn = $interpolate(attrs.placeholder || ''),
optionsFn = $parse(attrs.oiMultiselectOptions),
keyUpDownWerePressed = false,
matchesWereReset = false;
matchesWereReset = false,
optionsFn = $parse(attrs.oiMultiselectOptions);

return function(scope, element, attrs, ctrl) {
var inputElement = element.find('input'),
listElement = angular.element(element[0].querySelector('.multiselect-dropdown')),
placeholder = placeholderFn(scope),
options = angular.extend({}, oiMultiselect.options, optionsFn(scope)),
options = angular.extend({}, oiMultiselect.options, optionsFn(scope.$parent)),
lastQueryFn = options.saveLastQuery ? $injector.get(options.saveLastQuery) : function() {return ''};

options.newItemModelFn = function (query) {
return (optionsFn({$query: query}) || {}).newItemModel || query;
};

if (angular.isDefined(attrs.autofocus)) {
$timeout(function() {
inputElement[0].focus();
Expand All @@ -68,11 +73,24 @@ angular.module('oi.multiselect')
scope.$parent.$watch(attrs.ngModel, function(value) {
adjustInput();

if (multiple) {
scope.output = value;
} else {
scope.output = value ? [value] : [];
var output = value instanceof Array ? value : value ? [value]: [],
promise = $q.when(output);

if (selectAsFn && value) {
promise = getMatches(null, value)
.then(function(collection) {
return oiUtils.intersection(collection, output, oiUtils.isEqual, selectAs);
});
timeoutPromise = null; //`resetMatches` should not cancel the `promise`
}

promise.then(function(collection) {
scope.output = collection;

if (collection.length !== output.length) {
scope.removeItem(collection.length); //if newItem was not created
}
});
});

scope.$watch('query', function(inputValue, oldValue) {
Expand Down Expand Up @@ -128,17 +146,22 @@ angular.module('oi.multiselect')
scope.addItem = function addItem(option) {
lastQuery = scope.query;

//duplicate
if (oiUtils.intersection(scope.output, [option], null, getLabel, getLabel).length) return;

//limit is reached
if (!isNaN(multipleLimit) && scope.output.length >= multipleLimit) return;

var optionGroup = scope.groups[getGroupName(option)];
var modelOption = selectAsFn ? selectAs(option) : option;

optionGroup.splice(optionGroup.indexOf(option), 1);

if (multiple) {
ctrl.$setViewValue(angular.isArray(ctrl.$modelValue) ? ctrl.$modelValue.concat(option) : [option]);
ctrl.$setViewValue(angular.isArray(ctrl.$modelValue) ? ctrl.$modelValue.concat(modelOption) : [modelOption]);
updateGroupPos();
} else {
ctrl.$setViewValue(option);
ctrl.$setViewValue(modelOption);
resetMatches();
}

Expand Down Expand Up @@ -185,17 +208,45 @@ angular.module('oi.multiselect')
}
};

function saveOn(triggerName) {
var isTriggered = (new RegExp(triggerName)).test(options.saveTrigger),
isNewItem = options.newItem && scope.query,
isSelectedItem = angular.isNumber(scope.selectorPosition),
selectedOrder = scope.order[scope.selectorPosition],
newItemFn = options.newItemFn || options.newItemModelFn,
itemPromise = $q.reject();

if (isTriggered && (isNewItem || isSelectedItem && selectedOrder)) {
scope.showLoader = true;
itemPromise = $q.when(selectedOrder || newItemFn(scope.query));
}

itemPromise
.then(scope.addItem)
.finally(function() {
var bottom = scope.order.length - 1;

if (scope.selectorPosition === bottom) {
setOption(listElement, 0); //TODO optimise when list will be closed
}
options.newItemFn && !isSelectedItem || $timeout(angular.noop); //TODO $applyAsync work since Angular 1.3
resetMatches();
});
}

scope.keyParser = function keyParser(event) {
var top = 0,
bottom = scope.order.length - 1;

switch (event.keyCode) {
case 38: /* up */
scope.selectorPosition = angular.isNumber(scope.selectorPosition) ? scope.selectorPosition : top;
setOption(listElement, scope.selectorPosition === top ? bottom : scope.selectorPosition - 1);
keyUpDownWerePressed = true;
break;

case 40: /* down */
scope.selectorPosition = angular.isNumber(scope.selectorPosition) ? scope.selectorPosition : top - 1;
setOption(listElement, scope.selectorPosition === bottom ? top : scope.selectorPosition + 1);
keyUpDownWerePressed = true;
if (!scope.query.length && !scope.isOpen) {
Expand All @@ -208,13 +259,14 @@ angular.module('oi.multiselect')
break;

case 13: /* enter */
//case 9: /* tab */
if (!oiUtils.groupsIsEmpty(scope.groups)) {
scope.addItem(scope.order[scope.selectorPosition]);
if (scope.selectorPosition === bottom) {
setOption(listElement, 0);
}
}
saveOn('enter');
break;
case 9: /* tab */
saveOn('tab');
break;
case 220: /* slash */
saveOn('slash');
event.preventDefault(); //backslash interpreted as a regexp
break;

case 27: /* esc */
Expand Down Expand Up @@ -268,7 +320,7 @@ angular.module('oi.multiselect')

function blurHandler(event) {
if (event.target.ownerDocument.activeElement !== inputElement[0]) {
resetMatches();
saveOn('blur');
$document.off('click', blurHandler);
scope.isFocused = false;
scope.$digest();
Expand All @@ -283,39 +335,32 @@ angular.module('oi.multiselect')
}

function trackBy(item) {
locals = {};
locals[valueName] = item;
return trackByFn(scope, locals);
return oiUtils.getValue(valueName, item, scope, trackByFn);
}

function filter(list) {
locals = {};
//'name.subname' -> {name: {subname: list}}'
valuesName.split('.').reduce(function(previousValue, currentItem, index, arr) {
return previousValue[currentItem] = index < arr.length - 1 ? {} : list;
}, locals);
return filteredValuesFn(scope.$parent, locals);
function selectAs(item) {
return oiUtils.getValue(valueName, item, scope, selectAsFn);
}

function getLabel(item) {
locals = {};
locals[valueName] = item;
return displayFn(scope, locals);
return oiUtils.getValue(valueName, item, scope, displayFn);
}

function getGroupName(option) {
locals = {};
locals[valueName] = option;
return groupByFn(scope, locals) || '';
return oiUtils.getValue(valueName, option, scope, groupByFn) || '';
}

function getMatches(query) {
var values = valuesFn(scope.$parent, {$query: query}),
function filter(list) {
return oiUtils.getValue(valuesName, list, scope.$parent, filteredValuesFn);
}

function getMatches(query, querySelectAs) {
var values = valuesFn(scope.$parent, {$query: query, $querySelectAs: querySelectAs}),
waitTime = 0;

scope.selectorPosition = 0;
scope.selectorPosition = options.newItem === 'prompt' ? false : 0;

if (!query) {
if (!query && !querySelectAs) {
scope.oldQuery = null;
}

Expand All @@ -326,14 +371,26 @@ angular.module('oi.multiselect')

timeoutPromise = $timeout(function() {
scope.showLoader = true;
$q.when(values).then(function(values) {
scope.groups = group(filter(removeChoosenFromList($filter(options.listFilter)(toArr(values), query, getLabel))));
updateGroupPos();

}).finally(function(){
scope.showLoader = false;
});
return $q.when(values)
.then(function(values) {
if (!querySelectAs) {
var filteredList = $filter(options.listFilter)(toArr(values), query, getLabel);
var withoutOverlap = oiUtils.intersection(filteredList, scope.output, oiUtils.isEqual, trackBy, trackBy, true);
var filteredOutput = filter(withoutOverlap);

scope.groups = group(filteredOutput);

updateGroupPos();
}
return values;
})
.finally(function(){
scope.showLoader = false;
});
}, waitTime);

return timeoutPromise;
}

function toArr(list) {
Expand All @@ -342,23 +399,6 @@ angular.module('oi.multiselect')
return [].concat(input);
}

function removeChoosenFromList(input) {
var i, j, chosen = [].concat(scope.output);

for (i = 0; i < input.length; i++) {
for (j = 0; j < chosen.length; j++) {
if (trackBy(input[i]) === trackBy(chosen[j])) {
input.splice(i, 1);
chosen.splice(j, 1);
i--;
break;
}
}
}

return input;
}

function updateGroupPos() {
var i, key, value, collectionKeys = [], groupCount = 0;

Expand Down
Loading

0 comments on commit 2ce5b8a

Please sign in to comment.