rails_admin 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rails_admin might be problematic. Click here for more details.

Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/rails_admin/jquery.migrate.js +3 -0
  3. data/app/assets/javascripts/rails_admin/ra.filter-box.js +1 -4
  4. data/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js +3 -3
  5. data/app/assets/javascripts/rails_admin/ra.widgets.js +1 -1
  6. data/app/assets/javascripts/rails_admin/rails_admin.js +2 -1
  7. data/app/assets/javascripts/rails_admin/ui.js +2 -2
  8. data/app/helpers/rails_admin/form_builder.rb +2 -0
  9. data/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb +1 -1
  10. data/lib/rails_admin/version.rb +2 -2
  11. data/vendor/assets/javascripts/rails_admin/bootstrap-datetimepicker.js +317 -150
  12. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-affix.js +50 -28
  13. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-alert.js +10 -7
  14. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-button.js +35 -20
  15. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-carousel.js +48 -25
  16. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-collapse.js +70 -28
  17. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-dropdown.js +56 -42
  18. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-modal.js +118 -40
  19. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-popover.js +26 -16
  20. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-scrollspy.js +29 -27
  21. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-tab.js +46 -19
  22. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-tooltip.js +280 -60
  23. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-transition.js +5 -5
  24. data/vendor/assets/javascripts/rails_admin/jquery.pjax.js +317 -160
  25. data/vendor/assets/javascripts/rails_admin/moment-with-locales.js +11210 -9290
  26. metadata +3 -3
  27. data/config/initializers/devise_patch.rb +0 -9
@@ -1,8 +1,8 @@
1
1
  /* ========================================================================
2
- * Bootstrap: popover.js v3.2.0
3
- * http://getbootstrap.com/javascript/#popovers
2
+ * Bootstrap: popover.js v3.4.1
3
+ * https://getbootstrap.com/docs/3.4/javascript/#popovers
4
4
  * ========================================================================
5
- * Copyright 2011-2014 Twitter, Inc.
5
+ * Copyright 2011-2019 Twitter, Inc.
6
6
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7
7
  * ======================================================================== */
8
8
 
@@ -19,7 +19,7 @@
19
19
 
20
20
  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
21
21
 
22
- Popover.VERSION = '3.2.0'
22
+ Popover.VERSION = '3.4.1'
23
23
 
24
24
  Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
25
25
  placement: 'right',
@@ -45,10 +45,25 @@
45
45
  var title = this.getTitle()
46
46
  var content = this.getContent()
47
47
 
48
- $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
49
- $tip.find('.popover-content').empty()[ // we use append for html objects to maintain js events
50
- this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
51
- ](content)
48
+ if (this.options.html) {
49
+ var typeContent = typeof content
50
+
51
+ if (this.options.sanitize) {
52
+ title = this.sanitizeHtml(title)
53
+
54
+ if (typeContent === 'string') {
55
+ content = this.sanitizeHtml(content)
56
+ }
57
+ }
58
+
59
+ $tip.find('.popover-title').html(title)
60
+ $tip.find('.popover-content').children().detach().end()[
61
+ typeContent === 'string' ? 'html' : 'append'
62
+ ](content)
63
+ } else {
64
+ $tip.find('.popover-title').text(title)
65
+ $tip.find('.popover-content').children().detach().end().text(content)
66
+ }
52
67
 
53
68
  $tip.removeClass('fade top bottom left right in')
54
69
 
@@ -67,19 +82,14 @@
67
82
 
68
83
  return $e.attr('data-content')
69
84
  || (typeof o.content == 'function' ?
70
- o.content.call($e[0]) :
71
- o.content)
85
+ o.content.call($e[0]) :
86
+ o.content)
72
87
  }
73
88
 
74
89
  Popover.prototype.arrow = function () {
75
90
  return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
76
91
  }
77
92
 
78
- Popover.prototype.tip = function () {
79
- if (!this.$tip) this.$tip = $(this.options.template)
80
- return this.$tip
81
- }
82
-
83
93
 
84
94
  // POPOVER PLUGIN DEFINITION
85
95
  // =========================
@@ -90,7 +100,7 @@
90
100
  var data = $this.data('bs.popover')
91
101
  var options = typeof option == 'object' && option
92
102
 
93
- if (!data && option == 'destroy') return
103
+ if (!data && /destroy|hide/.test(option)) return
94
104
  if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
95
105
  if (typeof option == 'string') data[option]()
96
106
  })
@@ -1,8 +1,8 @@
1
1
  /* ========================================================================
2
- * Bootstrap: scrollspy.js v3.2.0
3
- * http://getbootstrap.com/javascript/#scrollspy
2
+ * Bootstrap: scrollspy.js v3.4.1
3
+ * https://getbootstrap.com/docs/3.4/javascript/#scrollspy
4
4
  * ========================================================================
5
- * Copyright 2011-2014 Twitter, Inc.
5
+ * Copyright 2011-2019 Twitter, Inc.
6
6
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7
7
  * ======================================================================== */
8
8
 
@@ -14,10 +14,8 @@
14
14
  // ==========================
15
15
 
16
16
  function ScrollSpy(element, options) {
17
- var process = $.proxy(this.process, this)
18
-
19
- this.$body = $('body')
20
- this.$scrollElement = $(element).is('body') ? $(window) : $(element)
17
+ this.$body = $(document.body)
18
+ this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
21
19
  this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
22
20
  this.selector = (this.options.target || '') + ' .nav li > a'
23
21
  this.offsets = []
@@ -25,12 +23,12 @@
25
23
  this.activeTarget = null
26
24
  this.scrollHeight = 0
27
25
 
28
- this.$scrollElement.on('scroll.bs.scrollspy', process)
26
+ this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
29
27
  this.refresh()
30
28
  this.process()
31
29
  }
32
30
 
33
- ScrollSpy.VERSION = '3.2.0'
31
+ ScrollSpy.VERSION = '3.4.1'
34
32
 
35
33
  ScrollSpy.DEFAULTS = {
36
34
  offset: 10
@@ -41,20 +39,19 @@
41
39
  }
42
40
 
43
41
  ScrollSpy.prototype.refresh = function () {
44
- var offsetMethod = 'offset'
45
- var offsetBase = 0
42
+ var that = this
43
+ var offsetMethod = 'offset'
44
+ var offsetBase = 0
45
+
46
+ this.offsets = []
47
+ this.targets = []
48
+ this.scrollHeight = this.getScrollHeight()
46
49
 
47
50
  if (!$.isWindow(this.$scrollElement[0])) {
48
51
  offsetMethod = 'position'
49
52
  offsetBase = this.$scrollElement.scrollTop()
50
53
  }
51
54
 
52
- this.offsets = []
53
- this.targets = []
54
- this.scrollHeight = this.getScrollHeight()
55
-
56
- var self = this
57
-
58
55
  this.$body
59
56
  .find(this.selector)
60
57
  .map(function () {
@@ -69,8 +66,8 @@
69
66
  })
70
67
  .sort(function (a, b) { return a[0] - b[0] })
71
68
  .each(function () {
72
- self.offsets.push(this[0])
73
- self.targets.push(this[1])
69
+ that.offsets.push(this[0])
70
+ that.targets.push(this[1])
74
71
  })
75
72
  }
76
73
 
@@ -91,14 +88,15 @@
91
88
  return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
92
89
  }
93
90
 
94
- if (activeTarget && scrollTop <= offsets[0]) {
95
- return activeTarget != (i = targets[0]) && this.activate(i)
91
+ if (activeTarget && scrollTop < offsets[0]) {
92
+ this.activeTarget = null
93
+ return this.clear()
96
94
  }
97
95
 
98
96
  for (i = offsets.length; i--;) {
99
97
  activeTarget != targets[i]
100
98
  && scrollTop >= offsets[i]
101
- && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
99
+ && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])
102
100
  && this.activate(targets[i])
103
101
  }
104
102
  }
@@ -106,13 +104,11 @@
106
104
  ScrollSpy.prototype.activate = function (target) {
107
105
  this.activeTarget = target
108
106
 
109
- $(this.selector)
110
- .parentsUntil(this.options.target, '.active')
111
- .removeClass('active')
107
+ this.clear()
112
108
 
113
109
  var selector = this.selector +
114
- '[data-target="' + target + '"],' +
115
- this.selector + '[href="' + target + '"]'
110
+ '[data-target="' + target + '"],' +
111
+ this.selector + '[href="' + target + '"]'
116
112
 
117
113
  var active = $(selector)
118
114
  .parents('li')
@@ -127,6 +123,12 @@
127
123
  active.trigger('activate.bs.scrollspy')
128
124
  }
129
125
 
126
+ ScrollSpy.prototype.clear = function () {
127
+ $(this.selector)
128
+ .parentsUntil(this.options.target, '.active')
129
+ .removeClass('active')
130
+ }
131
+
130
132
 
131
133
  // SCROLLSPY PLUGIN DEFINITION
132
134
  // ===========================
@@ -1,8 +1,8 @@
1
1
  /* ========================================================================
2
- * Bootstrap: tab.js v3.2.0
3
- * http://getbootstrap.com/javascript/#tabs
2
+ * Bootstrap: tab.js v3.4.1
3
+ * https://getbootstrap.com/docs/3.4/javascript/#tabs
4
4
  * ========================================================================
5
- * Copyright 2011-2014 Twitter, Inc.
5
+ * Copyright 2011-2019 Twitter, Inc.
6
6
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7
7
  * ======================================================================== */
8
8
 
@@ -14,10 +14,14 @@
14
14
  // ====================
15
15
 
16
16
  var Tab = function (element) {
17
+ // jscs:disable requireDollarBeforejQueryAssignment
17
18
  this.element = $(element)
19
+ // jscs:enable requireDollarBeforejQueryAssignment
18
20
  }
19
21
 
20
- Tab.VERSION = '3.2.0'
22
+ Tab.VERSION = '3.4.1'
23
+
24
+ Tab.TRANSITION_DURATION = 150
21
25
 
22
26
  Tab.prototype.show = function () {
23
27
  var $this = this.element
@@ -31,22 +35,30 @@
31
35
 
32
36
  if ($this.parent('li').hasClass('active')) return
33
37
 
34
- var previous = $ul.find('.active:last a')[0]
35
- var e = $.Event('show.bs.tab', {
36
- relatedTarget: previous
38
+ var $previous = $ul.find('.active:last a')
39
+ var hideEvent = $.Event('hide.bs.tab', {
40
+ relatedTarget: $this[0]
41
+ })
42
+ var showEvent = $.Event('show.bs.tab', {
43
+ relatedTarget: $previous[0]
37
44
  })
38
45
 
39
- $this.trigger(e)
46
+ $previous.trigger(hideEvent)
47
+ $this.trigger(showEvent)
40
48
 
41
- if (e.isDefaultPrevented()) return
49
+ if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
42
50
 
43
- var $target = $(selector)
51
+ var $target = $(document).find(selector)
44
52
 
45
53
  this.activate($this.closest('li'), $ul)
46
54
  this.activate($target, $target.parent(), function () {
55
+ $previous.trigger({
56
+ type: 'hidden.bs.tab',
57
+ relatedTarget: $this[0]
58
+ })
47
59
  $this.trigger({
48
60
  type: 'shown.bs.tab',
49
- relatedTarget: previous
61
+ relatedTarget: $previous[0]
50
62
  })
51
63
  })
52
64
  }
@@ -55,15 +67,21 @@
55
67
  var $active = container.find('> .active')
56
68
  var transition = callback
57
69
  && $.support.transition
58
- && $active.hasClass('fade')
70
+ && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)
59
71
 
60
72
  function next() {
61
73
  $active
62
74
  .removeClass('active')
63
75
  .find('> .dropdown-menu > .active')
64
76
  .removeClass('active')
77
+ .end()
78
+ .find('[data-toggle="tab"]')
79
+ .attr('aria-expanded', false)
65
80
 
66
- element.addClass('active')
81
+ element
82
+ .addClass('active')
83
+ .find('[data-toggle="tab"]')
84
+ .attr('aria-expanded', true)
67
85
 
68
86
  if (transition) {
69
87
  element[0].offsetWidth // reflow for transition
@@ -72,17 +90,22 @@
72
90
  element.removeClass('fade')
73
91
  }
74
92
 
75
- if (element.parent('.dropdown-menu')) {
76
- element.closest('li.dropdown').addClass('active')
93
+ if (element.parent('.dropdown-menu').length) {
94
+ element
95
+ .closest('li.dropdown')
96
+ .addClass('active')
97
+ .end()
98
+ .find('[data-toggle="tab"]')
99
+ .attr('aria-expanded', true)
77
100
  }
78
101
 
79
102
  callback && callback()
80
103
  }
81
104
 
82
- transition ?
105
+ $active.length && transition ?
83
106
  $active
84
107
  .one('bsTransitionEnd', next)
85
- .emulateTransitionEnd(150) :
108
+ .emulateTransitionEnd(Tab.TRANSITION_DURATION) :
86
109
  next()
87
110
 
88
111
  $active.removeClass('in')
@@ -120,9 +143,13 @@
120
143
  // TAB DATA-API
121
144
  // ============
122
145
 
123
- $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
146
+ var clickHandler = function (e) {
124
147
  e.preventDefault()
125
148
  Plugin.call($(this), 'show')
126
- })
149
+ }
150
+
151
+ $(document)
152
+ .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
153
+ .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
127
154
 
128
155
  }(jQuery);
@@ -1,31 +1,164 @@
1
1
  /* ========================================================================
2
- * Bootstrap: tooltip.js v3.2.0
3
- * http://getbootstrap.com/javascript/#tooltip
2
+ * Bootstrap: tooltip.js v3.4.1
3
+ * https://getbootstrap.com/docs/3.4/javascript/#tooltip
4
4
  * Inspired by the original jQuery.tipsy by Jason Frame
5
5
  * ========================================================================
6
- * Copyright 2011-2014 Twitter, Inc.
6
+ * Copyright 2011-2019 Twitter, Inc.
7
7
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8
8
  * ======================================================================== */
9
9
 
10
-
11
10
  +function ($) {
12
11
  'use strict';
13
12
 
13
+ var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
14
+
15
+ var uriAttrs = [
16
+ 'background',
17
+ 'cite',
18
+ 'href',
19
+ 'itemtype',
20
+ 'longdesc',
21
+ 'poster',
22
+ 'src',
23
+ 'xlink:href'
24
+ ]
25
+
26
+ var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
27
+
28
+ var DefaultWhitelist = {
29
+ // Global attributes allowed on any supplied element below.
30
+ '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
31
+ a: ['target', 'href', 'title', 'rel'],
32
+ area: [],
33
+ b: [],
34
+ br: [],
35
+ col: [],
36
+ code: [],
37
+ div: [],
38
+ em: [],
39
+ hr: [],
40
+ h1: [],
41
+ h2: [],
42
+ h3: [],
43
+ h4: [],
44
+ h5: [],
45
+ h6: [],
46
+ i: [],
47
+ img: ['src', 'alt', 'title', 'width', 'height'],
48
+ li: [],
49
+ ol: [],
50
+ p: [],
51
+ pre: [],
52
+ s: [],
53
+ small: [],
54
+ span: [],
55
+ sub: [],
56
+ sup: [],
57
+ strong: [],
58
+ u: [],
59
+ ul: []
60
+ }
61
+
62
+ /**
63
+ * A pattern that recognizes a commonly useful subset of URLs that are safe.
64
+ *
65
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
66
+ */
67
+ var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
68
+
69
+ /**
70
+ * A pattern that matches safe data URLs. Only matches image, video and audio types.
71
+ *
72
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
73
+ */
74
+ var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
75
+
76
+ function allowedAttribute(attr, allowedAttributeList) {
77
+ var attrName = attr.nodeName.toLowerCase()
78
+
79
+ if ($.inArray(attrName, allowedAttributeList) !== -1) {
80
+ if ($.inArray(attrName, uriAttrs) !== -1) {
81
+ return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
82
+ }
83
+
84
+ return true
85
+ }
86
+
87
+ var regExp = $(allowedAttributeList).filter(function (index, value) {
88
+ return value instanceof RegExp
89
+ })
90
+
91
+ // Check if a regular expression validates the attribute.
92
+ for (var i = 0, l = regExp.length; i < l; i++) {
93
+ if (attrName.match(regExp[i])) {
94
+ return true
95
+ }
96
+ }
97
+
98
+ return false
99
+ }
100
+
101
+ function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
102
+ if (unsafeHtml.length === 0) {
103
+ return unsafeHtml
104
+ }
105
+
106
+ if (sanitizeFn && typeof sanitizeFn === 'function') {
107
+ return sanitizeFn(unsafeHtml)
108
+ }
109
+
110
+ // IE 8 and below don't support createHTMLDocument
111
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
112
+ return unsafeHtml
113
+ }
114
+
115
+ var createdDocument = document.implementation.createHTMLDocument('sanitization')
116
+ createdDocument.body.innerHTML = unsafeHtml
117
+
118
+ var whitelistKeys = $.map(whiteList, function (el, i) { return i })
119
+ var elements = $(createdDocument.body).find('*')
120
+
121
+ for (var i = 0, len = elements.length; i < len; i++) {
122
+ var el = elements[i]
123
+ var elName = el.nodeName.toLowerCase()
124
+
125
+ if ($.inArray(elName, whitelistKeys) === -1) {
126
+ el.parentNode.removeChild(el)
127
+
128
+ continue
129
+ }
130
+
131
+ var attributeList = $.map(el.attributes, function (el) { return el })
132
+ var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
133
+
134
+ for (var j = 0, len2 = attributeList.length; j < len2; j++) {
135
+ if (!allowedAttribute(attributeList[j], whitelistedAttributes)) {
136
+ el.removeAttribute(attributeList[j].nodeName)
137
+ }
138
+ }
139
+ }
140
+
141
+ return createdDocument.body.innerHTML
142
+ }
143
+
14
144
  // TOOLTIP PUBLIC CLASS DEFINITION
15
145
  // ===============================
16
146
 
17
147
  var Tooltip = function (element, options) {
18
- this.type =
19
- this.options =
20
- this.enabled =
21
- this.timeout =
22
- this.hoverState =
148
+ this.type = null
149
+ this.options = null
150
+ this.enabled = null
151
+ this.timeout = null
152
+ this.hoverState = null
23
153
  this.$element = null
154
+ this.inState = null
24
155
 
25
156
  this.init('tooltip', element, options)
26
157
  }
27
158
 
28
- Tooltip.VERSION = '3.2.0'
159
+ Tooltip.VERSION = '3.4.1'
160
+
161
+ Tooltip.TRANSITION_DURATION = 150
29
162
 
30
163
  Tooltip.DEFAULTS = {
31
164
  animation: true,
@@ -40,7 +173,10 @@
40
173
  viewport: {
41
174
  selector: 'body',
42
175
  padding: 0
43
- }
176
+ },
177
+ sanitize : true,
178
+ sanitizeFn : null,
179
+ whiteList : DefaultWhitelist
44
180
  }
45
181
 
46
182
  Tooltip.prototype.init = function (type, element, options) {
@@ -48,7 +184,12 @@
48
184
  this.type = type
49
185
  this.$element = $(element)
50
186
  this.options = this.getOptions(options)
51
- this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
187
+ this.$viewport = this.options.viewport && $(document).find($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
188
+ this.inState = { click: false, hover: false, focus: false }
189
+
190
+ if (this.$element[0] instanceof document.constructor && !this.options.selector) {
191
+ throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
192
+ }
52
193
 
53
194
  var triggers = this.options.trigger.split(' ')
54
195
 
@@ -76,7 +217,15 @@
76
217
  }
77
218
 
78
219
  Tooltip.prototype.getOptions = function (options) {
79
- options = $.extend({}, this.getDefaults(), this.$element.data(), options)
220
+ var dataAttributes = this.$element.data()
221
+
222
+ for (var dataAttr in dataAttributes) {
223
+ if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
224
+ delete dataAttributes[dataAttr]
225
+ }
226
+ }
227
+
228
+ options = $.extend({}, this.getDefaults(), dataAttributes, options)
80
229
 
81
230
  if (options.delay && typeof options.delay == 'number') {
82
231
  options.delay = {
@@ -85,6 +234,10 @@
85
234
  }
86
235
  }
87
236
 
237
+ if (options.sanitize) {
238
+ options.template = sanitizeHtml(options.template, options.whiteList, options.sanitizeFn)
239
+ }
240
+
88
241
  return options
89
242
  }
90
243
 
@@ -108,6 +261,15 @@
108
261
  $(obj.currentTarget).data('bs.' + this.type, self)
109
262
  }
110
263
 
264
+ if (obj instanceof $.Event) {
265
+ self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
266
+ }
267
+
268
+ if (self.tip().hasClass('in') || self.hoverState == 'in') {
269
+ self.hoverState = 'in'
270
+ return
271
+ }
272
+
111
273
  clearTimeout(self.timeout)
112
274
 
113
275
  self.hoverState = 'in'
@@ -119,6 +281,14 @@
119
281
  }, self.options.delay.show)
120
282
  }
121
283
 
284
+ Tooltip.prototype.isInStateTrue = function () {
285
+ for (var key in this.inState) {
286
+ if (this.inState[key]) return true
287
+ }
288
+
289
+ return false
290
+ }
291
+
122
292
  Tooltip.prototype.leave = function (obj) {
123
293
  var self = obj instanceof this.constructor ?
124
294
  obj : $(obj.currentTarget).data('bs.' + this.type)
@@ -128,6 +298,12 @@
128
298
  $(obj.currentTarget).data('bs.' + this.type, self)
129
299
  }
130
300
 
301
+ if (obj instanceof $.Event) {
302
+ self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
303
+ }
304
+
305
+ if (self.isInStateTrue()) return
306
+
131
307
  clearTimeout(self.timeout)
132
308
 
133
309
  self.hoverState = 'out'
@@ -145,7 +321,7 @@
145
321
  if (this.hasContent() && this.enabled) {
146
322
  this.$element.trigger(e)
147
323
 
148
- var inDom = $.contains(document.documentElement, this.$element[0])
324
+ var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
149
325
  if (e.isDefaultPrevented() || !inDom) return
150
326
  var that = this
151
327
 
@@ -173,7 +349,8 @@
173
349
  .addClass(placement)
174
350
  .data('bs.' + this.type, this)
175
351
 
176
- this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
352
+ this.options.container ? $tip.appendTo($(document).find(this.options.container)) : $tip.insertAfter(this.$element)
353
+ this.$element.trigger('inserted.bs.' + this.type)
177
354
 
178
355
  var pos = this.getPosition()
179
356
  var actualWidth = $tip[0].offsetWidth
@@ -181,13 +358,12 @@
181
358
 
182
359
  if (autoPlace) {
183
360
  var orgPlacement = placement
184
- var $parent = this.$element.parent()
185
- var parentDim = this.getPosition($parent)
361
+ var viewportDim = this.getPosition(this.$viewport)
186
362
 
187
- placement = placement == 'bottom' && pos.top + pos.height + actualHeight - parentDim.scroll > parentDim.height ? 'top' :
188
- placement == 'top' && pos.top - parentDim.scroll - actualHeight < 0 ? 'bottom' :
189
- placement == 'right' && pos.right + actualWidth > parentDim.width ? 'left' :
190
- placement == 'left' && pos.left - actualWidth < parentDim.left ? 'right' :
363
+ placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' :
364
+ placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' :
365
+ placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' :
366
+ placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' :
191
367
  placement
192
368
 
193
369
  $tip
@@ -200,14 +376,17 @@
200
376
  this.applyPlacement(calculatedOffset, placement)
201
377
 
202
378
  var complete = function () {
379
+ var prevHoverState = that.hoverState
203
380
  that.$element.trigger('shown.bs.' + that.type)
204
381
  that.hoverState = null
382
+
383
+ if (prevHoverState == 'out') that.leave(that)
205
384
  }
206
385
 
207
386
  $.support.transition && this.$tip.hasClass('fade') ?
208
387
  $tip
209
388
  .one('bsTransitionEnd', complete)
210
- .emulateTransitionEnd(150) :
389
+ .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
211
390
  complete()
212
391
  }
213
392
  }
@@ -225,8 +404,8 @@
225
404
  if (isNaN(marginTop)) marginTop = 0
226
405
  if (isNaN(marginLeft)) marginLeft = 0
227
406
 
228
- offset.top = offset.top + marginTop
229
- offset.left = offset.left + marginLeft
407
+ offset.top += marginTop
408
+ offset.left += marginLeft
230
409
 
231
410
  // $.fn.offset doesn't round pixel values
232
411
  // so we use setOffset directly with our own function B-0
@@ -254,36 +433,50 @@
254
433
  if (delta.left) offset.left += delta.left
255
434
  else offset.top += delta.top
256
435
 
257
- var arrowDelta = delta.left ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
258
- var arrowPosition = delta.left ? 'left' : 'top'
259
- var arrowOffsetPosition = delta.left ? 'offsetWidth' : 'offsetHeight'
436
+ var isVertical = /top|bottom/.test(placement)
437
+ var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
438
+ var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
260
439
 
261
440
  $tip.offset(offset)
262
- this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition)
441
+ this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
263
442
  }
264
443
 
265
- Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
266
- this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
444
+ Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
445
+ this.arrow()
446
+ .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
447
+ .css(isVertical ? 'top' : 'left', '')
267
448
  }
268
449
 
269
450
  Tooltip.prototype.setContent = function () {
270
451
  var $tip = this.tip()
271
452
  var title = this.getTitle()
272
453
 
273
- $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
454
+ if (this.options.html) {
455
+ if (this.options.sanitize) {
456
+ title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
457
+ }
458
+
459
+ $tip.find('.tooltip-inner').html(title)
460
+ } else {
461
+ $tip.find('.tooltip-inner').text(title)
462
+ }
463
+
274
464
  $tip.removeClass('fade in top bottom left right')
275
465
  }
276
466
 
277
- Tooltip.prototype.hide = function () {
467
+ Tooltip.prototype.hide = function (callback) {
278
468
  var that = this
279
- var $tip = this.tip()
469
+ var $tip = $(this.$tip)
280
470
  var e = $.Event('hide.bs.' + this.type)
281
471
 
282
- this.$element.removeAttr('aria-describedby')
283
-
284
472
  function complete() {
285
473
  if (that.hoverState != 'in') $tip.detach()
286
- that.$element.trigger('hidden.bs.' + that.type)
474
+ if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
475
+ that.$element
476
+ .removeAttr('aria-describedby')
477
+ .trigger('hidden.bs.' + that.type)
478
+ }
479
+ callback && callback()
287
480
  }
288
481
 
289
482
  this.$element.trigger(e)
@@ -292,10 +485,10 @@
292
485
 
293
486
  $tip.removeClass('in')
294
487
 
295
- $.support.transition && this.$tip.hasClass('fade') ?
488
+ $.support.transition && $tip.hasClass('fade') ?
296
489
  $tip
297
490
  .one('bsTransitionEnd', complete)
298
- .emulateTransitionEnd(150) :
491
+ .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
299
492
  complete()
300
493
 
301
494
  this.hoverState = null
@@ -305,7 +498,7 @@
305
498
 
306
499
  Tooltip.prototype.fixTitle = function () {
307
500
  var $e = this.$element
308
- if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
501
+ if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') {
309
502
  $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
310
503
  }
311
504
  }
@@ -316,20 +509,30 @@
316
509
 
317
510
  Tooltip.prototype.getPosition = function ($element) {
318
511
  $element = $element || this.$element
512
+
319
513
  var el = $element[0]
320
514
  var isBody = el.tagName == 'BODY'
321
- return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : null, {
322
- scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop(),
323
- width: isBody ? $(window).width() : $element.outerWidth(),
324
- height: isBody ? $(window).height() : $element.outerHeight()
325
- }, isBody ? { top: 0, left: 0 } : $element.offset())
515
+
516
+ var elRect = el.getBoundingClientRect()
517
+ if (elRect.width == null) {
518
+ // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
519
+ elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
520
+ }
521
+ var isSvg = window.SVGElement && el instanceof window.SVGElement
522
+ // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.
523
+ // See https://github.com/twbs/bootstrap/issues/20280
524
+ var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())
525
+ var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
526
+ var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
527
+
528
+ return $.extend({}, elRect, scroll, outerDims, elOffset)
326
529
  }
327
530
 
328
531
  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
329
- return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
330
- placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
532
+ return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
533
+ placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
331
534
  placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
332
- /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
535
+ /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
333
536
 
334
537
  }
335
538
 
@@ -353,7 +556,7 @@
353
556
  var rightEdgeOffset = pos.left + viewportPadding + actualWidth
354
557
  if (leftEdgeOffset < viewportDimensions.left) { // left overflow
355
558
  delta.left = viewportDimensions.left - leftEdgeOffset
356
- } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
559
+ } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
357
560
  delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
358
561
  }
359
562
  }
@@ -379,21 +582,19 @@
379
582
  }
380
583
 
381
584
  Tooltip.prototype.tip = function () {
382
- return (this.$tip = this.$tip || $(this.options.template))
585
+ if (!this.$tip) {
586
+ this.$tip = $(this.options.template)
587
+ if (this.$tip.length != 1) {
588
+ throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')
589
+ }
590
+ }
591
+ return this.$tip
383
592
  }
384
593
 
385
594
  Tooltip.prototype.arrow = function () {
386
595
  return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
387
596
  }
388
597
 
389
- Tooltip.prototype.validate = function () {
390
- if (!this.$element[0].parentNode) {
391
- this.hide()
392
- this.$element = null
393
- this.options = null
394
- }
395
- }
396
-
397
598
  Tooltip.prototype.enable = function () {
398
599
  this.enabled = true
399
600
  }
@@ -416,14 +617,33 @@
416
617
  }
417
618
  }
418
619
 
419
- self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
620
+ if (e) {
621
+ self.inState.click = !self.inState.click
622
+ if (self.isInStateTrue()) self.enter(self)
623
+ else self.leave(self)
624
+ } else {
625
+ self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
626
+ }
420
627
  }
421
628
 
422
629
  Tooltip.prototype.destroy = function () {
630
+ var that = this
423
631
  clearTimeout(this.timeout)
424
- this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
632
+ this.hide(function () {
633
+ that.$element.off('.' + that.type).removeData('bs.' + that.type)
634
+ if (that.$tip) {
635
+ that.$tip.detach()
636
+ }
637
+ that.$tip = null
638
+ that.$arrow = null
639
+ that.$viewport = null
640
+ that.$element = null
641
+ })
425
642
  }
426
643
 
644
+ Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
645
+ return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
646
+ }
427
647
 
428
648
  // TOOLTIP PLUGIN DEFINITION
429
649
  // =========================
@@ -434,7 +654,7 @@
434
654
  var data = $this.data('bs.tooltip')
435
655
  var options = typeof option == 'object' && option
436
656
 
437
- if (!data && option == 'destroy') return
657
+ if (!data && /destroy|hide/.test(option)) return
438
658
  if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
439
659
  if (typeof option == 'string') data[option]()
440
660
  })