angular-cached-resource
Version:
An AngularJS module to interact with RESTful resources, even when browser is offline
287 lines (238 loc) • 9.55 kB
JavaScript
'use strict';
angular.module('mgcrea.ngStrap.select', ['mgcrea.ngStrap.tooltip', 'mgcrea.ngStrap.helpers.parseOptions'])
.provider('$select', function() {
var defaults = this.defaults = {
animation: 'am-fade',
prefixClass: 'select',
placement: 'bottom-left',
template: 'select/select.tpl.html',
trigger: 'focus',
container: false,
keyboard: true,
html: false,
delay: 0,
multiple: false,
sort: true,
caretHtml: ' <span class="caret"></span>',
placeholder: 'Choose among the following...',
maxLength: 3,
maxLengthHtml: 'selected'
};
this.$get = function($window, $document, $rootScope, $tooltip) {
var bodyEl = angular.element($window.document.body);
var isTouch = 'createTouch' in $window.document;
function SelectFactory(element, controller, config) {
var $select = {};
// Common vars
var options = angular.extend({}, defaults, config);
$select = $tooltip(element, options);
var parentScope = config.scope;
var scope = $select.$scope;
scope.$matches = [];
scope.$activeIndex = 0;
scope.$isMultiple = options.multiple;
scope.$activate = function(index) {
scope.$$postDigest(function() {
$select.activate(index);
});
};
scope.$select = function(index, evt) {
scope.$$postDigest(function() {
$select.select(index);
});
};
scope.$isVisible = function() {
return $select.$isVisible();
};
scope.$isActive = function(index) {
return $select.$isActive(index);
};
// Public methods
$select.update = function(matches) {
scope.$matches = matches;
$select.$updateActiveIndex();
};
$select.activate = function(index) {
if(options.multiple) {
scope.$activeIndex.sort();
$select.$isActive(index) ? scope.$activeIndex.splice(scope.$activeIndex.indexOf(index), 1) : scope.$activeIndex.push(index);
if(options.sort) scope.$activeIndex.sort();
} else {
scope.$activeIndex = index;
}
return scope.$activeIndex;
};
$select.select = function(index) {
var value = scope.$matches[index].value;
$select.activate(index);
if(options.multiple) {
controller.$setViewValue(scope.$activeIndex.map(function(index) {
return scope.$matches[index].value;
}));
} else {
controller.$setViewValue(value);
}
controller.$render();
if(parentScope) parentScope.$digest();
// Hide if single select
if(!options.multiple) {
$select.hide();
}
// Emit event
scope.$emit('$select.select', value, index);
};
// Protected methods
$select.$updateActiveIndex = function() {
if(controller.$modelValue && scope.$matches.length) {
if(options.multiple && angular.isArray(controller.$modelValue)) {
scope.$activeIndex = controller.$modelValue.map(function(value) {
return $select.$getIndex(value);
});
} else {
scope.$activeIndex = $select.$getIndex(controller.$modelValue);
}
} else if(scope.$activeIndex >= scope.$matches.length) {
scope.$activeIndex = options.multiple ? [] : 0;
}
};
$select.$isVisible = function() {
if(!options.minLength || !controller) {
return scope.$matches.length;
}
// minLength support
return scope.$matches.length && controller.$viewValue.length >= options.minLength;
};
$select.$isActive = function(index) {
if(options.multiple) {
return scope.$activeIndex.indexOf(index) !== -1;
} else {
return scope.$activeIndex === index;
}
};
$select.$getIndex = function(value) {
var l = scope.$matches.length, i = l;
if(!l) return;
for(i = l; i--;) {
if(scope.$matches[i].value === value) break;
}
if(i < 0) return;
return i;
};
$select.$onMouseDown = function(evt) {
// Prevent blur on mousedown on .dropdown-menu
evt.preventDefault();
evt.stopPropagation();
// Emulate click for mobile devices
if(isTouch) {
var targetEl = angular.element(evt.target);
targetEl.triggerHandler('click');
}
};
$select.$onKeyDown = function(evt) {
if (!/(9|13|38|40)/.test(evt.keyCode)) return;
evt.preventDefault();
evt.stopPropagation();
// Select with enter
if(evt.keyCode === 13 || evt.keyCode === 9) {
return $select.select(scope.$activeIndex);
}
// Navigate with keyboard
if(evt.keyCode === 38 && scope.$activeIndex > 0) scope.$activeIndex--;
else if(evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) scope.$activeIndex++;
else if(angular.isUndefined(scope.$activeIndex)) scope.$activeIndex = 0;
scope.$digest();
};
// Overrides
var _show = $select.show;
$select.show = function() {
_show();
if(options.multiple) {
$select.$element.addClass('select-multiple');
}
setTimeout(function() {
$select.$element.on(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown);
if(options.keyboard) {
element.on('keydown', $select.$onKeyDown);
}
});
};
var _hide = $select.hide;
$select.hide = function() {
$select.$element.off(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown);
if(options.keyboard) {
element.off('keydown', $select.$onKeyDown);
}
_hide();
};
return $select;
}
SelectFactory.defaults = defaults;
return SelectFactory;
};
})
.directive('bsSelect', function($window, $parse, $q, $select, $parseOptions) {
var defaults = $select.defaults;
return {
restrict: 'EAC',
require: 'ngModel',
link: function postLink(scope, element, attr, controller) {
// Directive options
var options = {scope: scope};
angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'placeholder', 'multiple', 'maxLength', 'maxLengthHtml'], function(key) {
if(angular.isDefined(attr[key])) options[key] = attr[key];
});
// Add support for select markup
if(element[0].nodeName.toLowerCase() === 'select') {
var inputEl = element;
inputEl.css('display', 'none');
element = angular.element('<button type="button" class="btn btn-default"></button>');
inputEl.after(element);
}
// Build proper ngOptions
var parsedOptions = $parseOptions(attr.ngOptions);
// Initialize select
var select = $select(element, controller, options);
// Watch ngOptions values before filtering for changes
var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').trim();
scope.$watch(watchedOptions, function(newValue, oldValue) {
// console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue);
parsedOptions.valuesFn(scope, controller)
.then(function(values) {
select.update(values);
controller.$render();
});
}, true);
// Watch model for changes
scope.$watch(attr.ngModel, function(newValue, oldValue) {
// console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue);
select.$updateActiveIndex();
}, true);
// Model rendering in view
controller.$render = function () {
// console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue);
var selected, index;
if(options.multiple && angular.isArray(controller.$modelValue)) {
selected = controller.$modelValue.map(function(value) {
index = select.$getIndex(value);
return angular.isDefined(index) ? select.$scope.$matches[index].label : false;
}).filter(angular.isDefined);
if(selected.length > (options.maxLength || defaults.maxLength)) {
selected = selected.length + ' ' + (options.maxLengthHtml || defaults.maxLengthHtml);
} else {
selected = selected.join(', ');
}
} else {
index = select.$getIndex(controller.$modelValue);
selected = angular.isDefined(index) ? select.$scope.$matches[index].label : false;
}
element.html((selected ? selected : attr.placeholder || defaults.placeholder) + defaults.caretHtml);
};
// Garbage collection
scope.$on('$destroy', function() {
select.destroy();
options = null;
select = null;
});
}
};
});