Skip to content
This repository has been archived by the owner on Nov 12, 2017. It is now read-only.

Commit

Permalink
fix(dropdown): transclude options content manually
Browse files Browse the repository at this point in the history
As of Angular.js v1.2.18, nested transclusion with ng-repeat wasn't
binding to the correct scope (see angular/angular.js#7842). To fix that,
we should manually transclude the content of options and then, finally,
compile the options with ng-repeat.

Fixes #14
  • Loading branch information
Gustavo Henke committed Jun 23, 2014
1 parent 35d3e7f commit d933c8b
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 114 deletions.
4 changes: 2 additions & 2 deletions bower.json
Expand Up @@ -2,10 +2,10 @@
"name": "frontkit",
"version": "0.0.1",
"dependencies": {
"angular": "1.2.17"
"angular": "~1.2.10"
},
"devDependencies": {
"angular-mocks": "1.2.17",
"angular-mocks": "~1.2.10",
"holderjs": "~2.3.1",
"highlightjs": "~8.0.0",
"normalize.css": "~3.0.0"
Expand Down
228 changes: 117 additions & 111 deletions src/scripts/dropdown.js
Expand Up @@ -135,21 +135,7 @@

ctrl.parseOptions = function( model ) {
var currPromise;
$scope.$watch( model, watchFn, true );
$scope.$watch( model, function( newValue, oldValue ) {
var isFn = ng.isFunction;
var isPromise = !!newValue && isFn( newValue.then );
isPromise &= !!oldValue && isFn( oldValue.then );

// We won't handle anything here unless both values are promises.
if ( !ng.equals( newValue, oldValue ) || !isPromise ) {
return;
}

watchFn( newValue );
});

function watchFn( value ) {
$scope.$watch( model, function watchFn( value ) {
var promise;
currPromise = null;

Expand All @@ -174,7 +160,7 @@

currPromise = promise;
}
}
}, true );
};

function clearSearch() {
Expand Down Expand Up @@ -204,9 +190,10 @@
]);

module.directive( "dropdownOptions", [
"$compile",
"repeatParser",
"dropdownConfig",
function( repeatParser, dropdownConfig ) {
function( $compile, repeatParser, dropdownConfig ) {
var definition = {};

definition.restrict = "EA";
Expand All @@ -215,112 +202,131 @@
definition.templateUrl = "templates/dropdown/options.html";
definition.require = "^dropdown";

definition.compile = function( tElement, tAttr ) {
var model;
var option = tElement.querySelector( ".dropdown-option" );
var repeat = repeatParser.parse( tAttr.items );

if ( !repeat ) {
definition.compile = function( tElement ) {
// When in a detached case, we won't let compile go ahead
if ( !tElement.parent().length ) {
return;
}

model = repeat.expr;
repeat.expr = "$dropdown.options";

option.attr( "ng-repeat", repeatParser.toNgRepeat( repeat ) );
option.attr( "ng-click", "$dropdown.addItem( " + repeat.item + " )" );
option.attr( "ng-class", "{" +
"active: $dropdown.activeOption === " + repeat.item +
"}" );

return function( scope, element, attr, $dropdown ) {
var list = element[ 0 ];
configureOverflow();

$dropdown.parseOptions( model );
$dropdown.valueKey = tAttr.value || null;

scope.$watch( "$dropdown.open", adjustScroll );
scope.$watch( "$dropdown.activeOption", adjustScroll );

function adjustScroll() {
var fromScrollTop, index, activeElem;
var options = $dropdown.options;
var scrollTop = list.scrollTop;

if ( ng.isArray( options ) ) {
index = options.indexOf( $dropdown.activeOption );
} else {
// To keep compatibility with arrays, we'll init the index as -1
index = -1;
Object.keys( options ).some(function( key, i ) {
if ( options[ key ] === $dropdown.activeOption ) {
index = i;
return true;
}
});
}
return definition.link;
};

activeElem = list.querySelectorAll( ".dropdown-option" )[ index ];
definition.link = function( scope, element, attr, $dropdown, transclude ) {
var list = element[ 0 ];
var option = element.querySelector( ".dropdown-option" );
var repeat = repeatParser.parse( attr.items );

// If we have a repeat expr, let's use it to build the option list
if ( repeat ) {
$dropdown.parseOptions( repeat.expr );
repeat.expr = "$dropdown.options";

// Option list building
transclude(function( childs ) {
option.append( childs );
});

// Add a few directives to the option...
option.attr( "ng-repeat", repeatParser.toNgRepeat( repeat ) );
option.attr( "ng-click", "$dropdown.addItem( " + repeat.item + " )" );
option.attr( "ng-class", "{" +
"active: $dropdown.activeOption === " + repeat.item +
"}" );

// ...and compile it
$compile( option )( scope );
}

if ( !$dropdown.open || !activeElem ) {
// To be handled!
return;
}
// Configure the overflow for this list
configureOverflow();

fromScrollTop = activeElem.offsetTop - list.scrollTop;

// If the option is above the current scroll, we'll make it appear on the
// top of the scroll.
// Otherwise, it'll appear in the end of the scroll view.
if ( fromScrollTop < 0 ) {
scrollTop = activeElem.offsetTop;
} else if ( list.clientHeight <= fromScrollTop + activeElem.clientHeight ) {
scrollTop = activeElem.offsetTop +
activeElem.clientHeight -
list.clientHeight;
}
// Set the value key
$dropdown.valueKey = attr.value || null;

list.scrollTop = scrollTop;
}
// Scope Watches
// ---------------------------------------------------------------------------------
scope.$watch( "$dropdown.open", adjustScroll );
scope.$watch( "$dropdown.activeOption", adjustScroll );

// Functions
// ---------------------------------------------------------------------------------
function adjustScroll() {
var fromScrollTop, index, activeElem;
var options = $dropdown.options;
var scrollTop = list.scrollTop;

function configureOverflow() {
var height;
var view = list.ownerDocument.defaultView;
var styles = view.getComputedStyle( list, null );
var display = element.css( "display" );
var size = dropdownConfig.optionsPageSize;
var li = $( "<li class='dropdown-option'>&nbsp;</li>" )[ 0 ];
element.prepend( li );

// Temporarily show the element, just to calculate the li height
element.css( "display", "block" );

// Calculate the height, considering border/padding
height = li.clientHeight * size;
height = [ "padding", "border" ].reduce(function( value, prop ) {
var top = styles.getPropertyValue( prop + "-top" ) || "";
var bottom = styles.getPropertyValue( prop + "-bottom" ) || "";

value += +top.replace( "px", "" ) || 0;
value += +bottom.replace( "px", "" ) || 0;

return value;
}, height );

// Set overflow CSS rules
element.css({
"overflow-y": "auto",
"max-height": height + "px"
if ( ng.isArray( options ) ) {
index = options.indexOf( $dropdown.activeOption );
} else {
// To keep compatibility with arrays, we'll init the index as -1
index = -1;
Object.keys( options ).some(function( key, i ) {
if ( options[ key ] === $dropdown.activeOption ) {
index = i;
return true;
}
});
}

// And finally, set the element display to the previous value
element.css( "display", display );
activeElem = list.querySelectorAll( ".dropdown-option" )[ index ];

// Also remove the dummy <li> created previously
$( li ).remove();
if ( !$dropdown.open || !activeElem ) {
// To be handled!
return;
}

fromScrollTop = activeElem.offsetTop - list.scrollTop;

// If the option is above the current scroll, we'll make it appear on the
// top of the scroll.
// Otherwise, it'll appear in the end of the scroll view.
if ( fromScrollTop < 0 ) {
scrollTop = activeElem.offsetTop;
} else if ( list.clientHeight <= fromScrollTop + activeElem.clientHeight ) {
scrollTop = activeElem.offsetTop +
activeElem.clientHeight -
list.clientHeight;
}
};

list.scrollTop = scrollTop;
}

function configureOverflow() {
var height;
var view = list.ownerDocument.defaultView;
var styles = view.getComputedStyle( list, null );
var display = element.css( "display" );
var size = dropdownConfig.optionsPageSize;
var li = $( "<li class='dropdown-option'>&nbsp;</li>" )[ 0 ];
element.prepend( li );

// Temporarily show the element, just to calculate the li height
element.css( "display", "block" );

// Calculate the height, considering border/padding
height = li.clientHeight * size;
height = [ "padding", "border" ].reduce(function( value, prop ) {
var top = styles.getPropertyValue( prop + "-top" ) || "";
var bottom = styles.getPropertyValue( prop + "-bottom" ) || "";

value += +top.replace( "px", "" ) || 0;
value += +bottom.replace( "px", "" ) || 0;

return value;
}, height );

// Set overflow CSS rules
element.css({
"overflow-y": "auto",
"max-height": height + "px"
});

// And finally, set the element display to the previous value
element.css( "display", display );

// Also remove the dummy <li> created previously
$( li ).remove();
}
};

return definition;
Expand Down
2 changes: 1 addition & 1 deletion src/templates/dropdown/options.html
@@ -1,3 +1,3 @@
<ul class="dropdown-options">
<li class="dropdown-option" ng-transclude></li>
<li class="dropdown-option"></li>
</ul>

0 comments on commit d933c8b

Please sign in to comment.