shoulda-matchers 2.6.1 → 2.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +9 -0
  4. data/.yardopts +8 -0
  5. data/Appraisals +82 -33
  6. data/Gemfile +18 -2
  7. data/Gemfile.lock +13 -1
  8. data/NEWS.md +27 -2
  9. data/README.md +83 -1329
  10. data/Rakefile +118 -1
  11. data/cucumber.yml +1 -0
  12. data/doc_config/gh-pages/index.html.erb +9 -0
  13. data/doc_config/yard/setup.rb +22 -0
  14. data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
  15. data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
  18. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
  19. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
  20. data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
  21. data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
  22. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +298 -0
  23. data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
  24. data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
  25. data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
  26. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
  27. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
  28. data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
  29. data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
  30. data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
  31. data/doc_config/yard/templates/default/layout/html/setup.rb +40 -0
  32. data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
  33. data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
  34. data/docs.watchr +5 -0
  35. data/features/rails_integration.feature +32 -0
  36. data/features/step_definitions/rails_steps.rb +55 -9
  37. data/features/support/env.rb +1 -0
  38. data/gemfiles/3.0.gemfile +13 -1
  39. data/gemfiles/3.0.gemfile.lock +13 -1
  40. data/gemfiles/3.1.gemfile +17 -2
  41. data/gemfiles/3.1.gemfile.lock +31 -2
  42. data/gemfiles/3.1_1.9.2.gemfile +33 -0
  43. data/gemfiles/3.1_1.9.2.gemfile.lock +203 -0
  44. data/gemfiles/3.2.gemfile +18 -2
  45. data/gemfiles/3.2.gemfile.lock +32 -2
  46. data/gemfiles/3.2_1.9.2.gemfile +32 -0
  47. data/gemfiles/3.2_1.9.2.gemfile.lock +200 -0
  48. data/gemfiles/4.0.0.gemfile +20 -1
  49. data/gemfiles/4.0.0.gemfile.lock +46 -2
  50. data/gemfiles/4.0.1.gemfile +20 -1
  51. data/gemfiles/4.0.1.gemfile.lock +46 -2
  52. data/gemfiles/4.1.gemfile +21 -2
  53. data/gemfiles/4.1.gemfile.lock +47 -4
  54. data/lib/shoulda/matchers/action_controller.rb +0 -20
  55. data/lib/shoulda/matchers/action_controller/callback_matcher.rb +119 -28
  56. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +22 -6
  57. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +43 -10
  58. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +40 -13
  59. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +63 -11
  60. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +34 -1
  61. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +84 -15
  62. data/lib/shoulda/matchers/action_controller/route_matcher.rb +84 -28
  63. data/lib/shoulda/matchers/action_controller/route_params.rb +4 -3
  64. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +76 -13
  65. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +147 -13
  66. data/lib/shoulda/matchers/action_controller/strong_parameters_matcher.rb +148 -2
  67. data/lib/shoulda/matchers/active_model.rb +0 -25
  68. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +66 -9
  69. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +161 -19
  70. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +5 -5
  71. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +92 -13
  72. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +218 -16
  73. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +198 -32
  74. data/lib/shoulda/matchers/active_model/errors.rb +5 -2
  75. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +1 -1
  76. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +29 -8
  77. data/lib/shoulda/matchers/active_model/helpers.rb +20 -8
  78. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  79. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +4 -6
  80. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +4 -3
  81. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +3 -2
  82. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +4 -3
  83. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +4 -3
  84. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +52 -14
  85. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +51 -13
  86. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +53 -7
  87. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +275 -19
  88. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +84 -14
  89. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +170 -41
  90. data/lib/shoulda/matchers/active_model/validation_matcher.rb +20 -15
  91. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +1 -2
  92. data/lib/shoulda/matchers/active_record.rb +1 -12
  93. data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +89 -15
  94. data/lib/shoulda/matchers/active_record/association_matcher.rb +726 -70
  95. data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  96. data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +4 -3
  97. data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +4 -3
  98. data/lib/shoulda/matchers/active_record/association_matchers/inverse_of_matcher.rb +4 -3
  99. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +2 -1
  100. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +4 -3
  101. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +4 -5
  102. data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +4 -3
  103. data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +4 -3
  104. data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +4 -3
  105. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +79 -15
  106. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +64 -15
  107. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +21 -7
  108. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +85 -10
  109. data/lib/shoulda/matchers/assertion_error.rb +7 -1
  110. data/lib/shoulda/matchers/doublespeak.rb +2 -1
  111. data/lib/shoulda/matchers/doublespeak/double.rb +3 -1
  112. data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -1
  113. data/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +1 -0
  114. data/lib/shoulda/matchers/doublespeak/object_double.rb +2 -1
  115. data/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +2 -1
  116. data/lib/shoulda/matchers/doublespeak/structs.rb +2 -0
  117. data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +2 -1
  118. data/lib/shoulda/matchers/doublespeak/world.rb +3 -4
  119. data/lib/shoulda/matchers/error.rb +1 -0
  120. data/lib/shoulda/matchers/independent/delegate_matcher.rb +108 -20
  121. data/lib/shoulda/matchers/independent/delegate_matcher/stubbed_target.rb +4 -3
  122. data/lib/shoulda/matchers/integrations/nunit_test_case_detection.rb +3 -0
  123. data/lib/shoulda/matchers/rails_shim.rb +3 -2
  124. data/lib/shoulda/matchers/version.rb +2 -1
  125. data/lib/shoulda/matchers/warn.rb +1 -0
  126. data/script/SUPPORTED_VERSIONS +1 -0
  127. data/script/install_gems_in_all_appraisals +14 -0
  128. data/script/run_all_tests +14 -0
  129. data/shoulda-matchers.gemspec +0 -10
  130. data/spec/report_warnings.rb +7 -0
  131. data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +1 -1
  132. data/spec/shoulda/matchers/action_controller/strong_parameters_matcher_spec.rb +9 -0
  133. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +0 -36
  134. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +2 -2
  135. data/spec/shoulda/matchers/doublespeak/double_spec.rb +1 -1
  136. data/spec/shoulda/matchers/doublespeak/world_spec.rb +11 -29
  137. data/spec/shoulda/matchers/doublespeak_spec.rb +3 -3
  138. data/spec/spec_helper.rb +17 -0
  139. data/spec/support/class_builder.rb +4 -0
  140. data/spec/support/test_application.rb +1 -1
  141. data/spec/warnings_spy.rb +64 -0
  142. data/spec/warnings_spy/filesystem.rb +45 -0
  143. data/spec/warnings_spy/partitioner.rb +29 -0
  144. data/spec/warnings_spy/reader.rb +64 -0
  145. data/spec/warnings_spy/reporter.rb +87 -0
  146. metadata +49 -134
@@ -0,0 +1,8 @@
1
+ <% n = 1 %>
2
+ <% @items.each do |item| %>
3
+ <li class="r<%= n %> <%= item.has_tag?(:deprecated) ? 'deprecated' : '' %>">
4
+ <%= linkify item, h(item.name(true)) %>
5
+ <small>(<%= item.namespace.title %>)</small>
6
+ </li>
7
+ <% n = n == 2 ? 1 : 2 %>
8
+ <% end %>
@@ -0,0 +1,298 @@
1
+ function createSourceLinks() {
2
+ $('.method_details_list .source_code').
3
+ before("<span class='showSource'>[<a href='#' class='toggleSource'>View source</a>]</span>");
4
+ $('.toggleSource').toggle(function() {
5
+ $(this).parent().nextAll('.source_code').show();
6
+ $(this).text("Hide source");
7
+ StickyHeaders.update();
8
+ },
9
+ function() {
10
+ $(this).parent().nextAll('.source_code').hide();
11
+ $(this).text("View source");
12
+ StickyHeaders.update();
13
+ });
14
+ }
15
+
16
+ function createDefineLinks() {
17
+ var tHeight = 0;
18
+ $('.defines').after(" <a href='#' class='toggleDefines'>more...</a>");
19
+ $('.toggleDefines').toggle(function() {
20
+ tHeight = $(this).parent().prev().height();
21
+ $(this).prev().show();
22
+ $(this).parent().prev().height($(this).parent().height());
23
+ $(this).text("(less)");
24
+ StickyHeaders.update();
25
+ },
26
+ function() {
27
+ $(this).prev().hide();
28
+ $(this).parent().prev().height(tHeight);
29
+ $(this).text("more...");
30
+ StickyHeaders.update();
31
+ });
32
+ }
33
+
34
+ function createFullTreeLinks() {
35
+ var tHeight = 0;
36
+ $('.inheritanceTree').toggle(function() {
37
+ tHeight = $(this).parent().prev().height();
38
+ $(this).parent().toggleClass('showAll');
39
+ $(this).text("(hide)");
40
+ $(this).parent().prev().height($(this).parent().height());
41
+ StickyHeaders.update();
42
+ },
43
+ function() {
44
+ $(this).parent().toggleClass('showAll');
45
+ $(this).parent().prev().height(tHeight);
46
+ $(this).text("show all");
47
+ StickyHeaders.update();
48
+ });
49
+ }
50
+
51
+ function hideSearchFrameOnBlur() {
52
+ $(document.body).on('click', hideSearchFrame);
53
+ }
54
+
55
+ function initSearchFrameLinks() {
56
+ var search = $('.js-search');
57
+ var items = search.find('li');
58
+ var links = search.find('a');
59
+ var frame = $('.js-search-frame');
60
+
61
+ function listen() {
62
+ links.on('click', function (event) {
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ toggle(this, this.href);
66
+ });
67
+
68
+ $(document).on('keydown', function (event) {
69
+ if (event.which == 27) {
70
+ close();
71
+ }
72
+ })
73
+
74
+ $(document).on('click', function (event) {
75
+ close();
76
+ })
77
+ }
78
+
79
+ function toggle(link, frameSrc) {
80
+ if (isOpen() && clickedOnSameLink(frameSrc)) {
81
+ close();
82
+ } else {
83
+ open(link, frameSrc);
84
+ }
85
+ }
86
+
87
+ function isOpen() {
88
+ return frame.hasClass('open');
89
+ }
90
+
91
+ function clickedOnSameLink(location) {
92
+ return frame.attr('src') === location;
93
+ }
94
+
95
+ function reset() {
96
+ items.removeClass('active');
97
+ }
98
+
99
+ function open(link, location) {
100
+ reset();
101
+ $(link).parent().addClass('active');
102
+ frame.attr('src', location).one('load', function () {
103
+ frame.addClass('open');
104
+ })
105
+ }
106
+
107
+ function close() {
108
+ reset();
109
+ frame.removeClass('open');
110
+ }
111
+
112
+ listen();
113
+ }
114
+
115
+
116
+ function linkSummaries() {
117
+ $('.summary_signature').click(function() {
118
+ document.location = $(this).find('a').attr('href');
119
+ });
120
+ }
121
+
122
+ function framesInit() {
123
+ if (hasFrames) {
124
+ document.body.className = 'frames';
125
+ $('#menu .noframes a').attr('href', document.location);
126
+ try {
127
+ window.top.document.title = $('html head title').text();
128
+ } catch(error) {
129
+ // some browsers will not allow this when serving from file://
130
+ // but we don't want to stop the world.
131
+ }
132
+ }
133
+ else {
134
+ $('#menu .noframes a').text('frames').attr('href', framesUrl);
135
+ }
136
+ }
137
+
138
+ function keyboardShortcuts() {
139
+ if (window.top.frames.main) return;
140
+
141
+ $(document).keypress(function(evt) {
142
+ if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) return;
143
+
144
+ if (
145
+ typeof evt.target !== "undefined" &&
146
+ (evt.target.nodeName == "INPUT" || evt.target.nodeName == "TEXTAREA")
147
+ ) return;
148
+
149
+ switch (evt.charCode) {
150
+ case 67: case 99: $('#class_list_link').click(); break; // 'c'
151
+ case 77: case 109: $('#method_list_link').click(); break; // 'm'
152
+ case 70: case 102: $('#file_list_link').click(); break; // 'f'
153
+ default: break;
154
+ }
155
+ });
156
+ }
157
+
158
+ function summaryToggle() {
159
+ $('.summary_toggle').click(function() {
160
+ if (localStorage) {
161
+ localStorage.summaryCollapsed = $(this).text();
162
+ }
163
+ $('.summary_toggle').each(function() {
164
+ $(this).text($(this).text() == "collapse" ? "expand" : "collapse");
165
+ var next = $(this).parent().parent().nextAll('ul.summary').first();
166
+ if (next.hasClass('compact')) {
167
+ next.toggle();
168
+ next.nextAll('ul.summary').first().toggle();
169
+ }
170
+ else if (next.hasClass('summary')) {
171
+ var list = $('<ul class="summary compact" />');
172
+ list.html(next.html());
173
+ list.find('.summary_desc, .note').remove();
174
+ list.find('a').each(function() {
175
+ $(this).html($(this).find('strong').html());
176
+ $(this).parent().html($(this)[0].outerHTML);
177
+ });
178
+ next.before(list);
179
+ next.toggle();
180
+ }
181
+ });
182
+ StickyHeaders.update();
183
+ return false;
184
+ });
185
+ if (localStorage) {
186
+ if (localStorage.summaryCollapsed == "collapse") {
187
+ $('.summary_toggle').first().click();
188
+ }
189
+ else localStorage.summaryCollapsed = "expand";
190
+ }
191
+ }
192
+
193
+ function fixOutsideWorldLinks() {
194
+ $('a').each(function() {
195
+ if (window.location.host != this.host) this.target = '_parent';
196
+ });
197
+ }
198
+
199
+ function generateTOC() {
200
+ if ($('#filecontents').length === 0) return;
201
+ var _toc = $('<ol class="top"></ol>');
202
+ var show = false;
203
+ var toc = _toc;
204
+ var counter = 0;
205
+ var tags = ['h2', 'h3', 'h4', 'h5', 'h6'];
206
+ var i;
207
+ if ($('#filecontents h1').length > 1) tags.unshift('h1');
208
+ for (i = 0; i < tags.length; i++) { tags[i] = '#filecontents ' + tags[i]; }
209
+ var lastTag = parseInt(tags[0][1], 10);
210
+ $(tags.join(', ')).each(function() {
211
+ if ($(this).parents('.method_details .docstring').length != 0) return;
212
+ if (this.id == "filecontents") return;
213
+ show = true;
214
+ var thisTag = parseInt(this.tagName[1], 10);
215
+ if (this.id.length === 0) {
216
+ var proposedId = $(this).attr('toc-id');
217
+ if (typeof(proposedId) != "undefined") this.id = proposedId;
218
+ else {
219
+ var proposedId = $(this).text().replace(/[^a-z0-9-]/ig, '_');
220
+ if ($('#' + proposedId).length > 0) { proposedId += counter; counter++; }
221
+ this.id = proposedId;
222
+ }
223
+ }
224
+ if (thisTag > lastTag) {
225
+ for (i = 0; i < thisTag - lastTag; i++) {
226
+ var tmp = $('<ol/>'); toc.append(tmp); toc = tmp;
227
+ }
228
+ }
229
+ if (thisTag < lastTag) {
230
+ for (i = 0; i < lastTag - thisTag; i++) toc = toc.parent();
231
+ }
232
+ var title = $(this).attr('toc-title');
233
+ if (typeof(title) == "undefined") title = $(this).text();
234
+ toc.append('<li><a href="#' + this.id + '">' + title + '</a></li>');
235
+ lastTag = thisTag;
236
+ });
237
+ if (!show) return;
238
+ html = '<div id="toc"><p class="title"><a class="hide_toc" href="#"><strong>Table of Contents</strong></a> <small>(<a href="#" class="float_toc">left</a>)</small></p></div>';
239
+ $('#content').prepend(html);
240
+ $('#toc').append(_toc);
241
+ $('#toc .hide_toc').toggle(function() {
242
+ $('#toc .top').slideUp('fast');
243
+ $('#toc').toggleClass('hidden');
244
+ $('#toc .title small').toggle();
245
+ }, function() {
246
+ $('#toc .top').slideDown('fast');
247
+ $('#toc').toggleClass('hidden');
248
+ $('#toc .title small').toggle();
249
+ });
250
+ $('#toc .float_toc').toggle(function() {
251
+ $(this).text('float');
252
+ $('#toc').toggleClass('nofloat');
253
+ }, function() {
254
+ $(this).text('left');
255
+ $('#toc').toggleClass('nofloat');
256
+ });
257
+ }
258
+
259
+ function removeDlBoxIfEmpty() {
260
+ var $box = $('dl.box')
261
+ if (!$box.text().trim()) {
262
+ $box.remove();
263
+ }
264
+ }
265
+
266
+ function makeHeadersSticky() {
267
+ StickyHeaders
268
+ .set({
269
+ contentContainer: '#main',
270
+ stickyHeaderContainer: '#header',
271
+ switchOnCollisionWith: 'top',
272
+ fillHeadersWith: function (node) {
273
+ return $('<h3>').text(node.find('strong').text());
274
+ }
275
+ })
276
+ .add('.method_details > h3')
277
+ .activate();
278
+ }
279
+
280
+ function updateStickyHeaders() {
281
+ setTimeout(function () {
282
+ StickyHeaders.update()
283
+ }, 100)
284
+ }
285
+
286
+ $(makeHeadersSticky);
287
+ $(framesInit);
288
+ $(createSourceLinks);
289
+ $(createDefineLinks);
290
+ $(createFullTreeLinks);
291
+ $(initSearchFrameLinks);
292
+ $(linkSummaries);
293
+ $(keyboardShortcuts);
294
+ $(summaryToggle);
295
+ $(fixOutsideWorldLinks);
296
+ $(generateTOC);
297
+ $(removeDlBoxIfEmpty);
298
+ $(updateStickyHeaders);
@@ -0,0 +1 @@
1
+ // Override with nothing
@@ -0,0 +1,289 @@
1
+ window.StickyHeaders = (function ($) {
2
+ var me = {},
3
+ body, config, contentContainer, currentHeaderRangeIndex,
4
+ currentScrollOffset, elem, headerRanges, headers, isScrolling,
5
+ lastScrollOffset, selectors, stickyHeaderContainer;
6
+
7
+ function init() {
8
+ config = {
9
+ switchOnCollisionWith: 'top',
10
+ copy: 'element'
11
+ };
12
+ selectors = [];
13
+ headers = [];
14
+ headerRanges = [];
15
+
16
+ currentHeaderRangeIndex = -1;
17
+ currentScrollOffset = 0;
18
+ lastScrollOffset = 0;
19
+ isScrolling = false;
20
+
21
+ createStickyHeader();
22
+ }
23
+
24
+ function createStickyHeader() {
25
+ elem = $('<div>').attr('id', 'sticky-header');
26
+ }
27
+
28
+ function setHeaders() {
29
+ var elements = [],
30
+ stickyHeaderLineHeight = parseCssValue(elem.css('line-height')),
31
+ stickyHeaderTopPadding;
32
+
33
+ headers = [];
34
+ $.each(selectors, function (_, selector) {
35
+ contentContainer.find(selector).each(function (_, element) {
36
+ var $element = $(element),
37
+ fontSize = parseCssValue($element.css('font-size')),
38
+ topOffset = (
39
+ element.offsetTop +
40
+ parseFloat($element.css('padding-top'), 10) +
41
+ (- ((stickyHeaderLineHeight - fontSize) / 2))
42
+ ),
43
+ height = Math.round($element.height()),
44
+ outerHeight = Math.round($element.outerHeight(true)),
45
+ bottomOffset = topOffset + outerHeight;
46
+
47
+ headers.push({
48
+ element: element,
49
+ $element: $element,
50
+ topOffset: topOffset,
51
+ bottomOffset: bottomOffset
52
+ });
53
+ })
54
+ })
55
+ }
56
+
57
+ function setHeaderRanges() {
58
+ var offsetProp = config.switchOnCollisionWith + 'Offset',
59
+ start, end;
60
+
61
+ headerRanges = [];
62
+ for (var i = 0, len = headers.length; i < len; i++) {
63
+ start = headers[i][offsetProp];
64
+
65
+ if (headers[i+1]) {
66
+ end = headers[i+1][offsetProp];
67
+ } else {
68
+ end = null;
69
+ }
70
+
71
+ headerRanges.push({
72
+ start: start,
73
+ end: end,
74
+ element: headers[i].element
75
+ });
76
+ }
77
+
78
+ //debugHeaderRanges();
79
+ }
80
+
81
+ function debugHeaderRanges() {
82
+ contentContainer.find('.header-range-debug').remove();
83
+
84
+ $.each(headerRanges, function (i, range) {
85
+ var color = 'hsla('+(20*i)+', 100%, 50%, 0.15)',
86
+ debug = $('<div>')
87
+ .addClass('header-range-debug')
88
+ .css({
89
+ width: '100%',
90
+ position: 'absolute',
91
+ top: range.start + 'px',
92
+ height: (range.end === null ? '1px' : (range.end - range.start) + 'px'),
93
+ backgroundColor: color,
94
+ borderTop: '1px solid black'
95
+ })
96
+ .appendTo(contentContainer)
97
+
98
+ debug.append(
99
+ $('<div>')
100
+ .css({
101
+ position: 'absolute',
102
+ top: 0,
103
+ right: 0,
104
+ height: '2em',
105
+ lineHeight: '2em',
106
+ width: '40em',
107
+ fontSize: '13px',
108
+ backgroundColor: 'black',
109
+ color: 'white',
110
+ padding: '0 5px'
111
+ })
112
+ .text(headers[i].$element.text() + ' (#' + i + ')')
113
+ )
114
+ })
115
+ }
116
+
117
+ function setCurrentHeaderIndex() {
118
+ var scrollTop = contentContainer.scrollTop();
119
+
120
+ for (var i = 0, len = headers.length; i < len; i++) {
121
+ if (scrollTop < headers[i].bottomOffset) {
122
+ break;
123
+ }
124
+ currentHeaderIndex = i;
125
+ }
126
+ }
127
+
128
+ function render() {
129
+ var clonedHeader;
130
+
131
+ if (currentHeaderRangeIndex < 0 || currentHeaderRangeIndex > headerRanges.length-1) {
132
+ elem.removeClass('show');
133
+ body.removeClass('has-sticky-header');
134
+ }
135
+ else {
136
+ var realHeader = $(headerRanges[currentHeaderRangeIndex].element);
137
+
138
+ if (typeof config.fillHeadersWith === 'function') {
139
+ elem.html(config.fillHeadersWith(realHeader));
140
+ } else if (config.fillHeadersWith === 'content') {
141
+ elem.html(realHeader.clone().html());
142
+ } else {
143
+ elem.html(realHeader.clone());
144
+ }
145
+
146
+ elem.addClass('show');
147
+ body.addClass('has-sticky-header');
148
+ }
149
+
150
+ return me;
151
+ }
152
+
153
+ function determineCurrentHeaderRangeIndex(startIndex, direction) {
154
+ var index = startIndex;
155
+ while (true) {
156
+ currentHeaderRange = headerRanges[index];
157
+ if (!currentHeaderRange || isWithinRange(currentScrollOffset, currentHeaderRange)) {
158
+ break;
159
+ } else {
160
+ index += direction;
161
+ }
162
+ }
163
+ return index;
164
+ }
165
+
166
+ function onScroll() {
167
+ if (!headerRanges.length) {
168
+ return;
169
+ }
170
+
171
+ currentScrollOffset = contentContainer.scrollTop();
172
+
173
+ if (currentScrollOffset > headerRanges[0].start) {
174
+ var newCurrentHeaderRangeIndex = currentHeaderRangeIndex;
175
+ var currentHeaderRange = headerRanges[newCurrentHeaderRangeIndex];
176
+
177
+ if (newCurrentHeaderRangeIndex < 0) {
178
+ newCurrentHeaderRangeIndex = 0;
179
+ }
180
+
181
+ if (currentScrollOffset < lastScrollOffset) {
182
+ // scrolling up
183
+ newCurrentHeaderRangeIndex = determineCurrentHeaderRangeIndex(newCurrentHeaderRangeIndex, -1);
184
+ } else {
185
+ // scrolling down
186
+ newCurrentHeaderRangeIndex = determineCurrentHeaderRangeIndex(newCurrentHeaderRangeIndex, +1);
187
+ }
188
+ } else {
189
+ newCurrentHeaderRangeIndex = -1;
190
+ }
191
+
192
+ // only re-render when necessary
193
+ if (newCurrentHeaderRangeIndex !== undefined && currentHeaderRangeIndex !== newCurrentHeaderRangeIndex) {
194
+ currentHeaderRangeIndex = newCurrentHeaderRangeIndex;
195
+ render();
196
+ }
197
+
198
+ lastScrollOffset = currentScrollOffset;
199
+ }
200
+
201
+ function listenToScroll(element, callback, options) {
202
+ options = options || {};
203
+
204
+ if (options.every) {
205
+ element.on('scroll', function () {
206
+ isScrolling = true;
207
+ })
208
+
209
+ setInterval(function () {
210
+ if (isScrolling) {
211
+ callback();
212
+ isScrolling = false;
213
+ }
214
+ }, options.every);
215
+ }
216
+ else {
217
+ element.on('scroll', callback);
218
+ }
219
+ }
220
+
221
+ function isWithinRange(number, range) {
222
+ return (
223
+ number >= range.start &&
224
+ (
225
+ range.end === undefined ||
226
+ range.end === null ||
227
+ number <= range.end
228
+ )
229
+ );
230
+ }
231
+
232
+ function parseCssValue(value) {
233
+ if (value === null || value === undefined) {
234
+ return 0;
235
+ } else {
236
+ return parseInt(value, 10);
237
+ }
238
+ }
239
+
240
+ me.setHeaders = setHeaders;
241
+ me.setHeaderRanges = setHeaderRanges;
242
+
243
+ me.set = function (/* key, value | config */) {
244
+ if ($.isPlainObject(arguments[0])) {
245
+ $.extend(config, arguments[0]);
246
+ } else {
247
+ config[arguments[0]] = arguments[1];
248
+ }
249
+ return me;
250
+ }
251
+
252
+ me.add = function (/* selectors... */) {
253
+ selectors.push.apply(selectors, arguments);
254
+ return me;
255
+ }
256
+
257
+ me.activate = function () {
258
+ body = $('body');
259
+ contentContainer = config.contentContainer ? $(config.contentContainer) : body;
260
+ stickyHeaderContainer = config.stickyHeaderContainer ? $(config.stickyHeaderContainer) : contentContainer;
261
+
262
+ stickyHeaderContainer.append(elem);
263
+ setHeaders();
264
+ setHeaderRanges();
265
+ setCurrentHeaderIndex();
266
+
267
+ return me;
268
+ }
269
+
270
+ me.update = function () {
271
+ setHeaders();
272
+ setHeaderRanges();
273
+ render();
274
+
275
+ listenToScroll(contentContainer, onScroll);
276
+ }
277
+
278
+ me.getHeaders = function () {
279
+ return headers;
280
+ }
281
+
282
+ me.getHeaderRanges = function () {
283
+ return headerRanges;
284
+ }
285
+
286
+ init();
287
+
288
+ return me;
289
+ })(jQuery);