Skip to content

Instantly share code, notes, and snippets.

@jhartman86
Last active February 4, 2016 17:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhartman86/79d776ed57c439fab8ef to your computer and use it in GitHub Desktop.
Save jhartman86/79d776ed57c439fab8ef to your computer and use it in GitHub Desktop.
angular.module('sstt.common').
/**
* http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
* https://leanpub.com/recipes-with-angular-js/read#leanpub-auto-editing-text-in-place-using-html5-contenteditable
* http://jonathancreamer.com/working-with-all-the-different-kinds-of-scopes-in-angular/
* http://radify.io/blog/understanding-ngmodelcontroller-by-example-part-1/
* @todo: clean up key binding code; make configurable
* @todo: clean up/make more efficient the overlay list positioning code, and test
* across more browsers
* @todo: optimize transcludeFn to see if we can eliminate creating a new scope for every
* single iteration of the lists; cache list builds (maybe use documentFragments?)
* @todo: DESTRUCT everything (all event bindings, scopes, DOM nodes)
*/
directive('selectorate', ['$window', function( $window ){
var KEY_ARROW_DOWN = 40,
KEY_ARROW_UP = 38,
KEY_ESCAPE = 27,
KEY_TAB = 9,
KEY_SPACEBAR = 32,
KEY_ENTER = 13;
function _link( $scope, $elem, attrs, Controller, transcludeFn ){
var $optsList = angular.element($elem[0].querySelector('.selectorate-opts')),
$textInput = angular.element($elem[0].querySelector('[selectorate-input]')),
_transcluded = [];
$textInput.on('focus', function(){
$scope.$applyAsync(Controller.doListFilter);
});
var holdBlur = false;
$textInput.on('blur', function(){
if( holdBlur ){ return ;}
$scope.$applyAsync(Controller.clearList);
});
$optsList.on('mouseenter', function(){
holdBlur = true;
});
$optsList.on('mouseleave', function(){
holdBlur = false;
});
$textInput.on('keydown', function( e ){
var children = $optsList[0].children,
listLength = children.length,
current = $optsList[0].querySelector('.keyed'),
index = Array.prototype.slice.call(children).indexOf(current);
switch( e.keyCode ){
case 40: // down arrow
if( index + 1 !== listLength ){
angular.element(current).removeClass('keyed');
angular.element(children[index + 1]).addClass('keyed');
}
break;
case 38: // up arrow
if( (index - 1 < 0) !== true ){
angular.element(current).removeClass('keyed');
angular.element(children[index-1]).addClass('keyed');
}
break;
case 27: // escape
$scope.$applyAsync(Controller.clearList);
break;
case 9: case 32: case 13: // tab, spacebar, enter
// If user has typed and narrowed the list down to just
// one value, then hits tab, we should automatically
// select it and move to the next element
if( listLength === 1 ){
angular.element(children).triggerHandler('click');
$scope.$applyAsync(Controller.clearList);
}
// If a selection/highlight has been made...
if( current ){
angular.element(current).triggerHandler('click');
$scope.$applyAsync(Controller.clearList);
}
break;
}
});
function setPosition(){
var rect = $elem[0].getBoundingClientRect(),
width = rect.width,
top = rect.top;
console.log('adjusting');
$optsList.css({width:width + 'px',top:(rect.top+rect.height) + 'px'});
}
/**
* On every change to the filteredList data, we need to clean up
* all previously generated DOM nodes and scopes to prevent memory
* leaks.
* @param function Callback - "after cleanup, then func()"
* @return void
*/
function cleanup( _then ){
var _item;
while(_item = _transcluded.pop()){
_item[0].remove();
_item[1].$destroy();
}
_then();
}
/**
* Transclude function, takes the inner template and renders it so
* list styling is super easy.
* @param {jqLite} $cloned DOM element wrapped in jqLite
* @param {object} $scope Newly bound scope
* @return void
*/
function _transcluder( $cloned, $scope ){
$optsList.append($cloned);
_transcluded.push([$cloned, $scope]);
}
/**
* Watch changes to the filteredList, and if it has changed,
* render.
*/
$scope.$watchCollection('selectorate.filteredList', function( list, previous ){
cleanup(function(){
if( list && list !== previous && list.length ){
for(var _i = 0, _len = list.length; _i < _len; _i++){
var $newScope = $scope.$new();
$newScope.opt = list[_i];
transcludeFn($newScope, _transcluder);
}
}
});
});
angular.element($window).on('scroll resize', setPosition).triggerHandler('resize');
}
/**
* Important: the value that gets set as _key on the controller is crucial,
* as it indicates to the ngModelController how to map back and forth
* between an object behind-the-scenes and a string for the field to
* display.
*/
return {
restrict : 'AE',
transclude : true,
link : _link,
templateUrl : '/template/selectorate.html',
scope : {},
controllerAs : 'selectorate',
bindToController : {
_value : '=selectorate',
_listData : '=list',
_key : '@key',
// Optional: to attach to a form, pass the name
_formName : '@formName',
// Optional: if set, the filter search becomes '$'
_deepSearch : '=deepSearch',
// Optional: if unset, placeholder is 'undefined'
_placeholder : '@placeholder',
// Optional: control's listData depends on another model selection
_dependent : '=dependent',
// Optional: (requires _dependent == true); enable/disable form element
// unless _enabledWith is a valid object
_enabledWith : '=enabledWith',
// Optional: when the ngModelController is composing an empty object,
// we can make it compose a $resource if desired
_composableType : '=composableType'
},
controller : ['$scope', '$filter', function( $scope, $filter ){
this.disabled = false;
/**
* Store reference to this (Controller).
* @type {object}
*/
var self = this;
/**
* Filtered list results.
* @type {Array}
*/
this.filteredList = [];
/**
* Used by doListFilter to generate the filter object we use for
* the search. Note, if _filterProp isn't defined (eg. wasn't
* passed as an attribute, or == "*"), then we set the special
* '$' prop in the object, which means look through _everything_.
* @param {object} v ngModel value, objectified
* @return {object} Returns an object {} for the search
*/
function getFilterObj( v ){
var filter = {};
if( self._deepSearch ){
filter['$'] = v[self._key];
return filter;
}
filter[self._key] = v[self._key];
return filter;
}
/**
* Filter the listData against the text input value
* @param {object} v Object passed in that contains a property
* {{{_key}}:'string'} we can use to filter.
* @return void
*/
this.doListFilter = function( v ){
if( v && typeof(v) === 'object' ){
self.filteredList = $filter('filter')(self._listData, getFilterObj(v));
return;
}
self.filteredList = self._listData;
};
/**
* Clear the list
* @return {[type]} [description]
*/
this.clearList = function(){
self.filteredList = [];
};
/**
* Select the item from the list.
* @param {object} opt List item to choose
* @return void
*/
this.choose = function( opt ){
self._value = opt;
self.clearList();
};
/**
* Because _listData property is a two-way data binding, we can
* set it up so that if th _listData changes, we "reset" this
* instance. Case in point being - if there are two instances and
* the second instance depends on the list data from the first
* model value.
*/
if( this._dependent ){
// If the listData this control is dependent *on* changes,
// set this to no value and reset UI state.
$scope.$watch('selectorate._listData', function( v, prev ){
if( v !== prev ){
self._value = null;
}
});
// Don't enable the control until the model for _enabledWith
// becomes valid
$scope.$watch('selectorate._enabledWith', function( v ){
self.disabled = v ? false : true;
});
}
}]
};
}]).
/**
* Responsible for translating an object behind the scenes to just some
* text display in the input field.
*/
directive('selectorateInput', [function(){
function _link( $scope, $elem, attrs, controllers ){
var ngModel = controllers[0],
ctrlSelectorate = controllers[1];
/**
* Formats the output.
*/
ngModel.$formatters.push(function( mv ){
if( mv && typeof(mv) === 'object' ){
return mv[ctrlSelectorate._key];
}
});
/**
* Parses a receieved input into the correct object structure,
* which formatters can handle.
*/
ngModel.$parsers.push(function( vv ){
// Is it already an object? Then just return it.
if( vv && typeof(vv) === 'object' ){
return vv;
}
// If here, we need to compose to an object.
var obj = {};
obj[ctrlSelectorate._key] = vv;
// If a specific type of object to compose is defined (eg. a
// $resource), we can return that here. *Note* - it must be
// 'new'-able, as in have a constructor.
if( ctrlSelectorate._composableType ){
return new ctrlSelectorate._composableType(obj);
}
// Otherwise just return a plainly composed object.
return obj;
});
/**
* When a view change occurs, modify the list filter.
*/
ngModel.$viewChangeListeners.push(function(){
ctrlSelectorate.doListFilter(ngModel.$modelValue);
});
}
return {
restrict : 'A',
scope : false, // SHARE PARENT DIRECTIVE'S SCOPE!
require : ['ngModel', '^selectorate'],
link : _link
};
}]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment