diff options
| author | Zac Echola <[email protected]> | 2015-04-28 09:45:16 -0500 |
|---|---|---|
| committer | Zac Echola <[email protected]> | 2015-04-28 09:45:16 -0500 |
| commit | 407e8dc9d8d8e28d456a37d9d69e1d3c89d6fbb4 (patch) | |
| tree | d841221f51dd9852fbce1c562127158d35b8812d /js | |
| parent | e46c998316359f3368d80736d7eb750d85dc4da5 (diff) | |
| parent | e6438592e9ea28324ba1a4344e724cab28032208 (diff) | |
| download | bootstrap-407e8dc9d8d8e28d456a37d9d69e1d3c89d6fbb4.tar.xz bootstrap-407e8dc9d8d8e28d456a37d9d69e1d3c89d6fbb4.zip | |
Merge branch 'master' of https://github.com/twbs/bootstrap into list_group_buttons
Diffstat (limited to 'js')
| -rw-r--r-- | js/button.js | 8 | ||||
| -rw-r--r-- | js/dropdown.js | 79 | ||||
| -rw-r--r-- | js/tab.js | 2 | ||||
| -rw-r--r-- | js/tests/unit/button.js | 4 | ||||
| -rw-r--r-- | js/tests/unit/popover.js | 28 | ||||
| -rw-r--r-- | js/tests/unit/tooltip.js | 128 | ||||
| -rw-r--r-- | js/tooltip.js | 36 |
7 files changed, 214 insertions, 71 deletions
diff --git a/js/button.js b/js/button.js index 7c7c021f9..8716aff4f 100644 --- a/js/button.js +++ b/js/button.js @@ -31,7 +31,7 @@ var val = $el.is('input') ? 'val' : 'html' var data = $el.data() - state = state + 'Text' + state += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) @@ -56,8 +56,8 @@ if ($parent.length) { var $input = this.$element.find('input') if ($input.prop('type') == 'radio') { - if ($input.prop('checked') && this.$element.hasClass('active')) changed = false - else $parent.find('.active').removeClass('active') + if ($input.prop('checked')) changed = false + if (!$input.prop('checked') || !this.$element.hasClass('active')) $parent.find('.active').removeClass('active') } if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') } else { @@ -107,7 +107,7 @@ var $btn = $(e.target) if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') Plugin.call($btn, 'toggle') - e.preventDefault() + if (!$(e.target).is('input[type="radio"]')) e.preventDefault() }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) diff --git a/js/dropdown.js b/js/dropdown.js index 1fbf74a31..8fd86a025 100644 --- a/js/dropdown.js +++ b/js/dropdown.js @@ -21,6 +21,40 @@ Dropdown.VERSION = '3.3.4' + 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() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) + }) + } + Dropdown.prototype.toggle = function (e) { var $this = $(this) @@ -70,7 +104,7 @@ var $parent = getParent($this) var isActive = $parent.hasClass('open') - if ((!isActive && e.which != 27) || (isActive && e.which == 27)) { + if (!isActive && e.which != 27 || isActive && e.which == 27) { if (e.which == 27) $parent.find(toggle).trigger('focus') return $this.trigger('click') } @@ -82,47 +116,13 @@ var index = $items.index(e.target) - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 $items.eq(index).trigger('focus') } - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) - }) - } - - 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 // ========================== @@ -160,7 +160,6 @@ .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) }(jQuery); @@ -65,7 +65,7 @@ var $active = container.find('> .active') var transition = callback && $.support.transition - && (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length) + && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) function next() { $active diff --git a/js/tests/unit/button.js b/js/tests/unit/button.js index 02312ebaf..ed7b248d4 100644 --- a/js/tests/unit/button.js +++ b/js/tests/unit/button.js @@ -167,13 +167,13 @@ $(function () { assert.ok(!$btn2.find('input').prop('checked'), 'btn2 is not checked') $btn2.find('input').trigger('click') assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class') - assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is checked') + assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked') assert.ok($btn2.hasClass('active'), 'btn2 has active class') assert.ok($btn2.find('input').prop('checked'), 'btn2 is checked') $btn2.find('input').trigger('click') // clicking an already checked radio should not un-check it assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class') - assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is checked') + assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked') assert.ok($btn2.hasClass('active'), 'btn2 has active class') assert.ok($btn2.find('input').prop('checked'), 'btn2 is checked') }) diff --git a/js/tests/unit/popover.js b/js/tests/unit/popover.js index fbabab9e3..a25df3a58 100644 --- a/js/tests/unit/popover.js +++ b/js/tests/unit/popover.js @@ -259,4 +259,32 @@ $(function () { assert.strictEqual($popover.data('bs.popover'), undefined, 'should not initialize the popover') }) + QUnit.test('should throw an error when template contains multiple top-level elements', function (assert) { + assert.expect(1) + assert.throws(function () { + $('<span data-toggle="popover" data-title="some title" data-content="some content">some text</span>') + .appendTo('#qunit-fixture') + .bootstrapPopover({ template: '<div>Foo</div><div>Bar</div>' }) + .bootstrapPopover('show') + }, new Error('popover `template` option must consist of exactly 1 top-level element!')) + }) + + QUnit.test('should fire inserted event', function (assert) { + assert.expect(2) + var done = assert.async() + + $('<a href="#">@Johann-S</a>') + .appendTo('#qunit-fixture') + .on('inserted.bs.popover', function () { + assert.notEqual($('.popover').length, 0, 'popover was inserted') + assert.ok(true, 'inserted event fired') + done() + }) + .bootstrapPopover({ + title: 'Test', + content: 'Test' + }) + .bootstrapPopover('show') + }) + }) diff --git a/js/tests/unit/tooltip.js b/js/tests/unit/tooltip.js index 57a59db18..bdcf0bbd0 100644 --- a/js/tests/unit/tooltip.js +++ b/js/tests/unit/tooltip.js @@ -139,6 +139,20 @@ $(function () { .bootstrapTooltip('show') }) + QUnit.test('should fire inserted event', function (assert) { + assert.expect(2) + var done = assert.async() + + $('<div title="tooltip title"/>') + .appendTo('#qunit-fixture') + .on('inserted.bs.tooltip', function () { + assert.notEqual($('.tooltip').length, 0, 'tooltip was inserted') + assert.ok(true, 'inserted event fired') + done() + }) + .bootstrapTooltip('show') + }) + QUnit.test('should fire shown event', function (assert) { assert.expect(1) var done = assert.async() @@ -362,23 +376,19 @@ $(function () { assert.strictEqual($('.tooltip').length, 0, 'tooltip removed from dom') }) - QUnit.test('should be placed dynamically with the dynamic placement option', function (assert) { + QUnit.test('should be placed dynamically to viewport with the dynamic placement option', function (assert) { assert.expect(6) - var $style = $('<style> a[rel="tooltip"] { display: inline-block; position: absolute; } </style>') + var $style = $('<style> div[rel="tooltip"] { position: absolute; } #qunit-fixture { top: inherit; left: inherit } </style>').appendTo('head') var $container = $('<div/>') .css({ - position: 'absolute', - overflow: 'hidden', - width: 600, - height: 400, - top: 0, - left: 0 + position: 'relative', + height: '100%' }) - .appendTo(document.body) + .appendTo('#qunit-fixture') var $topTooltip = $('<div style="left: 0; top: 0;" rel="tooltip" title="Top tooltip">Top Dynamic Tooltip</div>') .appendTo($container) - .bootstrapTooltip({ placement: 'auto' }) + .bootstrapTooltip({ placement: 'auto', viewport: '#qunit-fixture' }) $topTooltip.bootstrapTooltip('show') assert.ok($('.tooltip').is('.bottom'), 'top positioned tooltip is dynamically positioned to bottom') @@ -388,7 +398,7 @@ $(function () { var $rightTooltip = $('<div style="right: 0;" rel="tooltip" title="Right tooltip">Right Dynamic Tooltip</div>') .appendTo($container) - .bootstrapTooltip({ placement: 'right auto' }) + .bootstrapTooltip({ placement: 'right auto', viewport: '#qunit-fixture' }) $rightTooltip.bootstrapTooltip('show') assert.ok($('.tooltip').is('.left'), 'right positioned tooltip is dynamically positioned left') @@ -398,7 +408,7 @@ $(function () { var $leftTooltip = $('<div style="left: 0;" rel="tooltip" title="Left tooltip">Left Dynamic Tooltip</div>') .appendTo($container) - .bootstrapTooltip({ placement: 'auto left' }) + .bootstrapTooltip({ placement: 'auto left', viewport: '#qunit-fixture' }) $leftTooltip.bootstrapTooltip('show') assert.ok($('.tooltip').is('.right'), 'left positioned tooltip is dynamically positioned right') @@ -436,6 +446,31 @@ $(function () { $styles.remove() }) + QUnit.test('should position tip on top if viewport has enough space and is not parent', function (assert) { + assert.expect(2) + var styles = '<style>' + + '#section { height: 300px; border: 1px solid red; margin-top: 100px; }' + + 'div[rel="tooltip"] { width: 150px; border: 1px solid blue; }' + + '</style>' + var $styles = $(styles).appendTo('head') + + var $container = $('<div id="section"/>').appendTo('#qunit-fixture') + var $target = $('<div rel="tooltip" title="tip"/>') + .appendTo($container) + .bootstrapTooltip({ + placement: 'auto top', + viewport: '#qunit-fixture' + }) + + $target.bootstrapTooltip('show') + assert.ok($('.tooltip').is('.top'), 'top positioned tooltip is dynamically positioned to top') + + $target.bootstrapTooltip('hide') + assert.strictEqual($('.tooltip').length, 0, 'tooltip removed from dom') + + $styles.remove() + }) + QUnit.test('should position tip on bottom if the tip\'s dimension exceeds the viewport area and placement is "auto top"', function (assert) { assert.expect(2) var styles = '<style>' @@ -719,6 +754,65 @@ $(function () { $styles.remove() }) + QUnit.test('should get viewport element from function', function (assert) { + assert.expect(3) + var styles = '<style>' + + '.tooltip, .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; }' + + '.container-viewport { position: absolute; top: 50px; left: 60px; width: 300px; height: 300px; }' + + 'a[rel="tooltip"] { position: fixed; }' + + '</style>' + var $styles = $(styles).appendTo('head') + + var $container = $('<div class="container-viewport"/>').appendTo(document.body) + var $target = $('<a href="#" rel="tooltip" title="tip" style="top: 50px; left: 350px;"/>').appendTo($container) + $target + .bootstrapTooltip({ + placement: 'bottom', + viewport: function ($element) { + assert.strictEqual($element[0], $target[0], 'viewport function was passed target as argument') + return ($element.closest('.container-viewport')) + } + }) + + $target.bootstrapTooltip('show') + var $tooltip = $container.find('.tooltip') + assert.strictEqual(Math.round($tooltip.offset().left), Math.round(60 + $container.width() - $tooltip[0].offsetWidth)) + + $target.bootstrapTooltip('hide') + assert.strictEqual($('.tooltip').length, 0, 'tooltip removed from dom') + + $container.remove() + $styles.remove() + }) + + QUnit.test('should not misplace the tip when the right edge offset is greater or equal than the viewport width', function (assert) { + assert.expect(2) + var styles = '<style>' + + '.tooltip, .tooltip *, .tooltip *:before, .tooltip *:after { box-sizing: border-box; }' + + '.container-viewport, .container-viewport *, .container-viewport *:before, .container-viewport *:after { box-sizing: border-box; }' + + '.tooltip, .tooltip .tooltip-inner { width: 50px; height: 50px; max-width: none; background: red; }' + + '.container-viewport { padding: 100px; margin-left: 100px; width: 100px; }' + + '</style>' + var $styles = $(styles).appendTo('head') + + var $container = $('<div class="container-viewport"/>').appendTo(document.body) + var $target = $('<a href="#" rel="tooltip" title="tip">foobar</a>') + .appendTo($container) + .bootstrapTooltip({ + viewport: '.container-viewport' + }) + + $target.bootstrapTooltip('show') + var $tooltip = $container.find('.tooltip') + assert.strictEqual(Math.round($tooltip.offset().left), Math.round($target.position().left + $target.width() / 2 - $tooltip[0].offsetWidth / 2)) + + $target.bootstrapTooltip('hide') + assert.strictEqual($('.tooltip').length, 0, 'tooltip removed from dom') + + $container.remove() + $styles.remove() + }) + QUnit.test('should not error when trying to show an auto-placed tooltip that has been removed from the dom', function (assert) { assert.expect(1) var passed = true @@ -1180,4 +1274,14 @@ $(function () { assert.strictEqual($tooltip.data('bs.tooltip'), undefined, 'should not initialize the tooltip') }) + QUnit.test('should throw an error when template contains multiple top-level elements', function (assert) { + assert.expect(1) + assert.throws(function () { + $('<a href="#" data-toggle="tooltip" title="Another tooltip"></a>') + .appendTo('#qunit-fixture') + .bootstrapTooltip({ template: '<div>Foo</div><div>Bar</div>' }) + .bootstrapTooltip('show') + }, new Error('tooltip `template` option must consist of exactly 1 top-level element!')) + }) + }) diff --git a/js/tooltip.js b/js/tooltip.js index 27367880f..af1483aba 100644 --- a/js/tooltip.js +++ b/js/tooltip.js @@ -50,7 +50,7 @@ this.type = type this.$element = $(element) this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport) + this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') @@ -185,6 +185,7 @@ .data('bs.' + this.type, this) this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + this.$element.trigger('inserted.bs.' + this.type) var pos = this.getPosition() var actualWidth = $tip[0].offsetWidth @@ -192,13 +193,12 @@ if (autoPlace) { var orgPlacement = placement - var $container = this.options.container ? $(this.options.container) : this.$element.parent() - var containerDim = this.getPosition($container) + var viewportDim = this.getPosition(this.$viewport) - placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' : + placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement $tip @@ -239,8 +239,8 @@ if (isNaN(marginTop)) marginTop = 0 if (isNaN(marginLeft)) marginLeft = 0 - offset.top = offset.top + marginTop - offset.left = offset.left + marginLeft + offset.top += marginTop + offset.left += marginLeft // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 @@ -322,7 +322,7 @@ Tooltip.prototype.fixTitle = function () { var $e = this.$element - if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') { + if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') } } @@ -377,7 +377,7 @@ var rightEdgeOffset = pos.left + viewportPadding + actualWidth if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow + } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset } } @@ -403,7 +403,13 @@ } Tooltip.prototype.tip = function () { - return (this.$tip = this.$tip || $(this.options.template)) + if (!this.$tip) { + this.$tip = $(this.options.template) + if (this.$tip.length != 1) { + throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') + } + } + return this.$tip } Tooltip.prototype.arrow = function () { @@ -440,6 +446,12 @@ clearTimeout(this.timeout) this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type) + if (that.$tip) { + that.$tip.detach() + } + that.$tip = null + that.$arrow = null + that.$viewport = null }) } |
