Commit 35b96eb9 authored by Robert Knight's avatar Robert Knight

Add a custom build of Angular UI bootstrap for dropdown component

h/static/scripts/vendor/angular-bootstrap.js contains
a legacy version of the dropdown component

ui-bootstrap contains the current version with additional features
including the ability to open and close the dropdown via an
'is-open' attribute, keyboard navigation support and the ability
to use it via attribute directives.

This build has been lightly edited for Angular 1.2.x compatibility
(the original build requires a couple of features from Angular 1.3)

Card 89
parent 9f7ff156
......@@ -122,6 +122,7 @@ module.exports = angular.module('h', [
'ngTagsInput'
'ngWebSocket'
'toastr'
require('./vendor/ui-bootstrap-custom-0.13.4')
])
.controller('AppController', require('./app-controller'))
......
......@@ -8,53 +8,6 @@
var directive = {};
directive.dropdownToggle =
['$document', '$location', '$window',
function ($document, $location, $window) {
var openElement = null, close;
return {
restrict: 'C',
link: function(scope, element, attrs) {
scope.$watch(function dropdownTogglePathWatch(){return $location.path();}, function dropdownTogglePathWatchAction() {
close && close();
});
element.parent().bind('click', function(event) {
close && close();
});
element.bind('click', function(event) {
event.preventDefault();
event.stopPropagation();
var iWasOpen = false;
if (openElement) {
iWasOpen = openElement === element;
close();
}
if (!iWasOpen){
element.parent().addClass('open');
openElement = element;
close = function (event) {
event && event.preventDefault();
event && event.stopPropagation();
$document.unbind('click', close);
element.parent().removeClass('open');
close = null;
openElement = null;
}
$document.bind('click', close);
}
});
}
};
}];
directive.tabbable = function() {
return {
restrict: 'C',
......
/*
* angular-ui-bootstrap
* http://angular-ui.github.io/bootstrap/
* Version: 0.13.4 - 2015-09-03
* License: MIT
*/
module.exports = 'ui.bootstrap';
angular.module("ui.bootstrap", ["ui.bootstrap.dropdown","ui.bootstrap.position"]);
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
.constant('dropdownConfig', {
openClass: 'open'
})
// H - stub implementation for $templateRequest for
// Angular 1.2 compatibility
.factory('$templateRequest', function() {
return function() {
throw new Error('$templateRequest service is not implemented');
}
})
.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
var openScope = null;
this.open = function(dropdownScope) {
if (!openScope) {
$document.bind('click', closeDropdown);
$document.bind('keydown', keybindFilter);
}
if (openScope && openScope !== dropdownScope) {
openScope.isOpen = false;
}
openScope = dropdownScope;
};
this.close = function(dropdownScope) {
if (openScope === dropdownScope) {
openScope = null;
$document.unbind('click', closeDropdown);
$document.unbind('keydown', keybindFilter);
}
};
var closeDropdown = function(evt) {
// This method may still be called during the same mouse event that
// unbound this event handler. So check openScope before proceeding.
if (!openScope) { return; }
if (evt && openScope.getAutoClose() === 'disabled') { return ; }
var toggleElement = openScope.getToggleElement();
if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
return;
}
var dropdownElement = openScope.getDropdownElement();
if (evt && openScope.getAutoClose() === 'outsideClick' &&
dropdownElement && dropdownElement[0].contains(evt.target)) {
return;
}
openScope.isOpen = false;
if (!$rootScope.$$phase) {
openScope.$apply();
}
};
var keybindFilter = function(evt) {
if (evt.which === 27) {
openScope.focusToggleElement();
closeDropdown();
} else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) {
evt.preventDefault();
evt.stopPropagation();
openScope.focusDropdownEntry(evt.which);
}
};
}])
.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
var self = this,
scope = $scope.$new(), // create a child scope so we are not polluting original one
templateScope,
openClass = dropdownConfig.openClass,
getIsOpen,
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
appendToBody = false,
keynavEnabled = false,
selectedOption = null,
body = $document.find('body');
this.init = function(element) {
self.$element = element;
if ($attrs.isOpen) {
getIsOpen = $parse($attrs.isOpen);
setIsOpen = getIsOpen.assign;
$scope.$watch(getIsOpen, function(value) {
scope.isOpen = !!value;
});
}
appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
keynavEnabled = angular.isDefined($attrs.keyboardNav);
if (appendToBody && self.dropdownMenu) {
body.append(self.dropdownMenu);
body.addClass('dropdown');
element.on('$destroy', function handleDestroyEvent() {
self.dropdownMenu.remove();
});
}
};
this.toggle = function(open) {
return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
};
// Allow other directives to watch status
this.isOpen = function() {
return scope.isOpen;
};
scope.getToggleElement = function() {
return self.toggleElement;
};
scope.getAutoClose = function() {
return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
};
scope.getElement = function() {
return self.$element;
};
scope.isKeynavEnabled = function() {
return keynavEnabled;
};
scope.focusDropdownEntry = function(keyCode) {
var elems = self.dropdownMenu ? //If append to body is used.
(angular.element(self.dropdownMenu).find('a')) :
(angular.element(self.$element).find('ul').eq(0).find('a'));
switch (keyCode) {
case (40): {
if (!angular.isNumber(self.selectedOption)) {
self.selectedOption = 0;
} else {
self.selectedOption = (self.selectedOption === elems.length -1 ?
self.selectedOption :
self.selectedOption + 1);
}
break;
}
case (38): {
if (!angular.isNumber(self.selectedOption)) {
self.selectedOption = elems.length - 1;
} else {
self.selectedOption = self.selectedOption === 0 ?
0 : self.selectedOption - 1;
}
break;
}
}
elems[self.selectedOption].focus();
};
scope.getDropdownElement = function() {
return self.dropdownMenu;
};
scope.focusToggleElement = function() {
if (self.toggleElement) {
self.toggleElement[0].focus();
}
};
scope.$watch('isOpen', function(isOpen, wasOpen) {
if (appendToBody && self.dropdownMenu) {
var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
var css = {
top: pos.top + 'px',
display: isOpen ? 'block' : 'none'
};
var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
if (!rightalign) {
css.left = pos.left + 'px';
css.right = 'auto';
} else {
css.left = 'auto';
css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px';
}
self.dropdownMenu.css(css);
}
var openContainer = appendToBody ? body : self.$element;
$animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass); /*.then(function() {
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
toggleInvoker($scope, { open: !!isOpen });
}
});*/
if (isOpen) {
if (self.dropdownMenuTemplateUrl) {
$templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
templateScope = scope.$new();
$compile(tplContent.trim())(templateScope, function(dropdownElement) {
var newEl = dropdownElement;
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
});
});
}
scope.focusToggleElement();
dropdownService.open(scope);
} else {
if (self.dropdownMenuTemplateUrl) {
if (templateScope) {
templateScope.$destroy();
}
var newEl = angular.element('<ul class="dropdown-menu"></ul>');
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
}
dropdownService.close(scope);
self.selectedOption = null;
}
if (angular.isFunction(setIsOpen)) {
setIsOpen($scope, isOpen);
}
});
$scope.$on('$locationChangeSuccess', function() {
if (scope.getAutoClose() !== 'disabled') {
scope.isOpen = false;
}
});
var offDestroy = $scope.$on('$destroy', function() {
scope.$destroy();
});
scope.$on('$destroy', offDestroy);
}])
.directive('dropdown', function() {
return {
controller: 'DropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
dropdownCtrl.init( element );
element.addClass('dropdown');
}
};
})
.directive('dropdownMenu', function() {
return {
restrict: 'AC',
require: '?^dropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if (!dropdownCtrl) {
return;
}
var tplUrl = attrs.templateUrl;
if (tplUrl) {
dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
}
if (!dropdownCtrl.dropdownMenu) {
dropdownCtrl.dropdownMenu = element;
}
}
};
})
.directive('keyboardNav', function() {
return {
restrict: 'A',
require: '?^dropdown',
link: function (scope, element, attrs, dropdownCtrl) {
element.bind('keydown', function(e) {
if ([38, 40].indexOf(e.which) !== -1) {
e.preventDefault();
e.stopPropagation();
var elems = dropdownCtrl.dropdownMenu.find('a');
switch (e.which) {
case (40): { // Down
if (!angular.isNumber(dropdownCtrl.selectedOption)) {
dropdownCtrl.selectedOption = 0;
} else {
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
}
break;
}
case (38): { // Up
if (!angular.isNumber(dropdownCtrl.selectedOption)) {
dropdownCtrl.selectedOption = elems.length - 1;
} else {
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
0 : dropdownCtrl.selectedOption - 1;
}
break;
}
}
elems[dropdownCtrl.selectedOption].focus();
}
});
}
};
})
.directive('dropdownToggle', function() {
return {
require: '?^dropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if (!dropdownCtrl) {
return;
}
element.addClass('dropdown-toggle');
dropdownCtrl.toggleElement = element;
var toggleDropdown = function(event) {
event.preventDefault();
if (!element.hasClass('disabled') && !attrs.disabled) {
scope.$apply(function() {
dropdownCtrl.toggle();
});
}
};
element.bind('click', toggleDropdown);
// WAI-ARIA
element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
element.attr('aria-expanded', !!isOpen);
});
scope.$on('$destroy', function() {
element.unbind('click', toggleDropdown);
});
}
};
});
angular.module('ui.bootstrap.position', [])
/**
* A set of utility methods that can be use to retrieve position of DOM elements.
* It is meant to be used where we need to absolute-position DOM elements in
* relation to other, existing elements (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
.factory('$position', ['$document', '$window', function($document, $window) {
function getStyle(el, cssprop) {
if (el.currentStyle) { //IE
return el.currentStyle[cssprop];
} else if ($window.getComputedStyle) {
return $window.getComputedStyle(el)[cssprop];
}
// finally try and get inline style
return el.style[cssprop];
}
/**
* Checks if a given element is statically positioned
* @param element - raw DOM element
*/
function isStaticPositioned(element) {
return (getStyle(element, 'position') || 'static' ) === 'static';
}
/**
* returns the closest, non-statically positioned parentOffset of a given element
* @param element
*/
var parentOffsetEl = function(element) {
var docDomEl = $document[0];
var offsetParent = element.offsetParent || docDomEl;
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || docDomEl;
};
return {
/**
* Provides read-only equivalent of jQuery's position function:
* http://api.jquery.com/position/
*/
position: function(element) {
var elBCR = this.offset(element);
var offsetParentBCR = { top: 0, left: 0 };
var offsetParentEl = parentOffsetEl(element[0]);
if (offsetParentEl != $document[0]) {
offsetParentBCR = this.offset(angular.element(offsetParentEl));
offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
}
var boundingClientRect = element[0].getBoundingClientRect();
return {
width: boundingClientRect.width || element.prop('offsetWidth'),
height: boundingClientRect.height || element.prop('offsetHeight'),
top: elBCR.top - offsetParentBCR.top,
left: elBCR.left - offsetParentBCR.left
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
* http://api.jquery.com/offset/
*/
offset: function(element) {
var boundingClientRect = element[0].getBoundingClientRect();
return {
width: boundingClientRect.width || element.prop('offsetWidth'),
height: boundingClientRect.height || element.prop('offsetHeight'),
top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
};
},
/**
* Provides coordinates for the targetEl in relation to hostEl
*/
positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
var positionStrParts = positionStr.split('-');
var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
var hostElPos,
targetElWidth,
targetElHeight,
targetElPos;
hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
targetElWidth = targetEl.prop('offsetWidth');
targetElHeight = targetEl.prop('offsetHeight');
var shiftWidth = {
center: function() {
return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
},
left: function() {
return hostElPos.left;
},
right: function() {
return hostElPos.left + hostElPos.width;
}
};
var shiftHeight = {
center: function() {
return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
},
top: function() {
return hostElPos.top;
},
bottom: function() {
return hostElPos.top + hostElPos.height;
}
};
switch (pos0) {
case 'right':
targetElPos = {
top: shiftHeight[pos1](),
left: shiftWidth[pos0]()
};
break;
case 'left':
targetElPos = {
top: shiftHeight[pos1](),
left: hostElPos.left - targetElWidth
};
break;
case 'bottom':
targetElPos = {
top: shiftHeight[pos0](),
left: shiftWidth[pos1]()
};
break;
default:
targetElPos = {
top: hostElPos.top - targetElHeight,
left: shiftWidth[pos1]()
};
break;
}
return targetElPos;
}
};
}]);
<div class="pull-right dropdown">
<div class="pull-right" dropdown>
<span class="dropdown-toggle"
dropdown-toggle
data-toggle="dropdown"
role="button"
ng-switch on="groups.focused().public"
......
......@@ -18,10 +18,10 @@
'other': 'Showing {} selected annotations.'}"></span>
<a href="" ng-click="clearSelection()">Clear selection</a>.</li>
<li ng-show="isStream">
<span class="ng-cloak dropdown">
<span class="ng-cloak" dropdown>
<span role="button"
class="dropdown-toggle"
data-toggle="dropdown">
data-toggle="dropdown"
dropdown-toggle>
Sorted by {{sort.name | lowercase}}
<i class="h-icon-arrow-drop-down"></i>
</span>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment