From 834220ea20ce5b7cd31edfb624a28b4bf8b29a6a Mon Sep 17 00:00:00 2001 From: fat Date: Sat, 3 Jan 2015 13:58:44 -0800 Subject: bootstrap onto closure --- js/alert.js | 280 ++++-- js/button.js | 275 +++-- js/carousel.js | 669 ++++++++++--- js/collapse.js | 526 +++++++--- js/dropdown.js | 375 +++++-- js/externs/bootstrap.js | 6 + js/externs/jQuery.js | 2159 ++++++++++++++++++++++++++++++++++++++++ js/hover.js | 4 - js/modal.js | 752 +++++++++----- js/popover.js | 320 ++++-- js/scrollspy.js | 427 +++++--- js/tab.js | 395 +++++--- js/tests/closure.html | 83 ++ js/tests/index.html | 4 +- js/tests/unit/button.js | 55 - js/tests/unit/carousel.js | 9 +- js/tests/unit/collapse.js | 2 +- js/tests/unit/modal.js | 2 +- js/tests/unit/tooltip.js | 22 +- js/tests/visual/affix.html | 299 ------ js/tests/visual/alert.html | 2 +- js/tests/visual/button.html | 6 +- js/tests/visual/carousel.html | 2 +- js/tests/visual/collapse.html | 2 +- js/tests/visual/dropdown.html | 2 +- js/tests/visual/modal.html | 2 +- js/tests/visual/popover.html | 2 +- js/tests/visual/scrollspy.html | 2 +- js/tests/visual/tab.html | 2 +- js/tests/visual/tooltip.html | 2 +- js/tooltip.js | 1086 +++++++++++++------- js/util.js | 165 +++ 32 files changed, 6087 insertions(+), 1852 deletions(-) create mode 100644 js/externs/bootstrap.js create mode 100644 js/externs/jQuery.js delete mode 100644 js/hover.js create mode 100644 js/tests/closure.html delete mode 100644 js/tests/visual/affix.html create mode 100644 js/util.js (limited to 'js') diff --git a/js/alert.js b/js/alert.js index e8212caf0..38bce06a4 100644 --- a/js/alert.js +++ b/js/alert.js @@ -1,94 +1,260 @@ -/* ======================================================================== - * Bootstrap: alert.js v3.3.2 +/** ======================================================================= + * Bootstrap: alert.js v4.0.0 * http://getbootstrap.com/javascript/#alerts * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - + * ======================================================================== + * @fileoverview - Bootstrap's generic alert component. Add dismiss + * functionality to all alert messages with this plugin. + * + * Public Methods & Properties: + * + * + $.alert + * + $.alert.noConflict + * + $.alert.Constructor + * + $.alert.Constructor.VERSION + * + $.alert.Constructor.prototype.close + * + * ======================================================================== + */ -+function ($) { - 'use strict'; +'use strict'; - // ALERT CLASS DEFINITION - // ====================== - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) +/** + * Our Alert class. + * @param {Element=} opt_element + * @constructor + */ +var Alert = function (opt_element) { + if (opt_element) { + $(opt_element).on('click', Alert._DISMISS_SELECTOR, Alert._handleDismiss(this)) } +} - Alert.VERSION = '3.3.2' - Alert.TRANSITION_DURATION = 150 +/** + * @const + * @type {string} + */ +Alert['VERSION'] = '4.0.0' - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } +/** + * @const + * @type {string} + * @private + */ +Alert._NAME = 'alert' - var $parent = $(selector) - if (e) e.preventDefault() +/** + * @const + * @type {string} + * @private + */ +Alert._DATA_KEY = 'bs.alert' + + +/** + * @const + * @type {string} + * @private + */ +Alert._DISMISS_SELECTOR = '[data-dismiss="alert"]' + + +/** + * @const + * @type {number} + * @private + */ +Alert._TRANSITION_DURATION = 150 - if (!$parent.length) { - $parent = $this.closest('.alert') - } - $parent.trigger(e = $.Event('close.bs.alert')) +/** + * @const + * @type {Function} + * @private + */ +Alert._JQUERY_NO_CONFLICT = $.fn[Alert._NAME] - if (e.isDefaultPrevented()) return - $parent.removeClass('in') +/** + * @const + * @enum {string} + * @private + */ +Alert._Event = { + CLOSE : 'close.bs.alert', + CLOSED : 'closed.bs.alert' +} - function removeElement() { - // detach from parent, fire event then clean up data - $parent.detach().trigger('closed.bs.alert').remove() + +/** + * @const + * @enum {string} + * @private + */ +Alert._ClassName = { + ALERT : 'alert', + FADE : 'fade', + IN : 'in' +} + + +/** + * Provides the jQuery Interface for the alert component. + * @param {string=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Alert._jQueryInterface = function (opt_config) { + return this.each(function () { + var $this = $(this) + var data = $this.data(Alert._DATA_KEY) + + if (!data) { + data = new Alert(this) + $this.data(Alert._DATA_KEY, data) } - $.support.transition && $parent.hasClass('fade') ? - $parent - .one('bsTransitionEnd', removeElement) - .emulateTransitionEnd(Alert.TRANSITION_DURATION) : - removeElement() + if (opt_config === 'close') { + data[opt_config](this) + } + }) +} + + +/** + * Close the alert component + * @param {Alert} alertInstance + * @return {Function} + * @private + */ +Alert._handleDismiss = function (alertInstance) { + return function (event) { + if (event) { + event.preventDefault() + } + + alertInstance['close'](this) } +} + + +/** + * Close the alert component + * @param {Element} element + */ +Alert.prototype['close'] = function (element) { + var rootElement = this._getRootElement(element) + var customEvent = this._triggerCloseEvent(rootElement) + + if (customEvent.isDefaultPrevented()) return + this._removeElement(rootElement) +} - // ALERT PLUGIN DEFINITION - // ======================= - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') +/** + * Tries to get the alert's root element + * @return {Element} + * @private + */ +Alert.prototype._getRootElement = function (element) { + var parent = false + var selector = Bootstrap.getSelectorFromElement(element) - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) + if (selector) { + parent = $(selector)[0] } - var old = $.fn.alert + if (!parent) { + parent = $(element).closest('.' + Alert._ClassName.ALERT)[0] + } + + return parent +} - $.fn.alert = Plugin - $.fn.alert.Constructor = Alert +/** + * Trigger close event on element + * @return {$.Event} + * @private + */ +Alert.prototype._triggerCloseEvent = function (element) { + var closeEvent = $.Event(Alert._Event.CLOSE) + $(element).trigger(closeEvent) + return closeEvent +} - // ALERT NO CONFLICT - // ================= - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this +/** + * Trigger closed event and remove element from dom + * @private + */ +Alert.prototype._removeElement = function (element) { + $(element).removeClass(Alert._ClassName.IN) + + if (!Bootstrap.transition || !$(element).hasClass(Alert._ClassName.FADE)) { + this._destroyElement(element) + return } + $(element) + .one(Bootstrap.TRANSITION_END, this._destroyElement.bind(this, element)) + .emulateTransitionEnd(Alert._TRANSITION_DURATION) +} + + +/** + * clean up any lingering jquery data and kill element + * @private + */ +Alert.prototype._destroyElement = function (element) { + $(element) + .detach() + .trigger(Alert._Event.CLOSED) + .remove() +} + + +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ + +/** + * @const + * @type {Function} + */ +$.fn[Alert._NAME] = Alert._jQueryInterface + + +/** + * @const + * @type {Function} + */ +$.fn[Alert._NAME]['Constructor'] = Alert + + +/** + * @return {Function} + */ +$.fn[Alert._NAME]['noConflict'] = function () { + $.fn[Alert._NAME] = Alert._JQUERY_NO_CONFLICT + return Alert._jQueryInterface +} - // ALERT DATA-API - // ============== - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ -}(jQuery); +$(document).on('click.bs.alert.data-api', Alert._DISMISS_SELECTOR, Alert._handleDismiss(new Alert)) diff --git a/js/button.js b/js/button.js index 4d5690173..8ee2d6b08 100644 --- a/js/button.js +++ b/js/button.js @@ -1,116 +1,207 @@ -/* ======================================================================== - * Bootstrap: button.js v3.3.2 +/** ======================================================================= + * Bootstrap: button.js v4.0.0 * http://getbootstrap.com/javascript/#buttons * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - this.isLoading = false - } - - Button.VERSION = '3.3.2' - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state = state + 'Text' - - if (data.resetText == null) $el.data('resetText', $el[val]()) - - // push to event loop to allow forms to submit - setTimeout($.proxy(function () { - $el[val](data[state] == null ? this.options[state] : data[state]) + * ======================================================================== + * @fileoverview - Bootstrap's generic button component. + * + * Note (@fat): Deprecated "setState" – imo, better solutions for managing a + * buttons state should exist outside this plugin. + * + * Public Methods & Properties: + * + * + $.button + * + $.button.noConflict + * + $.button.Constructor + * + $.button.Constructor.VERSION + * + $.button.Constructor.prototype.toggle + * + * ======================================================================== + */ + +'use strict'; + + +/** + * Our Button class. + * @param {Element!} element + * @constructor + */ +var Button = function (element) { + + /** @private {Element} */ + this._element = element + +} + + +/** + * @const + * @type {string} + */ +Button['VERSION'] = '4.0.0' + + +/** + * @const + * @type {string} + * @private + */ +Button._NAME = 'button' + + +/** + * @const + * @type {string} + * @private + */ +Button._DATA_KEY = 'bs.button' + + +/** + * @const + * @type {Function} + * @private + */ +Button._JQUERY_NO_CONFLICT = $.fn[Button._NAME] + + +/** + * @const + * @enum {string} + * @private + */ +Button._ClassName = { + ACTIVE : 'active', + BUTTON : 'btn', + FOCUS : 'focus' +} + + +/** + * @const + * @enum {string} + * @private + */ +Button._Selector = { + DATA_TOGGLE_CARROT : '[data-toggle^="button"]', + DATA_TOGGLE : '[data-toggle="buttons"]', + INPUT : 'input', + ACTIVE : '.active', + BUTTON : '.btn' +} + + +/** + * Provides the jQuery Interface for the Button component. + * @param {string=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Button._jQueryInterface = function (opt_config) { + return this.each(function () { + var data = $(this).data(Button._DATA_KEY) + + if (!data) { + data = new Button(this) + $(this).data(Button._DATA_KEY, data) + } - if (state == 'loadingText') { - this.isLoading = true - $el.addClass(d).attr(d, d) - } else if (this.isLoading) { - this.isLoading = false - $el.removeClass(d).removeAttr(d) + if (opt_config === 'toggle') { + data[opt_config]() + } + }) +} + + +/** + * Toggle's the button active state + */ +Button.prototype['toggle'] = function () { + var triggerChangeEvent = true + var rootElement = $(this._element).closest(Button._Selector.DATA_TOGGLE)[0] + + if (rootElement) { + var input = $(this._element).find(Button._Selector.INPUT)[0] + if (input) { + if (input.type == 'radio') { + if (input.checked && $(this._element).hasClass(Button._ClassName.ACTIVE)) { + triggerChangeEvent = false + } else { + var activeElement = $(rootElement).find(Button._Selector.ACTIVE)[0] + if (activeElement) { + $(activeElement).removeClass(Button._ClassName.ACTIVE) + } + } } - }, this), 0) - } - - Button.prototype.toggle = function () { - var changed = true - var $parent = this.$element.closest('[data-toggle="buttons"]') - 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 (triggerChangeEvent) { + input.checked = !$(this._element).hasClass(Button._ClassName.ACTIVE) + $(this._element).trigger('change') } - if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') - } else { - this.$element.attr('aria-pressed', !this.$element.hasClass('active')) } + } else { + this._element.setAttribute('aria-pressed', !$(this._element).hasClass(Button._ClassName.ACTIVE)) + } - if (changed) this.$element.toggleClass('active') + if (triggerChangeEvent) { + $(this._element).toggleClass(Button._ClassName.ACTIVE) } +} - // BUTTON PLUGIN DEFINITION - // ======================== +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option +/** + * @const + * @type {Function} + */ +$.fn[Button._NAME] = Button._jQueryInterface - if (!data) $this.data('bs.button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } +/** + * @const + * @type {Function} + */ +$.fn[Button._NAME]['Constructor'] = Button - var old = $.fn.button - $.fn.button = Plugin - $.fn.button.Constructor = Button +/** + * @const + * @type {Function} + */ +$.fn[Button._NAME]['noConflict'] = function () { + $.fn[Button._NAME] = Button._JQUERY_NO_CONFLICT + return this +} - // BUTTON NO CONFLICT - // ================== +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } +$(document) + .on('click.bs.button.data-api', Button._Selector.DATA_TOGGLE_CARROT, function (event) { + event.preventDefault() + var button = event.target - // BUTTON DATA-API - // =============== - - $(document) - .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - Plugin.call($btn, 'toggle') - 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)) - }) + if (!$(button).hasClass(Button._ClassName.BUTTON)) { + button = $(button).closest(Button._Selector.BUTTON) + } -}(jQuery); + Button._jQueryInterface.call($(button), 'toggle') + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', Button._Selector.DATA_TOGGLE_CARROT, function (event) { + var button = $(event.target).closest(Button._Selector.BUTTON)[0] + $(button).toggleClass(Button._ClassName.FOCUS, /^focus(in)?$/.test(event.type)) + }) diff --git a/js/carousel.js b/js/carousel.js index 8aa04e117..8f33f4860 100644 --- a/js/carousel.js +++ b/js/carousel.js @@ -1,237 +1,576 @@ -/* ======================================================================== - * Bootstrap: carousel.js v3.3.2 +/** ======================================================================= + * Bootstrap: carousel.js v4.0.0 * http://getbootstrap.com/javascript/#carousel * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ + * ======================================================================== + * @fileoverview - Bootstrap's carousel. A slideshow component for cycling + * through elements, like a carousel. Nested carousels are not supported. + * + * Public Methods & Properties: + * + * + $.carousel + * + $.carousel.noConflict + * + $.carousel.Constructor + * + $.carousel.Constructor.VERSION + * + $.carousel.Constructor.Defaults + * + $.carousel.Constructor.Defaults.interval + * + $.carousel.Constructor.Defaults.pause + * + $.carousel.Constructor.Defaults.wrap + * + $.carousel.Constructor.Defaults.keyboard + * + $.carousel.Constructor.Defaults.slide + * + $.carousel.Constructor.prototype.next + * + $.carousel.Constructor.prototype.prev + * + $.carousel.Constructor.prototype.pause + * + $.carousel.Constructor.prototype.cycle + * + * ======================================================================== + */ + +'use strict'; + + +/** + * Our carousel class. + * @param {Element!} element + * @param {Object=} opt_config + * @constructor + */ +var Carousel = function (element, opt_config) { + + /** @private {Element} */ + this._element = $(element)[0] + + /** @private {Element} */ + this._indicatorsElement = $(this._element).find(Carousel._Selector.INDICATORS)[0] + + /** @private {?Object} */ + this._config = opt_config || null + /** @private {boolean} */ + this._isPaused = false -+function ($) { - 'use strict'; + /** @private {boolean} */ + this._isSliding = false + + /** @private {?number} */ + this._interval = null + + /** @private {?Element} */ + this._activeElement = null + + /** @private {?Array} */ + this._items = null + + this._addEventListeners() + +} + + +/** + * @const + * @type {string} + */ +Carousel['VERSION'] = '4.0.0' + + +/** + * @const + * @type {Object} + */ +Carousel['Defaults'] = { + 'interval' : 5000, + 'pause' : 'hover', + 'wrap' : true, + 'keyboard' : true, + 'slide' : false +} + + +/** + * @const + * @type {string} + * @private + */ +Carousel._NAME = 'carousel' + + +/** + * @const + * @type {string} + * @private + */ +Carousel._DATA_KEY = 'bs.carousel' + + +/** + * @const + * @type {number} + * @private + */ +Carousel._TRANSITION_DURATION = 600 + + +/** + * @const + * @enum {string} + * @private + */ +Carousel._Direction = { + NEXT : 'next', + PREVIOUS : 'prev' +} + + +/** + * @const + * @enum {string} + * @private + */ +Carousel._Event = { + SLIDE : 'slide.bs.carousel', + SLID : 'slid.bs.carousel' +} + + +/** + * @const + * @enum {string} + * @private + */ +Carousel._ClassName = { + CAROUSEL : 'carousel', + ACTIVE : 'active', + SLIDE : 'slide', + RIGHT : 'right', + LEFT : 'left', + ITEM : 'carousel-item' +} + + +/** + * @const + * @enum {string} + * @private + */ +Carousel._Selector = { + ACTIVE : '.active', + ACTIVE_ITEM : '.active.carousel-item', + ITEM : '.carousel-item', + NEXT_PREV : '.next, .prev', + INDICATORS : '.carousel-indicators' +} + + +/** + * @const + * @type {Function} + * @private + */ +Carousel._JQUERY_NO_CONFLICT = $.fn[Carousel._NAME] + + +/** + * @param {Object=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Carousel._jQueryInterface = function (opt_config) { + return this.each(function () { + var data = $(this).data(Carousel._DATA_KEY) + var config = $.extend({}, Carousel['Defaults'], $(this).data(), typeof opt_config == 'object' && opt_config) + var action = typeof opt_config == 'string' ? opt_config : config.slide + + if (!data) { + data = new Carousel(this, config) + $(this).data(Carousel._DATA_KEY, data) + } + + if (typeof opt_config == 'number') { + data.to(opt_config) - // CAROUSEL CLASS DEFINITION - // ========================= + } else if (action) { + data[action]() + + } else if (config.interval) { + data['pause']() + data['cycle']() + } + }) +} - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = - this.sliding = - this.interval = - this.$active = - this.$items = null - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) +/** + * Click handler for data api + * @param {Event} event + * @this {Element} + * @private + */ +Carousel._dataApiClickHandler = function (event) { + var selector = Bootstrap.getSelectorFromElement(this) - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + if (!selector) { + return } - Carousel.VERSION = '3.3.2' + var target = $(selector)[0] + + if (!target || !$(target).hasClass(Carousel._ClassName.CAROUSEL)) { + return + } - Carousel.TRANSITION_DURATION = 600 + var config = $.extend({}, $(target).data(), $(this).data()) - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true + var slideIndex = this.getAttribute('data-slide-to') + if (slideIndex) { + config.interval = false } - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } + Carousel._jQueryInterface.call($(target), config) - e.preventDefault() + if (slideIndex) { + $(target).data(Carousel._DATA_KEY).to(slideIndex) } - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) + event.preventDefault() +} - this.interval && clearInterval(this.interval) - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) +/** + * Advance the carousel to the next slide + */ +Carousel.prototype['next'] = function () { + if (!this._isSliding) { + this._slide(Carousel._Direction.NEXT) + } +} + - return this +/** + * Return the carousel to the previous slide + */ +Carousel.prototype['prev'] = function () { + if (!this._isSliding) { + this._slide(Carousel._Direction.PREVIOUS) } +} - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.carousel-item') - return this.$items.index(item || this.$active) + +/** + * Pause the carousel cycle + * @param {Event=} opt_event + */ +Carousel.prototype['pause'] = function (opt_event) { + if (!opt_event) { + this._isPaused = true } - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) + if ($(this._element).find(Carousel._Selector.NEXT_PREV)[0] && Bootstrap.transition) { + $(this._element).trigger(Bootstrap.transition.end) + this['cycle'](true) } - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.carousel-item.active')) + clearInterval(this._interval) + this._interval = null +} + - if (pos > (this.$items.length - 1) || pos < 0) return +/** + * Cycle to the next carousel item + * @param {Event|boolean=} opt_event + */ +Carousel.prototype['cycle'] = function (opt_event) { + if (!opt_event) { + this._isPaused = false + } - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() + if (this._interval) { + clearInterval(this._interval) + this._interval = null + } - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + if (this._config['interval'] && !this._isPaused) { + this._interval = setInterval(this['next'].bind(this), this._config['interval']) } +} - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } +/** + * @return {Object} + */ +Carousel.prototype['getConfig'] = function () { + return this._config +} + - this.interval = clearInterval(this.interval) +/** + * Move active carousel item to specified index + * @param {number} index + */ +Carousel.prototype.to = function (index) { + this._activeElement = $(this._element).find(Carousel._Selector.ACTIVE_ITEM)[0] - return this + var activeIndex = this._getItemIndex(this._activeElement) + + if (index > (this._items.length - 1) || index < 0) { + return } - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') + if (this._isSliding) { + $(this._element).one(Carousel._Event.SLID, function () { this.to(index) }.bind(this)) + return } - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') + if (activeIndex == index) { + this['pause']() + this['cycle']() + return } - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.carousel-item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this + var direction = index > activeIndex ? + Carousel._Direction.NEXT : + Carousel._Direction.PREVIOUS - if ($next.hasClass('active')) return (this.sliding = false) + this._slide(direction, this._items[index]) +} - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - this.sliding = true +/** + * Add event listeners to root element + * @private + */ +Carousel.prototype._addEventListeners = function () { + if (this._config['keyboard']) { + $(this._element).on('keydown.bs.carousel', this._keydown.bind(this)) + } - isCycling && this.pause() + if (this._config['pause'] == 'hover' && !('ontouchstart' in document.documentElement)) { + $(this._element) + .on('mouseenter.bs.carousel', this['pause'].bind(this)) + .on('mouseleave.bs.carousel', this['cycle'].bind(this)) + } +} - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } +/** + * Keydown handler + * @param {Event} event + * @private + */ +Carousel.prototype._keydown = function (event) { + event.preventDefault() - isCycling && this.cycle() + if (/input|textarea/i.test(event.target.tagName)) return - return this + switch (event.which) { + case 37: this['prev'](); break + case 39: this['next'](); break + default: return + } +} + + +/** + * Get item index + * @param {Element} element + * @return {number} + * @private + */ +Carousel.prototype._getItemIndex = function (element) { + this._items = $.makeArray($(element).parent().find(Carousel._Selector.ITEM)) + + return this._items.indexOf(element) +} + + +/** + * Get next displayed item based on direction + * @param {Carousel._Direction} direction + * @param {Element} activeElement + * @return {Element} + * @private + */ +Carousel.prototype._getItemByDirection = function (direction, activeElement) { + var activeIndex = this._getItemIndex(activeElement) + var isGoingToWrap = (direction === Carousel._Direction.PREVIOUS && activeIndex === 0) || + (direction === Carousel._Direction.NEXT && activeIndex == (this._items.length - 1)) + + if (isGoingToWrap && !this._config['wrap']) { + return activeElement } + var delta = direction == Carousel._Direction.PREVIOUS ? -1 : 1 + var itemIndex = (activeIndex + delta) % this._items.length - // CAROUSEL PLUGIN DEFINITION - // ========================== + return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex] +} - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) +/** + * Trigger slide event on element + * @param {Element} relatedTarget + * @param {Carousel._ClassName} directionalClassname + * @return {$.Event} + * @private + */ +Carousel.prototype._triggerSlideEvent = function (relatedTarget, directionalClassname) { + var slideEvent = $.Event(Carousel._Event.SLIDE, { + relatedTarget: relatedTarget, + direction: directionalClassname + }) + + $(this._element).trigger(slideEvent) + + return slideEvent +} + + +/** + * Set the active indicator if available + * @param {Element} element + * @private + */ +Carousel.prototype._setActiveIndicatorElement = function (element) { + if (this._indicatorsElement) { + $(this._indicatorsElement) + .find(Carousel._Selector.ACTIVE) + .removeClass(Carousel._ClassName.ACTIVE) + + var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)] + if (nextIndicator) { + $(nextIndicator).addClass(Carousel._ClassName.ACTIVE) + } } +} + + +/** + * Slide the carousel element in a direction + * @param {Carousel._Direction} direction + * @param {Element=} opt_nextElement + */ +Carousel.prototype._slide = function (direction, opt_nextElement) { + var activeElement = $(this._element).find(Carousel._Selector.ACTIVE_ITEM)[0] + var nextElement = opt_nextElement || activeElement && this._getItemByDirection(direction, activeElement) + + var isCycling = !!this._interval - var old = $.fn.carousel + var directionalClassName = direction == Carousel._Direction.NEXT ? + Carousel._ClassName.LEFT : + Carousel._ClassName.RIGHT - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel + if (nextElement && $(nextElement).hasClass(Carousel._ClassName.ACTIVE)) { + this._isSliding = false + return + } + + var slideEvent = this._triggerSlideEvent(nextElement, directionalClassName) + if (slideEvent.isDefaultPrevented()) { + return + } + if (!activeElement || !nextElement) { + // some weirdness is happening, so we bail (maybe throw exception here alerting user that they're dom is off + return + } - // CAROUSEL NO CONFLICT - // ==================== + this._isSliding = true - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this + if (isCycling) { + this['pause']() } + this._setActiveIndicatorElement(nextElement) - // CAROUSEL DATA-API - // ================= + var slidEvent = $.Event(Carousel._Event.SLID, { relatedTarget: nextElement, direction: directionalClassName }) - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false + if (Bootstrap.transition && $(this._element).hasClass(Carousel._ClassName.SLIDE)) { + $(nextElement).addClass(direction) - Plugin.call($target, options) + Bootstrap.reflow(nextElement) - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } + $(activeElement).addClass(directionalClassName) + $(nextElement).addClass(directionalClassName) + + $(activeElement) + .one(Bootstrap.TRANSITION_END, function () { + $(nextElement) + .removeClass(directionalClassName) + .removeClass(direction) + + $(nextElement).addClass(Carousel._ClassName.ACTIVE) + + $(activeElement) + .removeClass(Carousel._ClassName.ACTIVE) + .removeClass(direction) + .removeClass(directionalClassName) - e.preventDefault() + this._isSliding = false + + setTimeout(function () { + $(this._element).trigger(slidEvent) + }.bind(this), 0) + }.bind(this)) + .emulateTransitionEnd(Carousel._TRANSITION_DURATION) + + } else { + $(activeElement).removeClass(Carousel._ClassName.ACTIVE) + $(nextElement).addClass(Carousel._ClassName.ACTIVE) + + this._isSliding = false + $(this._element).trigger(slidEvent) + } + + if (isCycling) { + this['cycle']() } +} - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ + +/** + * @const + * @type {Function} + */ +$.fn[Carousel._NAME] = Carousel._jQueryInterface + -}(jQuery); +/** + * @const + * @type {Function} + */ +$.fn[Carousel._NAME]['Constructor'] = Carousel + + +/** + * @const + * @type {Function} + */ +$.fn[Carousel._NAME]['noConflict'] = function () { + $.fn[Carousel._NAME] = Carousel._JQUERY_NO_CONFLICT + return this +} + + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +$(document) + .on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', Carousel._dataApiClickHandler) + +$(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Carousel._jQueryInterface.call($carousel, /** @type {Object} */ ($carousel.data())) + }) +}) diff --git a/js/collapse.js b/js/collapse.js index 2bc30e7ba..156163e3f 100644 --- a/js/collapse.js +++ b/js/collapse.js @@ -1,211 +1,455 @@ -/* ======================================================================== - * Bootstrap: collapse.js v3.3.2 +/** ======================================================================= + * Bootstrap: collapse.js v4.0.0 * http://getbootstrap.com/javascript/#collapse * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ + * ======================================================================== + * @fileoverview - Bootstrap's collapse plugin. Flexible support for + * collapsible components like accordions and navigation. + * + * Public Methods & Properties: + * + * + $.carousel + * + $.carousel.noConflict + * + $.carousel.Constructor + * + $.carousel.Constructor.VERSION + * + $.carousel.Constructor.Defaults + * + $.carousel.Constructor.Defaults.toggle + * + $.carousel.Constructor.Defaults.trigger + * + $.carousel.Constructor.Defaults.parent + * + $.carousel.Constructor.prototype.toggle + * + $.carousel.Constructor.prototype.show + * + $.carousel.Constructor.prototype.hide + * + * ======================================================================== + */ +'use strict'; -+function ($) { - 'use strict'; - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ +/** + * Our collapse class. + * @param {Element!} element + * @param {Object=} opt_config + * @constructor + */ +var Collapse = function (element, opt_config) { - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $(this.options.trigger).filter('[href="#' + element.id + '"], [data-target="#' + element.id + '"]') - this.transitioning = null + /** @private {Element} */ + this._element = element - if (this.options.parent) { - this.$parent = this.getParent() - } else { - this.addAriaAndCollapsedClass(this.$element, this.$trigger) - } + /** @private {Object} */ + this._config = $.extend({}, Collapse['Defaults'], opt_config) - if (this.options.toggle) this.toggle() - } + /** @private {Element} */ + this._trigger = typeof this._config['trigger'] == 'string' ? + $(this._config['trigger'])[0] : this._config['trigger'] - Collapse.VERSION = '3.3.2' + /** @private {boolean} */ + this._isTransitioning = false - Collapse.TRANSITION_DURATION = 350 + /** @private {?Element} */ + this._parent = this._config['parent'] ? this._getParent() : null - Collapse.DEFAULTS = { - toggle: true, - trigger: '[data-toggle="collapse"]' + if (!this._config['parent']) { + this._addAriaAndCollapsedClass(this._element, this._trigger) } - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' + if (this._config['toggle']) { + this['toggle']() } - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var activesData - var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') +} + + +/** + * @const + * @type {string} + */ +Collapse['VERSION'] = '4.0.0' + + +/** + * @const + * @type {Object} + */ +Collapse['Defaults'] = { + 'toggle' : true, + 'trigger' : '[data-toggle="collapse"]', + 'parent' : null +} + + +/** + * @const + * @type {string} + * @private + */ +Collapse._NAME = 'collapse' + + +/** + * @const + * @type {string} + * @private + */ +Collapse._DATA_KEY = 'bs.collapse' + + +/** + * @const + * @type {number} + * @private + */ +Collapse._TRANSITION_DURATION = 600 + + +/** + * @const + * @type {Function} + * @private + */ +Collapse._JQUERY_NO_CONFLICT = $.fn[Collapse._NAME] + + +/** + * @const + * @enum {string} + * @private + */ +Collapse._Event = { + SHOW : 'show.bs.collapse', + SHOWN : 'shown.bs.collapse', + HIDE : 'hide.bs.collapse', + HIDDEN : 'hidden.bs.collapse' +} + + +/** + * @const + * @enum {string} + * @private + */ +Collapse._ClassName = { + IN : 'in', + COLLAPSE : 'collapse', + COLLAPSING : 'collapsing', + COLLAPSED : 'collapsed' +} + + +/** + * @const + * @enum {string} + * @private + */ +Collapse._Dimension = { + WIDTH : 'width', + HEIGHT : 'height' +} + + +/** + * @const + * @enum {string} + * @private + */ +Collapse._Selector = { + ACTIVES : '.panel > .in, .panel > .collapsing' +} + + +/** + * Provides the jQuery Interface for the alert component. + * @param {Object|string=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Collapse._jQueryInterface = function (opt_config) { + return this.each(function () { + var $this = $(this) + var data = $this.data(Collapse._DATA_KEY) + var config = $.extend({}, Collapse['Defaults'], $this.data(), typeof opt_config == 'object' && opt_config) - if (actives && actives.length) { - activesData = actives.data('bs.collapse') - if (activesData && activesData.transitioning) return + if (!data && config['toggle'] && opt_config == 'show') { + config['toggle'] = false } - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return + if (!data) { + data = new Collapse(this, config) + $this.data(Collapse._DATA_KEY, data) + } - if (actives && actives.length) { - Plugin.call(actives, 'hide') - activesData || actives.data('bs.collapse', null) + if (typeof opt_config == 'string') { + data[opt_config]() } + }) +} + + +/** + * Function for getting target element from element + * @return {Element} + * @private + */ +Collapse._getTargetFromElement = function (element) { + var selector = Bootstrap.getSelectorFromElement(element) + + return selector ? $(selector)[0] : null +} + - var dimension = this.dimension() +/** + * Toggles the collapse element based on the presence of the 'in' class + */ +Collapse.prototype['toggle'] = function () { + if ($(this._element).hasClass(Collapse._ClassName.IN)) { + this['hide']() + } else { + this['show']() + } +} - this.$element - .removeClass('collapse') - .addClass('collapsing')[dimension](0) - .attr('aria-expanded', true) - this.$trigger - .removeClass('collapsed') - .attr('aria-expanded', true) +/** + * Show's the collapsing element + */ +Collapse.prototype['show'] = function () { + if (this._isTransitioning || $(this._element).hasClass(Collapse._ClassName.IN)) { + return + } - this.transitioning = 1 + var activesData, actives - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('collapse in')[dimension]('') - this.transitioning = 0 - this.$element - .trigger('shown.bs.collapse') + if (this._parent) { + actives = $.makeArray($(Collapse._Selector.ACTIVES)) + if (!actives.length) { + actives = null } + } - if (!$.support.transition) return complete.call(this) + if (actives) { + activesData = $(actives).data(Collapse._DATA_KEY) + if (activesData && activesData._isTransitioning) { + return + } + } - var scrollSize = $.camelCase(['scroll', dimension].join('-')) + var startEvent = $.Event(Collapse._Event.SHOW) + $(this._element).trigger(startEvent) + if (startEvent.isDefaultPrevented()) { + return + } - this.$element - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + if (actives) { + Collapse._jQueryInterface.call($(actives), 'hide') + if (!activesData) { + $(actives).data(Collapse._DATA_KEY, null) + } } - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return + var dimension = this._getDimension() - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return + $(this._element) + .removeClass(Collapse._ClassName.COLLAPSE) + .addClass(Collapse._ClassName.COLLAPSING) - var dimension = this.dimension() + this._element.style[dimension] = 0 + this._element.setAttribute('aria-expanded', true) - this.$element[dimension](this.$element[dimension]())[0].offsetHeight + if (this._trigger) { + $(this._trigger).removeClass(Collapse._ClassName.COLLAPSED) + this._trigger.setAttribute('aria-expanded', true) + } - this.$element - .addClass('collapsing') - .removeClass('collapse in') - .attr('aria-expanded', false) + this['setTransitioning'](true) - this.$trigger - .addClass('collapsed') - .attr('aria-expanded', false) + var complete = function () { + $(this._element) + .removeClass(Collapse._ClassName.COLLAPSING) + .addClass(Collapse._ClassName.COLLAPSE) + .addClass(Collapse._ClassName.IN) - this.transitioning = 1 + this._element.style[dimension] = '' - var complete = function () { - this.transitioning = 0 - this.$element - .removeClass('collapsing') - .addClass('collapse') - .trigger('hidden.bs.collapse') - } + this['setTransitioning'](false) - if (!$.support.transition) return complete.call(this) + $(this._element).trigger(Collapse._Event.SHOWN) + }.bind(this) - this.$element - [dimension](0) - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + if (!Bootstrap.transition) { + complete() + return } - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() + var scrollSize = 'scroll' + (dimension[0].toUpperCase() + dimension.slice(1)) + + $(this._element) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Collapse._TRANSITION_DURATION) + + this._element.style[dimension] = this._element[scrollSize] + 'px' +} + + +/** + * Hides's the collapsing element + */ +Collapse.prototype['hide'] = function () { + if (this._isTransitioning || !$(this._element).hasClass(Collapse._ClassName.IN)) { + return } - Collapse.prototype.getParent = function () { - return $(this.options.parent) - .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') - .each($.proxy(function (i, element) { - var $element = $(element) - this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) - }, this)) - .end() + var startEvent = $.Event(Collapse._Event.HIDE) + $(this._element).trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this._getDimension() + var offsetDimension = dimension === Collapse._Dimension.WIDTH ? + 'offsetWidth' : 'offsetHeight' + + this._element.style[dimension] = this._element[offsetDimension] + 'px' + + Bootstrap.reflow(this._element) + + $(this._element) + .addClass(Collapse._ClassName.COLLAPSING) + .removeClass(Collapse._ClassName.COLLAPSE) + .removeClass(Collapse._ClassName.IN) + + this._element.setAttribute('aria-expanded', false) + + if (this._trigger) { + $(this._trigger).addClass(Collapse._ClassName.COLLAPSED) + this._trigger.setAttribute('aria-expanded', false) } - Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { - var isOpen = $element.hasClass('in') + this['setTransitioning'](true) + + var complete = function () { + this['setTransitioning'](false) + $(this._element) + .removeClass(Collapse._ClassName.COLLAPSING) + .addClass(Collapse._ClassName.COLLAPSE) + .trigger(Collapse._Event.HIDDEN) + + }.bind(this) - $element.attr('aria-expanded', isOpen) - $trigger - .toggleClass('collapsed', !isOpen) - .attr('aria-expanded', isOpen) + this._element.style[dimension] = 0 + + if (!Bootstrap.transition) { + return complete() } - function getTargetFromTrigger($trigger) { - var href - var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + $(this._element) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Collapse._TRANSITION_DURATION) +} + + - return $(target) +/** + * @param {boolean} isTransitioning + */ +Collapse.prototype['setTransitioning'] = function (isTransitioning) { + this._isTransitioning = isTransitioning +} + + +/** + * Returns the collapsing dimension + * @return {string} + * @private + */ +Collapse.prototype._getDimension = function () { + var hasWidth = $(this._element).hasClass(Collapse._Dimension.WIDTH) + return hasWidth ? Collapse._Dimension.WIDTH : Collapse._Dimension.HEIGHT +} + + +/** + * Returns the parent element + * @return {Element} + * @private + */ +Collapse.prototype._getParent = function () { + var selector = '[data-toggle="collapse"][data-parent="' + this._config['parent'] + '"]' + var parent = $(this._config['parent'])[0] + var elements = /** @type {Array.} */ ($.makeArray($(parent).find(selector))) + + for (var i = 0; i < elements.length; i++) { + this._addAriaAndCollapsedClass(Collapse._getTargetFromElement(elements[i]), elements[i]) } + return parent +} - // COLLAPSE PLUGIN DEFINITION - // ========================== - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) +/** + * Returns the parent element + * @param {Element} element + * @param {Element} trigger + * @private + */ +Collapse.prototype._addAriaAndCollapsedClass = function (element, trigger) { + if (element) { + var isOpen = $(element).hasClass(Collapse._ClassName.IN) + element.setAttribute('aria-expanded', isOpen) - if (!data && options.toggle && option == 'show') options.toggle = false - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) + if (trigger) { + trigger.setAttribute('aria-expanded', isOpen) + $(trigger).toggleClass(Collapse._ClassName.COLLAPSED, !isOpen) + } } +} - var old = $.fn.collapse - $.fn.collapse = Plugin - $.fn.collapse.Constructor = Collapse +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ - // COLLAPSE NO CONFLICT - // ==================== +/** + * @const + * @type {Function} + */ +$.fn[Collapse._NAME] = Collapse._jQueryInterface - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } +/** + * @const + * @type {Function} + */ +$.fn[Collapse._NAME]['Constructor'] = Collapse - // COLLAPSE DATA-API - // ================= - $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { - var $this = $(this) +/** + * @const + * @type {Function} + */ +$.fn[Collapse._NAME]['noConflict'] = function () { + $.fn[Collapse._NAME] = Collapse._JQUERY_NO_CONFLICT + return this +} - if (!$this.attr('data-target')) e.preventDefault() - var $target = getTargetFromTrigger($this) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $.extend({}, $this.data(), { trigger: this }) +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ - Plugin.call($target, option) - }) +$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (event) { + event.preventDefault() + + var target = Collapse._getTargetFromElement(this) + + var data = $(target).data(Collapse._DATA_KEY) + var config = data ? 'toggle' : $.extend({}, $(this).data(), { trigger: this }) -}(jQuery); + Collapse._jQueryInterface.call($(target), config) +}) diff --git a/js/dropdown.js b/js/dropdown.js index 200e1c67b..4599d5ba9 100644 --- a/js/dropdown.js +++ b/js/dropdown.js @@ -1,161 +1,322 @@ -/* ======================================================================== - * Bootstrap: dropdown.js v3.3.2 - * http://getbootstrap.com/javascript/#dropdowns +/** ======================================================================= + * Bootstrap: dropdown.js v4.0.0 + * http://getbootstrap.com/javascript/#dropdown * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - + * ======================================================================== + * @fileoverview - Add dropdown menus to nearly anything with this simple + * plugin, including the navbar, tabs, and pills. + * + * Public Methods & Properties: + * + * + $.dropdown + * + $.dropdown.noConflict + * + $.dropdown.Constructor + * + $.dropdown.Constructor.VERSION + * + $.dropdown.Constructor.prototype.toggle + * + * ======================================================================== + */ + +'use strict'; + + +/** + * Our dropdown class. + * @param {Element!} element + * @constructor + */ +var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this['toggle']) +} + + +/** + * @const + * @type {string} + */ +Dropdown['VERSION'] = '4.0.0' + + +/** + * @const + * @type {string} + * @private + */ +Dropdown._NAME = 'dropdown' + + +/** + * @const + * @type {string} + * @private + */ +Dropdown._DATA_KEY = 'bs.dropdown' + + +/** + * @const + * @type {Function} + * @private + */ +Dropdown._JQUERY_NO_CONFLICT = $.fn[Dropdown._NAME] + + +/** + * @const + * @enum {string} + * @private + */ +Dropdown._Event = { + HIDE : 'hide.bs.dropdown', + HIDDEN : 'hidden.bs.dropdown', + SHOW : 'show.bs.dropdown', + SHOWN : 'shown.bs.dropdown' +} + + +/** + * @const + * @enum {string} + * @private + */ +Dropdown._ClassName = { + BACKDROP : 'dropdown-backdrop', + DISABLED : 'disabled', + OPEN : 'open' +} + + +/** + * @const + * @enum {string} + * @private + */ +Dropdown._Selector = { + BACKDROP : '.dropdown-backdrop', + DATA_TOGGLE : '[data-toggle="dropdown"]', + FORM_CHILD : '.dropdown form', + ROLE_MENU : '[role="menu"]', + ROLE_LISTBOX : '[role="listbox"]', + NAVBAR_NAV : '.navbar-nav', + VISIBLE_ITEMS : '[role="menu"] li:not(.divider) a, [role="listbox"] li:not(.divider) a' +} + + +/** + * Provides the jQuery Interface for the alert component. + * @param {string=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Dropdown._jQueryInterface = function (opt_config) { + return this.each(function () { + var data = $(this).data(Dropdown._DATA_KEY) + + if (!data) { + $(this).data(Dropdown._DATA_KEY, (data = new Dropdown(this))) + } -+function ($) { - 'use strict'; + if (typeof opt_config === 'string') { + data[opt_config].call(this) + } + }) +} - // DROPDOWN CLASS DEFINITION - // ========================= - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) +/** + * @param {Event=} opt_event + * @private + */ +Dropdown._clearMenus = function (opt_event) { + if (opt_event && opt_event.which == 3) { + return } - Dropdown.VERSION = '3.3.2' - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) + var backdrop = $(Dropdown._Selector.BACKDROP)[0] + if (backdrop) { + backdrop.parentNode.removeChild(backdrop) + } - if ($this.is('.disabled, :disabled')) return + var toggles = /** @type {Array.} */ ($.makeArray($(Dropdown._Selector.DATA_TOGGLE))) - var $parent = getParent($this) - var isActive = $parent.hasClass('open') + for (var i = 0; i < toggles.length; i++) { + var parent = Dropdown._getParentFromElement(toggles[i]) + var relatedTarget = { 'relatedTarget': toggles[i] } - clearMenus() + if (!$(parent).hasClass(Dropdown._ClassName.OPEN)) { + continue + } - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $(' - -
@@ -49,7 +45,7 @@ - + diff --git a/js/tests/visual/carousel.html b/js/tests/visual/carousel.html index 5b071679f..45424a5d1 100644 --- a/js/tests/visual/carousel.html +++ b/js/tests/visual/carousel.html @@ -44,7 +44,7 @@ - + diff --git a/js/tests/visual/collapse.html b/js/tests/visual/collapse.html index 89b36d7a6..1b81cf5ca 100644 --- a/js/tests/visual/collapse.html +++ b/js/tests/visual/collapse.html @@ -64,7 +64,7 @@ - + diff --git a/js/tests/visual/dropdown.html b/js/tests/visual/dropdown.html index 38f28bca1..6cf18df8e 100644 --- a/js/tests/visual/dropdown.html +++ b/js/tests/visual/dropdown.html @@ -93,7 +93,7 @@ - + diff --git a/js/tests/visual/modal.html b/js/tests/visual/modal.html index 86d1427f9..0d6e50ea2 100644 --- a/js/tests/visual/modal.html +++ b/js/tests/visual/modal.html @@ -63,7 +63,7 @@ - + diff --git a/js/tests/visual/popover.html b/js/tests/visual/popover.html index e71ebca8c..7446a6741 100644 --- a/js/tests/visual/popover.html +++ b/js/tests/visual/popover.html @@ -32,7 +32,7 @@ - + diff --git a/js/tests/visual/scrollspy.html b/js/tests/visual/scrollspy.html index 1edfbc383..13bc4383c 100644 --- a/js/tests/visual/scrollspy.html +++ b/js/tests/visual/scrollspy.html @@ -93,7 +93,7 @@ - + diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html index a1dd8214c..2480ce08c 100644 --- a/js/tests/visual/tab.html +++ b/js/tests/visual/tab.html @@ -157,7 +157,7 @@ - + diff --git a/js/tests/visual/tooltip.html b/js/tests/visual/tooltip.html index 7c8361e4d..c61b088d0 100644 --- a/js/tests/visual/tooltip.html +++ b/js/tests/visual/tooltip.html @@ -29,7 +29,7 @@ - + diff --git a/js/tooltip.js b/js/tooltip.js index 8663bb767..ced0e9282 100644 --- a/js/tooltip.js +++ b/js/tooltip.js @@ -1,472 +1,868 @@ -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.2 +/** ======================================================================= + * Bootstrap: tooltip.js v4.0.0 * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ + * ======================================================================== + * @fileoverview - Bootstrap's tooltip plugin. + * (Inspired by jQuery.tipsy by Jason Frame) + * + * Public Methods & Properties: + * + * + $.tooltip + * + $.tooltip.noConflict + * + $.tooltip.Constructor + * + $.tooltip.Constructor.VERSION + * + $.tooltip.Constructor.Defaults + * + $.tooltip.Constructor.Defaults.container + * + $.tooltip.Constructor.Defaults.animation + * + $.tooltip.Constructor.Defaults.placement + * + $.tooltip.Constructor.Defaults.selector + * + $.tooltip.Constructor.Defaults.template + * + $.tooltip.Constructor.Defaults.trigger + * + $.tooltip.Constructor.Defaults.title + * + $.tooltip.Constructor.Defaults.delay + * + $.tooltip.Constructor.Defaults.html + * + $.tooltip.Constructor.Defaults.viewport + * + $.tooltip.Constructor.Defaults.viewport.selector + * + $.tooltip.Constructor.Defaults.viewport.padding + * + $.tooltip.Constructor.prototype.enable + * + $.tooltip.Constructor.prototype.disable + * + $.tooltip.Constructor.prototype.destroy + * + $.tooltip.Constructor.prototype.toggleEnabled + * + $.tooltip.Constructor.prototype.toggle + * + $.tooltip.Constructor.prototype.show + * + $.tooltip.Constructor.prototype.hide + * + * ======================================================================== + */ +'use strict'; -+function ($) { - 'use strict'; - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== +/** + * Our tooltip class. + * @param {Element!} element + * @param {Object=} opt_config + * @constructor + */ +var Tooltip = function (element, opt_config) { - var Tooltip = function (element, options) { - this.type = - this.options = - this.enabled = - this.timeout = - this.hoverState = - this.$element = null + /** @private {boolean} */ + this._isEnabled = true - this.init('tooltip', element, options) - } + /** @private {number} */ + this._timeout = 0 - Tooltip.VERSION = '3.3.2' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } + /** @private {string} */ + this._hoverState = '' - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport) + /** @protected {Element} */ + this.element = element - var triggers = this.options.trigger.split(' ') + /** @protected {Object} */ + this.config = this._getConfig(opt_config) - for (var i = triggers.length; i--;) { - var trigger = triggers[i] + /** @protected {Element} */ + this.tip = null - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + /** @protected {Element} */ + this.arrow = null - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } + if (this.config['viewport']) { - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } + /** @private {Element} */ + this._viewport = $(this.config['viewport']['selector'] || this.config['viewport'])[0] - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS } - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) + this._setListeners() +} + + +/** + * @const + * @type {string} + */ +Tooltip['VERSION'] = '4.0.0' + + +/** + * @const + * @type {Object} + */ +Tooltip['Defaults'] = { + 'container' : false, + 'animation' : true, + 'placement' : 'top', + 'selector' : false, + 'template' : '', + 'trigger' : 'hover focus', + 'title' : '', + 'delay' : 0, + 'html' : false, + 'viewport': { + 'selector': 'body', + 'padding' : 0 + } +} + + +/** + * @const + * @enum {string} + * @protected + */ +Tooltip.Direction = { + TOP: 'top', + LEFT: 'left', + RIGHT: 'right', + BOTTOM: 'bottom' +} + + +/** + * @const + * @type {string} + * @private + */ +Tooltip._NAME = 'tooltip' + + +/** + * @const + * @type {string} + * @private + */ +Tooltip._DATA_KEY = 'bs.tooltip' + + +/** + * @const + * @type {number} + * @private + */ +Tooltip._TRANSITION_DURATION = 150 + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._HoverState = { + IN: 'in', + OUT: 'out' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._Event = { + HIDE : 'hide.bs.tooltip', + HIDDEN : 'hidden.bs.tooltip', + SHOW : 'show.bs.tooltip', + SHOWN : 'shown.bs.tooltip' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._ClassName = { + FADE : 'fade', + IN : 'in' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._Selector = { + TOOLTIP : '.tooltip', + TOOLTIP_INNER : '.tooltip-inner', + TOOLTIP_ARROW : '.tooltip-arrow' +} + + +/** + * @const + * @type {Function} + * @private + */ +Tooltip._JQUERY_NO_CONFLICT = $.fn[Tooltip._NAME] + + +/** + * @param {Object=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Tooltip._jQueryInterface = function (opt_config) { + return this.each(function () { + var data = $(this).data(Tooltip._DATA_KEY) + var config = typeof opt_config == 'object' ? opt_config : null + + if (!data && opt_config == 'destroy') { + return + } - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } + if (!data) { + data = new Tooltip(this, config) + $(this).data(Tooltip._DATA_KEY, data) } - return options + if (typeof opt_config === 'string') { + data[opt_config]() + } + }) +} + + +/** + * Enable tooltip + */ +Tooltip.prototype['enable'] = function () { + this._isEnabled = true +} + + +/** + * Disable tooltip + */ +Tooltip.prototype['disable'] = function () { + this._isEnabled = false +} + + +/** + * Toggle the tooltip enable state + */ +Tooltip.prototype['toggleEnabled'] = function () { + this._isEnabled = !this._isEnabled +} + +/** + * Toggle the tooltips display + * @param {Event} opt_event + */ +Tooltip.prototype['toggle'] = function (opt_event) { + var context = this + var dataKey = this.getDataKey() + + if (opt_event) { + context = $(opt_event.currentTarget).data(dataKey) + + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) + } } - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() + $(context.getTipElement()).hasClass(Tooltip._ClassName.IN) ? + context._leave(null, context) : + context._enter(null, context) +} - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - return options - } +/** + * Remove tooltip functionality + */ +Tooltip.prototype['destroy'] = function () { + clearTimeout(this._timeout) + this['hide'](function () { + $(this.element) + .off(Tooltip._Selector.TOOLTIP) + .removeData(this.getDataKey()) + }.bind(this)) +} + + +/** + * Show the tooltip + * todo (fat): ~fuck~ this is a big function - refactor out all of positioning logic + * and replace with external lib + */ +Tooltip.prototype['show'] = function () { + var showEvent = $.Event(this.getEventObject().SHOW) + + if (this.isWithContent() && this._isEnabled) { + $(this.element).trigger(showEvent) - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) + var isInTheDom = $.contains(this.element.ownerDocument.documentElement, this.element) - if (self && self.$tip && self.$tip.is(':visible')) { - self.hoverState = 'in' + if (showEvent.isDefaultPrevented() || !isInTheDom) { return } - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) + var tip = this.getTipElement() + var tipId = Bootstrap.getUID(this.getName()) + + tip.setAttribute('id', tipId) + this.element.setAttribute('aria-describedby', tipId) + + this.setContent() + + if (this.config['animation']) { + $(tip).addClass(Tooltip._ClassName.FADE) } - clearTimeout(self.timeout) + var placement = typeof this.config['placement'] == 'function' ? + this.config['placement'].call(this, tip, this.element) : + this.config['placement'] - self.hoverState = 'in' + var autoToken = /\s?auto?\s?/i + var isWithAutoPlacement = autoToken.test(placement) - if (!self.options.delay || !self.options.delay.show) return self.show() + if (isWithAutoPlacement) { + placement = placement.replace(autoToken, '') || Tooltip.Direction.TOP + } - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } + if (tip.parentNode && tip.parentNode.nodeType == Node.ELEMENT_NODE) { + tip.parentNode.removeChild(tip) + } + + tip.style.top = 0 + tip.style.left = 0 + tip.style.display = 'block' + + $(tip).addClass(Tooltip._NAME + '-' + placement) - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) + $(tip).data(this.getDataKey(), this) - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) + if (this.config['container']) { + $(this.config['container'])[0].appendChild(tip) + } else { + this.element.parentNode.insertBefore(tip, this.element.nextSibling) } - clearTimeout(self.timeout) + var position = this._getPosition() + var actualWidth = tip.offsetWidth + var actualHeight = tip.offsetHeight + + var calculatedPlacement = this._getCalculatedAutoPlacement(isWithAutoPlacement, placement, position, actualWidth, actualHeight) + var calculatedOffset = this._getCalculatedOffset(calculatedPlacement, position, actualWidth, actualHeight) - self.hoverState = 'out' + this._applyCalculatedPlacement(calculatedOffset, calculatedPlacement) - if (!self.options.delay || !self.options.delay.hide) return self.hide() + var complete = function () { + var prevHoverState = this.hoverState + $(this.element).trigger(this.getEventObject().SHOWN) + this.hoverState = null - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) + if (prevHoverState == 'out') this._leave(null, this) + }.bind(this) + + Bootstrap.transition && $(this._tip).hasClass(Tooltip._ClassName.FADE) ? + $(this._tip) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) : + complete() } +} - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) +/** + * Hide the tooltip breh + */ +Tooltip.prototype['hide'] = function (callback) { + var tip = this.getTipElement() + var hideEvent = $.Event(this.getEventObject().HIDE) - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this + var complete = function () { + if (this._hoverState != Tooltip._HoverState.IN) { + tip.parentNode.removeChild(tip) + } - var $tip = this.tip() + this.element.removeAttribute('aria-describedby') + $(this.element).trigger(this.getEventObject().HIDDEN) + + if (callback) { + callback() + } + }.bind(this) - var tipId = this.getUID(this.type) + $(this.element).trigger(hideEvent) - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) + if (hideEvent.isDefaultPrevented()) return - if (this.options.animation) $tip.addClass('fade') + $(tip).removeClass(Tooltip._ClassName.IN) - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement + if (Bootstrap.transition && $(this._tip).hasClass(Tooltip._ClassName.FADE)) { + $(tip) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) + } else { + complete() + } - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + this._hoverState = '' +} - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(this.type + '-' + placement) - .data('bs.' + this.type, this) - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) +/** + * @return {string} + */ +Tooltip.prototype['getHoverState'] = function (callback) { + return this._hoverState +} - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - if (autoPlace) { - var origPlacement = placement - var $container = this.options.container ? $(this.options.container) : this.$element.parent() - var containerDim = this.getPosition($container) +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getName = function () { + return Tooltip._NAME +} - 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 - $tip - .removeClass(this.type + '-' + origPlacement) - .addClass(this.type + '-' + placement) - } +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getDataKey = function () { + return Tooltip._DATA_KEY +} - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - this.applyPlacement(calculatedOffset, placement) +/** + * @return {Object} + * @protected + */ +Tooltip.prototype.getEventObject = function () { + return Tooltip._Event +} - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - if (prevHoverState == 'out') that.leave(that) - } +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getTitle = function () { + var title = this.element.getAttribute('data-original-title') - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } + if (!title) { + title = typeof this.config['title'] === 'function' ? + this.config['title'].call(this.element) : + this.config['title'] } - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for IE9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top = offset.top + marginTop - offset.left = offset.left + marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } + return /** @type {string} */ (title) +} - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - if (delta.left) offset.left += delta.left - else offset.top += delta.top +/** + * @return {Element} + * @protected + */ +Tooltip.prototype.getTipElement = function () { + return (this._tip = this._tip || $(this.config['template'])[0]) +} - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } +/** + * @return {Element} + * @protected + */ +Tooltip.prototype.getArrowElement = function () { + return (this.arrow = this.arrow || $(this.getTipElement()).find(Tooltip._Selector.TOOLTIP_ARROW)[0]) +} - Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) { - this.arrow() - .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isHorizontal ? 'top' : 'left', '') - } - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() +/** + * @return {boolean} + * @protected + */ +Tooltip.prototype.isWithContent = function () { + return !!this.getTitle() +} - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in tooltip-top tooltip-bottom tooltip-left tooltip-right') - } - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = this.tip() - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } +/** + * @protected + */ +Tooltip.prototype.setContent = function () { + var tip = this.getTipElement() + var title = this.getTitle() - this.$element.trigger(e) + $(tip).find(Tooltip._Selector.TOOLTIP_INNER)[0][this.config['html'] ? 'innerHTML' : 'innerText'] = title - if (e.isDefaultPrevented()) return + $(tip) + .removeClass(Tooltip._ClassName.FADE) + .removeClass(Tooltip._ClassName.IN) - $tip.removeClass('in') + for (var direction in Tooltip.Direction) { + $(tip).removeClass(Tooltip._NAME + '-' + direction) + } +} - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - this.hoverState = null +/** + * @private + */ +Tooltip.prototype._setListeners = function () { + var triggers = this.config['trigger'].split(' ') - return this - } + triggers.forEach(function (trigger) { + if (trigger == 'click') { + $(this.element).on('click.bs.tooltip', this.config['selector'], this['toggle'].bind(this)) - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + $(this.element) + .on(eventIn + '.bs.tooltip', this.config['selector'], this._enter.bind(this)) + .on(eventOut + '.bs.tooltip', this.config['selector'], this._leave.bind(this)) } - } + }.bind(this)) - Tooltip.prototype.hasContent = function () { - return this.getTitle() + if (this.config['selector']) { + this.config = $.extend({}, this.config, { 'trigger': 'manual', 'selector': '' }) + } else { + this._fixTitle() } +} - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - var el = $element[0] - var isBody = el.tagName == 'BODY' +/** + * @param {Object=} opt_config + * @return {Object} + * @private + */ +Tooltip.prototype._getConfig = function (opt_config) { + var config = $.extend({}, this.constructor['Defaults'], $(this.element).data(), opt_config) - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) + if (config['delay'] && typeof config['delay'] == 'number') { + config['delay'] = { + 'show': config['delay'], + 'hide': config['delay'] } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) } - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + return config +} - } - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } +/** + * @return {Object} + * @private + */ +Tooltip.prototype._getDelegateConfig = function () { + var config = {} + var defaults = this.constructor['Defaults'] + + if (this.config) { + for (var key in this.config) { + var value = this.config[key] + if (defaults[key] != value) config[key] = value } + } + + return config +} + + + +/** + * @param {boolean} isWithAutoPlacement + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {string} + * @private + */ +Tooltip.prototype._getCalculatedAutoPlacement = function (isWithAutoPlacement, placement, position, actualWidth, actualHeight) { + if (isWithAutoPlacement) { + var originalPlacement = placement + var container = this.config['container'] ? $(this.config['container'])[0] : this.element.parentNode + var containerDim = this._getPosition(/** @type {Element} */ (container)) + + placement = placement == Tooltip.Direction.BOTTOM && position.bottom + actualHeight > containerDim.bottom ? Tooltip.Direction.TOP : + placement == Tooltip.Direction.TOP && position.top - actualHeight < containerDim.top ? Tooltip.Direction.BOTTOM : + placement == Tooltip.Direction.RIGHT && position.right + actualWidth > containerDim.width ? Tooltip.Direction.LEFT : + placement == Tooltip.Direction.LEFT && position.left - actualWidth < containerDim.left ? Tooltip.Direction.RIGHT : + placement + + $(this._tip) + .removeClass(Tooltip._NAME + '-' + originalPlacement) + .addClass(Tooltip._NAME + '-' + placement) + } + return placement +} + + +/** + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {{left: number, top: number}} + * @private + */ +Tooltip.prototype._getCalculatedOffset = function (placement, position, actualWidth, actualHeight) { + return placement == Tooltip.Direction.BOTTOM ? { top: position.top + position.height, left: position.left + position.width / 2 - actualWidth / 2 } : + placement == Tooltip.Direction.TOP ? { top: position.top - actualHeight, left: position.left + position.width / 2 - actualWidth / 2 } : + placement == Tooltip.Direction.LEFT ? { top: position.top + position.height / 2 - actualHeight / 2, left: position.left - actualWidth } : + /* placement == Tooltip.Direction.RIGHT */ { top: position.top + position.height / 2 - actualHeight / 2, left: position.left + position.width } +} + + +/** + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {Object} + * @private + */ +Tooltip.prototype._getViewportAdjustedDelta = function (placement, position, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + + if (!this._viewport) { return delta } - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options + var viewportPadding = this.config['viewport'] && this.config['viewport']['padding'] || 0 + var viewportDimensions = this._getPosition(this._viewport) - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + if (placement === Tooltip.Direction.RIGHT || placement === Tooltip.Direction.LEFT) { + var topEdgeOffset = position.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = position.top + viewportPadding - viewportDimensions.scroll + actualHeight - return title + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + + } else { + var leftEdgeOffset = position.left - viewportPadding + var rightEdgeOffset = position.left + viewportPadding + actualWidth + + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + + } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } } - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix + return delta +} + + +/** + * @param {Element=} opt_element + * @return {Object} + * @private + */ +Tooltip.prototype._getPosition = function (opt_element) { + var element = opt_element || this.element + var isBody = element.tagName == 'BODY' + var rect = element.getBoundingClientRect() + var offset = isBody ? { top: 0, left: 0 } : $(element).offset() + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : this.element.scrollTop } + var outerDims = isBody ? { width: window.innerWidth, height: window.innerHeight } : null + + return $.extend({}, rect, scroll, outerDims, offset) +} + + +/** + * @param {{left: number, top: number}} offset + * @param {string} placement + * @private + */ +Tooltip.prototype._applyCalculatedPlacement = function (offset, placement) { + var tip = this.getTipElement() + var width = tip.offsetWidth + var height = tip.offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt(tip.style.marginTop, 10) + var marginLeft = parseInt(tip.style.marginLeft, 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) { + marginTop = 0 + } + if (isNaN(marginLeft)) { + marginLeft = 0 } - Tooltip.prototype.tip = function () { - return (this.$tip = this.$tip || $(this.options.template)) + offset.top = offset.top + marginTop + offset.left = offset.left + marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset(tip, $.extend({ + using: function (props) { + tip.style.top = Math.round(props.top) + 'px' + tip.style.left = Math.round(props.left) + 'px' + } + }, offset), 0) + + $(tip).addClass(Tooltip._ClassName.IN) + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = tip.offsetWidth + var actualHeight = tip.offsetHeight + + if (placement == Tooltip.Direction.TOP && actualHeight != height) { + offset.top = offset.top + height - actualHeight } - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + var delta = this._getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) { + offset.left += delta.left + } else { + offset.top += delta.top } - Tooltip.prototype.enable = function () { - this.enabled = true + var isVertical = placement === Tooltip.Direction.TOP || placement === Tooltip.Direction.BOTTOM + var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' + + $(tip).offset(offset) + + this._replaceArrow(arrowDelta, tip[arrowOffsetPosition], isVertical) +} + + +/** + * @param {number} delta + * @param {number} dimension + * @param {boolean} isHorizontal + * @private + */ +Tooltip.prototype._replaceArrow = function (delta, dimension, isHorizontal) { + var arrow = this.getArrowElement() + + arrow.style[isHorizontal ? 'left' : 'top'] = 50 * (1 - delta / dimension) + '%' + arrow.style[isHorizontal ? 'top' : 'left'] = '' +} + + + +/** + * @private + */ +Tooltip.prototype._fixTitle = function () { + if (this.element.getAttribute('title') || typeof this.element.getAttribute('data-original-title') != 'string') { + this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '') + this.element.setAttribute('title', '') } +} + - Tooltip.prototype.disable = function () { - this.enabled = false +/** + * @param {Event=} opt_event + * @param {Object=} opt_context + * @private + */ +Tooltip.prototype._enter = function (opt_event, opt_context) { + var dataKey = this.getDataKey() + var context = opt_context || $(opt_event.currentTarget).data(dataKey) + + if (context && context._tip && context._tip.offsetWidth) { + context._hoverState = Tooltip._HoverState.IN + return } - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) } - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } + clearTimeout(context._timeout) - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } + context._hoverState = Tooltip._HoverState.IN - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - }) + if (!context.config['delay'] || !context.config['delay']['show']) { + context['show']() + return } + context._timeout = setTimeout(function () { + if (context._hoverState == Tooltip._HoverState.IN) { + context['show']() + } + }, context.config['delay']['show']) +} + + +/** + * @param {Event=} opt_event + * @param {Object=} opt_context + * @private + */ +Tooltip.prototype._leave = function (opt_event, opt_context) { + var dataKey = this.getDataKey() + var context = opt_context || $(opt_event.currentTarget).data(dataKey) + + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) + } - // TOOLTIP PLUGIN DEFINITION - // ========================= + clearTimeout(context._timeout) - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option + context._hoverState = Tooltip._HoverState.OUT - if (!data && option == 'destroy') return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) + if (!context.config['delay'] || !context.config['delay']['hide']) { + context['hide']() + return } - var old = $.fn.tooltip + context._timeout = setTimeout(function () { + if (context._hoverState == Tooltip._HoverState.OUT) { + context['hide']() + } + }, context.config['delay']['hide']) +} - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - // TOOLTIP NO CONFLICT - // =================== +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ + +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME] = Tooltip._jQueryInterface + + +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME]['Constructor'] = Tooltip - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } -}(jQuery); +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME]['noConflict'] = function () { + $.fn[Tooltip._NAME] = Tooltip._JQUERY_NO_CONFLICT + return this +} diff --git a/js/util.js b/js/util.js new file mode 100644 index 000000000..294a5a960 --- /dev/null +++ b/js/util.js @@ -0,0 +1,165 @@ +/** ======================================================================= + * Bootstrap: util.js v4.0.0 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== + * @fileoverview - Bootstrap's private util helper. Adds private util + * helpers for things like accesibility and transitions. These methods are + * shared across all bootstrap plugins. + * ======================================================================== + */ + +'use strict'; + + +/** + * @type {Object} + */ +var Bootstrap = {} + + +/** + * @const + * @type {string} + */ +Bootstrap.TRANSITION_END = 'bsTransitionEnd' + + +/** + * @const + * @type {Object} + */ +Bootstrap.TransitionEndEvent = { + 'WebkitTransition' : 'webkitTransitionEnd', + 'MozTransition' : 'transitionend', + 'OTransition' : 'oTransitionEnd otransitionend', + 'transition' : 'transitionend' +} + + +/** + * @param {Function} childConstructor + * @param {Function} parentConstructor + */ +Bootstrap.inherits = function(childConstructor, parentConstructor) { + /** @constructor */ + function tempConstructor() {} + tempConstructor.prototype = parentConstructor.prototype + childConstructor.prototype = new tempConstructor() + /** @override */ + childConstructor.prototype.constructor = childConstructor +} + + +/** + * @param {Element} element + * @return {string|null} + */ +Bootstrap.getSelectorFromElement = function (element) { + var selector = element.getAttribute('data-target') + + if (!selector) { + selector = element.getAttribute('href') || '' + selector = /^#[a-z]/i.test(selector) ? selector : null + } + + return selector +} + + +/** + * @param {string} prefix + * @return {string} + */ +Bootstrap.getUID = function (prefix) { + do prefix += ~~(Math.random() * 1000000) + while (document.getElementById(prefix)) + return prefix +} + + +/** + * @return {Object} + */ +Bootstrap.getSpecialTransitionEndEvent = function () { + return { + bindType: Bootstrap.transition.end, + delegateType: Bootstrap.transition.end, + handle: /** @param {jQuery.Event} event */ (function (event) { + if ($(event.target).is(this)) { + return event.handleObj.handler.apply(this, arguments) + } + }) + } +} + + +/** + * @param {Element} element + */ +Bootstrap.reflow = function (element) { + new Function('bs',"return bs")(element.offsetHeight) +} + + +/** + * @return {Object|boolean} + */ +Bootstrap.transitionEndTest = function () { + if (window['QUnit']) { + return false + } + + var el = document.createElement('bootstrap') + for (var name in Bootstrap.TransitionEndEvent) { + if (el.style[name] !== undefined) { + return { end: Bootstrap.TransitionEndEvent[name] } + } + } + return false +} + + +/** + * @param {number} duration + * @this {Element} + * @return {Object} + */ +Bootstrap.transitionEndEmulator = function (duration) { + var called = false + + $(this).one(Bootstrap.TRANSITION_END, function () { + called = true + }) + + var callback = function () { + if (!called) { + $(this).trigger(Bootstrap.transition.end) + } + }.bind(this) + + setTimeout(callback, duration) + + return this +} + + +/** + * ------------------------------------------------------------------------ + * jQuery Interface + * ------------------------------------------------------------------------ + */ + +$.fn.emulateTransitionEnd = Bootstrap.transitionEndEmulator + +$(function () { + Bootstrap.transition = Bootstrap.transitionEndTest() + + if (!Bootstrap.transition) { + return + } + + $.event.special[Bootstrap.TRANSITION_END] = Bootstrap.getSpecialTransitionEndEvent() +}) -- cgit v1.2.3