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):