diff --git a/os_api_ref/__init__.py b/os_api_ref/__init__.py index 8d22130..4b7f47a 100644 --- a/os_api_ref/__init__.py +++ b/os_api_ref/__init__.py @@ -133,12 +133,14 @@ class RestExpandAllDirective(Directive): node = rest_expand_all() max_ver = app.config.os_api_ref_max_microversion min_ver = app.config.os_api_ref_min_microversion + releases = app.config.os_api_ref_release_microversions node['major'] = None try: if max_ver.split('.')[0] == min_ver.split('.')[0]: node['max_ver'] = int(max_ver.split('.')[1]) node['min_ver'] = int(min_ver.split('.')[1]) node['major'] = int(max_ver.split('.')[0]) + node['releases'] = releases except ValueError: # TODO(sdague): warn that we're ignoring this all pass @@ -510,7 +512,9 @@ def rest_expand_all_html(self, node): tmpl = """
%(extra_js)s -
%(selector)s
+
+%(selector)s +
""" + node.setdefault('selector', "") + node.setdefault('extra_js', "") + if node['major']: - selector = """ -
- \n""" - for x in range(node['min_ver'], node['max_ver'] + 1): - selector += ('\n' % (node['major'], x)) - selector += "
" - node['selector'] = selector - node['extra_js'] = ("") % ( - node['max_ver'], - node['min_ver']) - else: - node['selector'] = "" - node['extra_js'] = "" + node['selector'], node['extra_js'] = create_mv_selector(node) self.body.append(tmpl % node) raise nodes.SkipNode +def create_mv_selector(node): + + mv_list = '' + + for x in range(node['min_ver'], node['max_ver'] + 1): + mv_list += build_mv_item(node['major'], x, node['releases']) + + selector_tmpl = """ +
+
+ + +
+
+""" + + js_tmpl = """ + +""" + + selector_content = { + 'mv_list': mv_list + } + + js_content = { + 'min': node['min_ver'], + 'max': node['max_ver'] + } + + return selector_tmpl % selector_content, js_tmpl % js_content + + +def build_mv_item(major, micro, releases): + version = "%d.%d" % (major, micro) + if version in releases: + return '' % ( + version, version, releases[version].capitalize()) + else: + return '' % (version, version) + + def resolve_rest_references(app, doctree): for node in doctree.traverse(): if isinstance(node, rest_method): @@ -574,12 +614,11 @@ def resolve_rest_references(app, doctree): def copy_assets(app, exception): - assets = ('api-site.css', 'api-site.js') + assets = ('api-site.css', 'api-site.js', 'combobox.js') fonts = ( 'glyphicons-halflings-regular.ttf', 'glyphicons-halflings-regular.woff' ) - if app.builder.name != 'html' or exception: return app.info('Copying assets: %s' % ', '.join(assets)) @@ -597,12 +636,14 @@ def copy_assets(app, exception): def add_assets(app): app.add_stylesheet('api-site.css') app.add_javascript('api-site.js') + app.add_javascript('combobox.js') def setup(app): # Add some config options around microversions app.add_config_value('os_api_ref_max_microversion', '', 'env') app.add_config_value('os_api_ref_min_microversion', '', 'env') + app.add_config_value('os_api_ref_release_microversions', '', 'env') # TODO(sdague): if someone wants to support latex/pdf, or man page # generation using these stanzas, here is where you'd need to # specify content specific renderers. diff --git a/os_api_ref/assets/api-site.css b/os_api_ref/assets/api-site.css index af8fd9f..20a6f35 100644 --- a/os_api_ref/assets/api-site.css +++ b/os_api_ref/assets/api-site.css @@ -165,3 +165,53 @@ div.docs-top-contents { div.endpoint-container{ padding-left: 15px; } + +#expand-all { + margin-top: 23px; +} + +### Combobox Experiment +@media (min-width: 768px) { + .form-search .combobox-container, + .form-inline .combobox-container { + display: inline-block; + margin-bottom: 0; + vertical-align: top; + } + .form-search .combobox-container .input-group-addon, + .form-inline .combobox-container .input-group-addon { + width: auto; + } +} +.combobox-selected .caret { + display: none; +} +/* :not doesn't work in IE8 */ +.combobox-container:not(.combobox-selected) .glyphicon-remove { + display: none; +} +.typeahead-long { + max-height: 300px; + overflow-y: auto; +} +.control-group.error .combobox-container .add-on { + color: #B94A48; + border-color: #B94A48; +} +.control-group.error .combobox-container .caret { + border-top-color: #B94A48; +} +.control-group.warning .combobox-container .add-on { + color: #C09853; + border-color: #C09853; +} +.control-group.warning .combobox-container .caret { + border-top-color: #C09853; +} +.control-group.success .combobox-container .add-on { + color: #468847; + border-color: #468847; +} +.control-group.success .combobox-container .caret { + border-top-color: #468847; +} diff --git a/os_api_ref/assets/api-site.js b/os_api_ref/assets/api-site.js index a968be1..9c7a451 100644 --- a/os_api_ref/assets/api-site.js +++ b/os_api_ref/assets/api-site.js @@ -1,6 +1,3 @@ -var os_min_mv = 1; -var os_max_mv = 1; - (function() { // the list of expanded element ids var expanded = []; @@ -163,4 +160,19 @@ var os_max_mv = 1; $('[class^=rp_min_ver]').show(400); $('[class^=rp_max_ver]').show(400); } + + + $(document).ready(function(){ + $('#mv_select').combobox({appendId: '-visable'}); + $('#mv_select').on('change', function() { + var version = this.value; + if (version == "") { + reset_microversion(); + } else { + set_microversion(version); + } + }); + }); + + })(); diff --git a/os_api_ref/assets/combobox.js b/os_api_ref/assets/combobox.js new file mode 100644 index 0000000..d69a7fe --- /dev/null +++ b/os_api_ref/assets/combobox.js @@ -0,0 +1,462 @@ +/* ============================================================= + * bootstrap-combobox.js v1.1.7 + * ============================================================= + * Copyright 2012 Daniel Farrell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + +!function( $ ) { + + "use strict"; + + /* COMBOBOX PUBLIC CLASS DEFINITION + * ================================ */ + + var Combobox = function ( element, options ) { + this.options = $.extend({}, $.fn.combobox.defaults, options); + this.template = this.options.template || this.template + this.$source = $(element); + this.$container = this.setup(); + this.$element = this.$container.find('input[type=text]'); + this.$target = this.$container.find('input[type=hidden]'); + this.$button = this.$container.find('.dropdown-toggle'); + this.$menu = $(this.options.menu).appendTo('body'); + this.matcher = this.options.matcher || this.matcher; + this.sorter = this.options.sorter || this.sorter; + this.highlighter = this.options.highlighter || this.highlighter; + this.shown = false; + this.selected = false; + this.refresh(); + this.transferAttributes(); + this.listen(); + }; + + Combobox.prototype = { + + constructor: Combobox + + , setup: function () { + var combobox = $(this.template()); + this.$source.before(combobox); + this.$source.hide(); + return combobox; + } + + , disable: function() { + this.$element.prop('disabled', true); + this.$button.attr('disabled', true); + this.disabled = true; + this.$container.addClass('combobox-disabled'); + } + + , enable: function() { + this.$element.prop('disabled', false); + this.$button.attr('disabled', false); + this.disabled = false; + this.$container.removeClass('combobox-disabled'); + } + , parse: function () { + var that = this + , map = {} + , source = [] + , selected = false + , selectedValue = ''; + this.$source.find('option').each(function() { + var option = $(this); + if (option.val() === '') { + that.options.placeholder = option.text(); + return; + } + map[option.text()] = option.val(); + source.push(option.text()); + if (option.prop('selected')) { + selected = option.text(); + selectedValue = option.val(); + } + }) + this.map = map; + if (selected) { + this.$element.val(selected); + this.$target.val(selectedValue); + this.$container.addClass('combobox-selected'); + this.selected = true; + } + return source; + } + + , transferAttributes: function() { + this.options.placeholder = this.$source.attr('data-placeholder') || this.options.placeholder + if(this.options.appendId !== "undefined") { + this.$element.attr('id', this.$source.attr('id') + this.options.appendId); + } + this.$element.attr('placeholder', this.options.placeholder) + this.$target.prop('name', this.$source.prop('name')) + this.$target.val(this.$source.val()) + this.$source.removeAttr('name') // Remove from source otherwise form will pass parameter twice. + this.$element.attr('required', this.$source.attr('required')) + this.$element.attr('rel', this.$source.attr('rel')) + this.$element.attr('title', this.$source.attr('title')) + this.$element.attr('class', this.$source.attr('class')) + this.$element.attr('tabindex', this.$source.attr('tabindex')) + this.$source.removeAttr('tabindex') + if (this.$source.attr('disabled')!==undefined) + this.disable(); + } + + , select: function () { + var val = this.$menu.find('.active').attr('data-value'); + this.$element.val(this.updater(val)).trigger('change'); + this.$target.val(this.map[val]).trigger('change'); + this.$source.val(this.map[val]).trigger('change'); + this.$container.addClass('combobox-selected'); + this.selected = true; + return this.hide(); + } + + , updater: function (item) { + return item; + } + + , show: function () { + var pos = $.extend({}, this.$element.position(), { + height: this.$element[0].offsetHeight + }); + + this.$menu + .insertAfter(this.$element) + .css({ + top: pos.top + pos.height + , left: pos.left + }) + .show(); + + $('.dropdown-menu').on('mousedown', $.proxy(this.scrollSafety, this)); + + this.shown = true; + return this; + } + + , hide: function () { + this.$menu.hide(); + $('.dropdown-menu').off('mousedown', $.proxy(this.scrollSafety, this)); + this.$element.on('blur', $.proxy(this.blur, this)); + this.shown = false; + return this; + } + + , lookup: function (event) { + this.query = this.$element.val(); + return this.process(this.source); + } + + , process: function (items) { + var that = this; + + items = $.grep(items, function (item) { + return that.matcher(item); + }) + + items = this.sorter(items); + + if (!items.length) { + return this.shown ? this.hide() : this; + } + + return this.render(items.slice(0, this.options.items)).show(); + } + + , template: function() { + if (this.options.bsVersion == '2') { + return '
' + } else { + return '
' + } + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()); + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item; + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) {beginswith.push(item);} + else if (~item.indexOf(this.query)) {caseSensitive.push(item);} + else {caseInsensitive.push(item);} + } + + return beginswith.concat(caseSensitive, caseInsensitive); + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '' + match + ''; + }) + } + + , render: function (items) { + var that = this; + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item); + i.find('a').html(that.highlighter(item)); + return i[0]; + }) + + items.first().addClass('active'); + this.$menu.html(items); + return this; + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next(); + + if (!next.length) { + next = $(this.$menu.find('li')[0]); + } + + next.addClass('active'); + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev(); + + if (!prev.length) { + prev = this.$menu.find('li').last(); + } + + prev.addClass('active'); + } + + , toggle: function () { + if (!this.disabled) { + if (this.$container.hasClass('combobox-selected')) { + this.clearTarget(); + this.triggerChange(); + this.clearElement(); + } else { + if (this.shown) { + this.hide(); + } else { + this.clearElement(); + this.lookup(); + } + } + } + } + + , scrollSafety: function(e) { + if (e.target.tagName == 'UL') { + this.$element.off('blur'); + } + } + , clearElement: function () { + this.$element.val('').focus(); + } + + , clearTarget: function () { + this.$source.val(''); + this.$target.val(''); + this.$container.removeClass('combobox-selected'); + this.selected = false; + } + + , triggerChange: function () { + this.$source.trigger('change'); + } + + , refresh: function () { + this.source = this.parse(); + this.options.items = this.source.length; + } + + , listen: function () { + this.$element + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)); + + if (this.eventSupported('keydown')) { + this.$element.on('keydown', $.proxy(this.keydown, this)); + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + .on('mouseleave', 'li', $.proxy(this.mouseleave, this)); + + this.$button + .on('click', $.proxy(this.toggle, this)); + } + + , eventSupported: function(eventName) { + var isSupported = eventName in this.$element; + if (!isSupported) { + this.$element.setAttribute(eventName, 'return;'); + isSupported = typeof this.$element[eventName] === 'function'; + } + return isSupported; + } + + , move: function (e) { + if (!this.shown) {return;} + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault(); + break; + + case 38: // up arrow + e.preventDefault(); + this.prev(); + this.fixMenuScroll(); + break; + + case 40: // down arrow + e.preventDefault(); + this.next(); + this.fixMenuScroll(); + break; + } + + e.stopPropagation(); + } + + , fixMenuScroll: function(){ + var active = this.$menu.find('.active'); + if(active.length){ + var top = active.position().top; + var bottom = top + active.height(); + var scrollTop = this.$menu.scrollTop(); + var menuHeight = this.$menu.height(); + if(bottom > menuHeight){ + this.$menu.scrollTop(scrollTop + bottom - menuHeight); + } else if(top < 0){ + this.$menu.scrollTop(scrollTop + top); + } + } + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]); + this.move(e); + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) {return;} + this.move(e); + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + if (!this.shown){ + this.toggle(); + } + break; + case 39: // right arrow + case 38: // up arrow + case 37: // left arrow + case 36: // home + case 35: // end + case 16: // shift + case 17: // ctrl + case 18: // alt + break; + + case 9: // tab + case 13: // enter + if (!this.shown) {return;} + this.select(); + break; + + case 27: // escape + if (!this.shown) {return;} + this.hide(); + break; + + default: + this.clearTarget(); + this.lookup(); + } + + e.stopPropagation(); + e.preventDefault(); + } + + , focus: function (e) { + this.focused = true; + } + + , blur: function (e) { + var that = this; + this.focused = false; + var val = this.$element.val(); + if (!this.selected && val !== '' ) { + this.$element.val(''); + this.$source.val('').trigger('change'); + this.$target.val('').trigger('change'); + } + if (!this.mousedover && this.shown) {setTimeout(function () { that.hide(); }, 200);} + } + + , click: function (e) { + e.stopPropagation(); + e.preventDefault(); + this.select(); + this.$element.focus(); + } + + , mouseenter: function (e) { + this.mousedover = true; + this.$menu.find('.active').removeClass('active'); + $(e.currentTarget).addClass('active'); + } + + , mouseleave: function (e) { + this.mousedover = false; + } + }; + + /* COMBOBOX PLUGIN DEFINITION + * =========================== */ + $.fn.combobox = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('combobox') + , options = typeof option == 'object' && option; + if(!data) {$this.data('combobox', (data = new Combobox(this, options)));} + if (typeof option == 'string') {data[option]();} + }); + }; + + $.fn.combobox.defaults = { + bsVersion: '3' + , menu: '' + , item: '
  • ' + }; + + $.fn.combobox.Constructor = Combobox; + +}( window.jQuery ); diff --git a/os_api_ref/tests/test_microversions.py b/os_api_ref/tests/test_microversions.py index 16bf27d..7a4f0de 100644 --- a/os_api_ref/tests/test_microversions.py +++ b/os_api_ref/tests/test_microversions.py @@ -100,40 +100,7 @@ class TestMicroversions(base.TestCase): def test_mv_selector(self): - button_selectors = \ - """
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    """ + button_selectors = '' # noqa self.assertIn(button_selectors, self.content) def test_js_declares(self):