diff --git a/static/css/directives/repo-view/repo-panel-changes.css b/static/css/directives/repo-view/repo-panel-changes.css index 14838ee1d..8c4310a47 100644 --- a/static/css/directives/repo-view/repo-panel-changes.css +++ b/static/css/directives/repo-view/repo-panel-changes.css @@ -26,3 +26,9 @@ margin-right: 8px; vertical-align: middle; } + +.repo-panel-changes .multiselect-dropdown { + display: inline-block; + margin-left: 10px; + min-width: 200px; +} \ No newline at end of file diff --git a/static/css/directives/ui/multiselect-dropdown.css b/static/css/directives/ui/multiselect-dropdown.css new file mode 100644 index 000000000..76f29f3dd --- /dev/null +++ b/static/css/directives/ui/multiselect-dropdown.css @@ -0,0 +1,42 @@ +.multiselect-dropdown .dropdown, +.multiselect-dropdown .dropdown .btn-dropdown, +.multiselect-dropdown .dropdown .dropdown-menu { + width: 100%; +} + +.multiselect-dropdown .dropdown .btn-dropdown { + text-align: left; + position: relative; + padding-right: 16px; +} + +.multiselect-dropdown .dropdown .btn-dropdown .caret { + position: absolute; + top: 14px; + right: 10px; +} + +.multiselect-dropdown .none { + color: #ccc; + margin-right: 10px; +} + +.multiselect-dropdown .dropdown-menu { + padding: 10px; +} + +.multiselect-dropdown .dropdown-menu .menu-item { + padding: 4px; +} + +.multiselect-dropdown .dropdown-menu .menu-item .co-checkable-item { + margin-right: 6px; +} + +.multiselect-dropdown .dropdown-menu .menu-item .menu-item-template { + vertical-align: middle; +} + +.multiselect-dropdown .selected-item-template { + margin-right: 10px; +} diff --git a/static/directives/multiselect-dropdown.html b/static/directives/multiselect-dropdown.html new file mode 100644 index 000000000..11acda579 --- /dev/null +++ b/static/directives/multiselect-dropdown.html @@ -0,0 +1,31 @@ +
+ +
\ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-changes.html b/static/directives/repo-view/repo-panel-changes.html index 281fff6bb..094df41b0 100644 --- a/static/directives/repo-view/repo-panel-changes.html +++ b/static/directives/repo-view/repo-panel-changes.html @@ -1,23 +1,24 @@
+

+ Visualize Tags: + + {{ item }} + +

+
No tags selected to view
- Please select one or more tags in the Tags tab to visualize. + Please select one or more tags above.
-
-

- Visualize Tags: - - {{ tag }} - -

- +
diff --git a/static/js/directives/ng-transcope.js b/static/js/directives/ng-transcope.js new file mode 100644 index 000000000..795256e8b --- /dev/null +++ b/static/js/directives/ng-transcope.js @@ -0,0 +1,25 @@ +/** + * Directive to transclude a template under an ng-repeat. From: http://stackoverflow.com/a/24512435 + */ +angular.module('quay').directive('ngTranscope', function() { + return { + link: function( $scope, $element, $attrs, controller, $transclude ) { + if ( !$transclude ) { + throw minErr( 'ngTranscope' )( 'orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag( $element )); + } + var innerScope = $scope.$new(); + + $transclude( innerScope, function( clone ) { + $element.empty(); + $element.append( clone ); + $element.on( '$destroy', function() { + innerScope.$destroy(); + }); + }); + } + }; +}); diff --git a/static/js/directives/repo-view/repo-panel-changes.js b/static/js/directives/repo-view/repo-panel-changes.js index 7b5debe37..585d03c51 100644 --- a/static/js/directives/repo-view/repo-panel-changes.js +++ b/static/js/directives/repo-view/repo-panel-changes.js @@ -96,20 +96,24 @@ angular.module('quay').directive('repoPanelChanges', function () { 'isEnabled': '=isEnabled' }, controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) { + $scope.tagNames = []; var update = function() { - if (!$scope.repository || !$scope.selectedTags) { return; } + if (!$scope.repository || !$scope.isEnabled) { return; } + $scope.tagNames = Object.keys($scope.repository.tags); $scope.currentImage = null; $scope.currentTag = null; - if (!$scope.tracker) { + if ($scope.tracker) { + refreshTree(); + } else { updateImages(); } }; var updateImages = function() { - if (!$scope.repository || !$scope.images) { return; } + if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; } $scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images); @@ -120,16 +124,17 @@ angular.module('quay').directive('repoPanelChanges', function () { $scope.$watch('selectedTags', update) $scope.$watch('repository', update); + $scope.$watch('isEnabled', update); + $scope.$watch('images', updateImages); - $scope.$watch('isEnabled', function(isEnabled) { - if (isEnabled) { - refreshTree(); - } - }); + $scope.updateState = function() { + update(); + }; var refreshTree = function() { - if (!$scope.repository || !$scope.images) { return; } + if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; } + if ($scope.selectedTags.length < 1) { return; } $('#image-history-container').empty(); @@ -149,6 +154,7 @@ angular.module('quay').directive('repoPanelChanges', function () { // Give enough time for the UI to be drawn before we resize the tree. $timeout(function() { $scope.tree.notifyResized(); + $scope.setTag($scope.selectedTags[0]); }, 100); // Listen for changes to the selected tag and image in the tree. diff --git a/static/js/directives/ui/multiselect-dropdown.js b/static/js/directives/ui/multiselect-dropdown.js new file mode 100644 index 000000000..d87629f8f --- /dev/null +++ b/static/js/directives/ui/multiselect-dropdown.js @@ -0,0 +1,35 @@ +/** + * An element which displays a dropdown for selecting multiple elements. + */ +angular.module('quay').directive('multiselectDropdown', function ($compile) { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/multiselect-dropdown.html', + transclude: true, + replace: false, + restrict: 'C', + scope: { + 'items': '=items', + 'selectedItems': '=selectedItems', + 'itemName': '@itemName', + 'itemChecked': '&itemChecked' + }, + controller: function($scope, $element) { + $scope.isChecked = function(checked, item) { + return checked.indexOf(item) >= 0; + }; + + $scope.toggleItem = function(item) { + var isChecked = $scope.isChecked($scope.selectedItems, item); + if (!isChecked) { + $scope.selectedItems.push(item); + } else { + var index = $scope.selectedItems.indexOf(item); + $scope.selectedItems.splice(index, 1); + } + $scope.itemChecked({'item': item, 'checked': !isChecked}); + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/lib/dropdowns-enhancement.js b/static/lib/dropdowns-enhancement.js new file mode 100644 index 000000000..3b885dc7d --- /dev/null +++ b/static/lib/dropdowns-enhancement.js @@ -0,0 +1,267 @@ +/* ======================================================================== + * Bootstrap Dropdowns Enhancement: dropdowns-enhancement.js v3.1.1 (Beta 1) + * http://behigh.github.io/bootstrap_dropdowns_enhancement/ + * ======================================================================== + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + +(function($) { + "use strict"; + + var toggle = '[data-toggle="dropdown"]', + disabled = '.disabled, :disabled', + backdrop = '.dropdown-backdrop', + menuClass = 'dropdown-menu', + subMenuClass = 'dropdown-submenu', + namespace = '.bs.dropdown.data-api', + eventNamespace = '.bs.dropdown', + openClass = 'open', + touchSupport = 'ontouchstart' in document.documentElement, + opened; + + + function Dropdown(element) { + $(element).on('click' + eventNamespace, this.toggle) + } + + var proto = Dropdown.prototype; + + proto.toggle = function(event) { + var $element = $(this); + + if ($element.is(disabled)) return; + + var $parent = getParent($element); + var isActive = $parent.hasClass(openClass); + var isSubMenu = $parent.hasClass(subMenuClass); + var menuTree = isSubMenu ? getSubMenuParents($parent) : null; + + closeOpened(event, menuTree); + + if (!isActive) { + if (!menuTree) + menuTree = [$parent]; + + if (touchSupport && !$parent.closest('.navbar-nav').length && !menuTree[0].find(backdrop).length) { + // if mobile we use a backdrop because click events don't delegate + $('
').appendTo(menuTree[0]).on('click', closeOpened) + } + + for (var i = 0, s = menuTree.length; i < s; i++) { + if (!menuTree[i].hasClass(openClass)) { + menuTree[i].addClass(openClass); + positioning(menuTree[i].children('.' + menuClass), menuTree[i]); + } + } + opened = menuTree[0]; + } + + return false; + }; + + proto.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return; + + var $this = $(this); + + e.preventDefault(); + e.stopPropagation(); + + if ($this.is('.disabled, :disabled')) return; + + var $parent = getParent($this); + var isActive = $parent.hasClass('open'); + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).trigger('focus'); + return $this.trigger('click') + } + + var desc = ' li:not(.divider):visible a'; + var desc1 = 'li:not(.divider):visible > input:not(disabled) ~ label'; + var $items = $parent.find(desc1 + ', ' + '[role="menu"]' + desc + ', [role="listbox"]' + desc); + + if (!$items.length) return; + + var index = $items.index($items.filter(':focus')); + + if (e.keyCode == 38 && index > 0) index--; // up + if (e.keyCode == 40 && index < $items.length - 1) index++; // down + if (!~index) index = 0; + + $items.eq(index).trigger('focus') + }; + + proto.change = function (e) { + + var + $parent, + $menu, + $toggle, + selector, + text = '', + $items; + + $menu = $(this).closest('.' + menuClass); + + $toggle = $menu.parent().find('[data-label-placement]'); + + if (!$toggle || !$toggle.length) { + $toggle = $menu.parent().find(toggle); + } + + if (!$toggle || !$toggle.length || $toggle.data('placeholder') === false) + return; // do nothing, no control + + ($toggle.data('placeholder') == undefined && $toggle.data('placeholder', $.trim($toggle.text()))); + text = $.data($toggle[0], 'placeholder'); + + $items = $menu.find('li > input:checked'); + + if ($items.length) { + text = []; + $items.each(function () { + var str = $(this).parent().find('label').eq(0), + label = str.find('.data-label'); + + if (label.length) { + var p = $('

'); + p.append(label.clone()); + str = p.html(); + } + else { + str = str.html(); + } + + + str && text.push($.trim(str)); + }); + + text = text.length < 4 ? text.join(', ') : text.length + ' selected'; + } + + var caret = $toggle.find('.caret'); + + $toggle.html(text || ' '); + if (caret.length) + $toggle.append(' ') && caret.appendTo($toggle); + + }; + + function positioning($menu, $control) { + if ($menu.hasClass('pull-center')) { + $menu.css('margin-right', $menu.outerWidth() / -2); + } + + if ($menu.hasClass('pull-middle')) { + $menu.css('margin-top', ($menu.outerHeight() / -2) - ($control.outerHeight() / 2)); + } + } + + function closeOpened(event, menuTree) { + if (opened) { + + if (!menuTree) { + menuTree = [opened]; + } + + var parent; + + if (opened[0] !== menuTree[0][0]) { + parent = opened; + } else { + parent = menuTree[menuTree.length - 1]; + if (parent.parent().hasClass(menuClass)) { + parent = parent.parent(); + } + } + + parent.find('.' + openClass).removeClass(openClass); + + if (parent.hasClass(openClass)) + parent.removeClass(openClass); + + if (parent === opened) { + opened = null; + $(backdrop).remove(); + } + } + } + + function getSubMenuParents($submenu) { + var result = [$submenu]; + var $parent; + while (!$parent || $parent.hasClass(subMenuClass)) { + $parent = ($parent || $submenu).parent(); + if ($parent.hasClass(menuClass)) { + $parent = $parent.parent(); + } + if ($parent.children(toggle)) { + result.unshift($parent); + } + } + return result; + } + + function getParent($this) { + var selector = $this.attr('data-target'); + + if (!selector) { + selector = $this.attr('href'); + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7 + } + + var $parent = selector && $(selector); + + return $parent && $parent.length ? $parent : $this.parent() + } + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown; + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this); + var data = $this.data('bs.dropdown'); + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))); + if (typeof option == 'string') data[option].call($this); + }) + }; + + $.fn.dropdown.Constructor = Dropdown; + + $.fn.dropdown.clearMenus = function(e) { + $(backdrop).remove(); + $('.' + openClass + ' ' + toggle).each(function () { + var $parent = getParent($(this)); + var relatedTarget = { relatedTarget: this }; + if (!$parent.hasClass('open')) return; + $parent.trigger(e = $.Event('hide' + eventNamespace, relatedTarget)); + if (e.isDefaultPrevented()) return; + $parent.removeClass('open').trigger('hidden' + eventNamespace, relatedTarget); + }); + return this; + }; + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old; + return this + }; + + + $(document).off(namespace) + .on('click' + namespace, closeOpened) + .on('click' + namespace, toggle, proto.toggle) + .on('click' + namespace, '.dropdown-menu > li > input[type="checkbox"] ~ label, .dropdown-menu > li > input[type="checkbox"], .dropdown-menu.noclose > li', function (e) { + e.stopPropagation() + }) + .on('change' + namespace, '.dropdown-menu > li > input[type="checkbox"], .dropdown-menu > li > input[type="radio"]', proto.change) + .on('keydown' + namespace, toggle + ', [role="menu"], [role="listbox"]', proto.keydown) +}(jQuery)); \ No newline at end of file