Unverified Commit 28cc2570 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #1197 from hypothesis/remove-old-dropdown

Remove old dropdown menu implementation
parents cf87bd52 03b90195
......@@ -131,7 +131,6 @@ function startAngularApp(config) {
// Angular addons which do not export the Angular module
// name via module.exports
['ngTagsInput', require('ng-tags-input')][0],
['ui.bootstrap', require('./vendor/ui-bootstrap-custom-tpls-0.13.4')][0],
// Local addons
'ngRaven',
......
<span dropdown keyboard-nav>
<button
type="button"
class="top-bar__btn"
dropdown-toggle
title="Sort by {{vm.sortKey}}">
<i class="h-icon-sort"></i>
</button>
<div class="dropdown-menu__top-arrow"></div>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row"
ng-repeat="key in vm.sortKeysAvailable"
ng-click="vm.onChangeSortKey({sortKey: key})"
><span class="dropdown-menu-radio"
ng-class="{'is-selected' : vm.sortKey === key}"
></span><a class="dropdown-menu__link" href="">{{key}}</a></li>
</ul>
</span>
/*
* angular-ui-bootstrap
* http://angular-ui.github.io/bootstrap/
* Version: 0.13.4 - 2015-09-03
* License: MIT
*/
angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.dropdown","ui.bootstrap.position"]);
angular.module("ui.bootstrap.tpls", []);
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
.constant('dropdownConfig', {
openClass: 'open'
})
.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;
}
};
}]);
......@@ -82,8 +82,6 @@
}
@mixin primary-action-btn {
// note that there is currently some duplication here between
// the styling for this element and <dropdown-menu-btn>
color: $color-seashell;
background-color: $color-dove-gray;
height: 35px;
......
......@@ -33,11 +33,6 @@ html {
font-size: $body1-font-size;
}
//Candidate for cleanup
.pull-right {
float: right;
}
//CLOSER////////////////////////////////
.close {
cursor: pointer;
......@@ -67,221 +62,6 @@ html {
}
}
//DROPDOWNS////////////////////////////////
.dropdown {
position: relative;
span {
cursor: pointer;
&:hover {
color: black;
}
}
}
.dropdown-toggle {
cursor: pointer;
&:active {
outline: 0;
}
}
.dropdown-menu {
visibility: hidden;
background: $white;
border: solid 1px $gray-lighter;
margin-top: 6px;
top: 100%;
float: left;
position: absolute;
z-index: 2;
&__link {
color: inherit;
display: block;
line-height: 1;
white-space: nowrap;
font-size: $body2-font-size;
cursor: pointer;
&:hover {
color: $brand-color;
}
}
&__link--subtle {
&:hover {
color: $color-gray;
}
}
&__link--disabled {
&:hover {
cursor: default;
color: inherit;
}
}
// These psuedo-elements add the speech bubble tail / triangle.
&:before,
&:after {
// http://www.red-team-design.com/css-diagonal-borders-still-not-rendering-properly-on-firefox
transform: scale(0.9999);
border-color: rgba(0, 0, 0, 0);
border-style: solid;
border-width: 0 7px 6px 7px;
content: '';
position: absolute;
height: 0;
left: 0;
width: 0;
}
&:before {
border-bottom-color: $gray-lighter;
top: -7px;
}
&:after {
border-bottom-color: $white;
top: -6px;
z-index: 3;
}
// Aligns the dropdown menu to right
&.pull-right {
right: 0;
left: auto;
text-align: right;
// Align the tail
&:before,
&:after {
left: auto;
right: 0;
}
}
// Aligns the dropdown menu's '▲' indicator to the center,
// the menu itself has to be aligned by the user
&.pull-center {
// Align the tail
&:before,
&:after {
left: auto;
right: 50%;
}
}
// Hides the dropdown menu's default ▲ indicator.
// This is used for cases where a separate element is used
// as the indicator
&.pull-none {
&:before,
&:after {
display: none;
}
}
}
.open {
& > .dropdown-menu {
@include smallshadow;
visibility: visible;
}
}
// A 'standalone' arrow for the top of a dropdown menu.
// .dropdown-menu has a "built-in" arrow using CSS :before,:after.
//
// The standalone arrow is used when the dropdown menu arrow needs
// to be positioned relative to the item which opens the menu rather
// than relative to the dropdown menu content.
//
// When using the standalone arrow, the built-in arrow is hidden using
// the .pull-none class
.dropdown-menu__top-arrow {
// the element itself forms the outer border of the arrow
$arrow-size: 7px;
$bottom-offset: $arrow-size - 1px;
visibility: hidden;
width: 0px;
height: 0px;
border-top: none;
border-left: $arrow-size solid transparent;
border-right: $arrow-size solid transparent;
border-bottom: $arrow-size solid $gray-lighter;
position: absolute;
right: 0px;
bottom: -$bottom-offset;
z-index: 3;
// :after is used to create another arrow of the same size at a 1px
// vertical offset, forming the fill and base of the arrow
&:after {
width: 0px;
height: 0px;
border-top: none;
border-left: $arrow-size solid transparent;
border-right: $arrow-size solid transparent;
border-bottom: $arrow-size solid white;
content: '';
position: absolute;
left: 0 - $arrow-size;
top: 1px;
}
}
.open > .dropdown-menu__top-arrow {
visibility: visible;
}
// Row in a dropdown menu
.dropdown-menu__row {
display: flex;
flex-direction: row;
align-items: center;
padding-left: 8px;
padding-right: 16px;
min-height: 40px;
min-width: 120px;
&:not(:first-child) {
border-top: dotted 1px $gray-lighter;
}
}
// Row in a dropdown menu
.dropdown-menu__row--no-border {
border: none;
}
.dropdown-menu__row--unpadded {
padding-left: 0px;
padding-right: 0px;
}
// Radio button in a dropdown menu
.dropdown-menu-radio {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-left: 8px;
margin-right: 8px;
border: 1px solid $color-silver;
}
.dropdown-menu-radio.is-selected {
border: 4px solid $color-dove-gray;
}
//TABS////////////////////////////////
.nav-tabs {
margin-bottom: 0.7em;
......
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