blacklight_range_limit 8.5.0 → 9.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +117 -26
  3. data/.gitignore +2 -1
  4. data/.solr_wrapper.yml +8 -0
  5. data/Gemfile +7 -1
  6. data/README.md +77 -86
  7. data/Rakefile +31 -0
  8. data/VERSION +1 -1
  9. data/app/assets/javascripts/blacklight-range-limit/index.js +345 -0
  10. data/app/components/blacklight_range_limit/range_facet_component.html.erb +29 -27
  11. data/app/components/blacklight_range_limit/range_facet_component.rb +15 -9
  12. data/app/components/blacklight_range_limit/range_form_component.html.erb +13 -5
  13. data/app/components/blacklight_range_limit/range_form_component.rb +8 -20
  14. data/app/presenters/blacklight_range_limit/facet_field_presenter.rb +13 -2
  15. data/app/presenters/blacklight_range_limit/facet_item_presenter.rb +6 -11
  16. data/app/presenters/blacklight_range_limit/filter_field.rb +2 -2
  17. data/blacklight_range_limit.gemspec +1 -1
  18. data/config/importmap.rb +9 -0
  19. data/config/locales/blacklight_range_limit.ar.yml +13 -8
  20. data/config/locales/blacklight_range_limit.de.yml +13 -8
  21. data/config/locales/blacklight_range_limit.en.yml +5 -1
  22. data/config/locales/blacklight_range_limit.es.yml +18 -0
  23. data/config/locales/blacklight_range_limit.it.yml +13 -8
  24. data/lib/blacklight_range_limit/controller_override.rb +1 -25
  25. data/lib/blacklight_range_limit/engine.rb +48 -0
  26. data/lib/blacklight_range_limit/range_limit_builder.rb +40 -3
  27. data/lib/blacklight_range_limit.rb +8 -31
  28. data/lib/generators/blacklight_range_limit/assets_generator.rb +82 -18
  29. data/lib/generators/blacklight_range_limit/install_generator.rb +1 -1
  30. data/lib/generators/blacklight_range_limit/jsbundling_bl7_fixup_generator.rb +98 -0
  31. data/package-lock.json +8 -7
  32. data/package.json +10 -14
  33. data/spec/components/range_facet_component_spec.rb +41 -32
  34. data/spec/components/range_form_component_spec.rb +2 -2
  35. data/spec/components/range_segments_component_spec.rb +64 -0
  36. data/spec/features/blacklight_range_limit_spec.rb +21 -13
  37. data/spec/features/run_through_spec.rb +157 -0
  38. data/spec/presenters/facet_field_presenter_spec.rb +72 -0
  39. data/spec/presenters/filter_field_spec.rb +36 -1
  40. data/spec/spec_helper.rb +10 -0
  41. data/spec/test_app_templates/Gemfile.extra +11 -1
  42. data/spec/test_app_templates/lib/generators/test_app_generator.rb +37 -6
  43. metadata +25 -51
  44. data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.esm.js +0 -534
  45. data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.esm.js.map +0 -1
  46. data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.umd.js +0 -542
  47. data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.umd.js.map +0 -1
  48. data/app/assets/javascripts/blacklight_range_limit.js +0 -27
  49. data/app/assets/stylesheets/blacklight_range_limit/blacklight_range_limit.css +0 -60
  50. data/app/assets/stylesheets/blacklight_range_limit.css +0 -7
  51. data/app/helpers/range_limit_helper.rb +0 -130
  52. data/app/javascript/blacklight_range_limit/index.js +0 -11
  53. data/app/javascript/blacklight_range_limit/range_limit_distro_facets.js +0 -76
  54. data/app/javascript/blacklight_range_limit/range_limit_plotting.js +0 -186
  55. data/app/javascript/blacklight_range_limit/range_limit_shared.js +0 -112
  56. data/app/javascript/blacklight_range_limit/range_limit_slider.js +0 -163
  57. data/app/javascripts/blacklight_range_limit/index.js +0 -13
  58. data/app/views/blacklight_range_limit/_range_segments.html.erb +0 -8
  59. data/app/views/blacklight_range_limit/range_segments.html.erb +0 -1
  60. data/app/views/catalog/range_limit_panel.html.erb +0 -1
  61. data/rollup.config.js +0 -37
  62. data/spec/features/a_javascript_spec.rb +0 -70
  63. data/spec/helpers/blacklight_range_limit_helper_spec.rb +0 -37
  64. data/vendor/assets/javascripts/bootstrap-slider.js +0 -388
  65. data/vendor/assets/javascripts/flot/jquery.canvaswrapper.js +0 -549
  66. data/vendor/assets/javascripts/flot/jquery.colorhelpers.js +0 -199
  67. data/vendor/assets/javascripts/flot/jquery.event.drag.js +0 -145
  68. data/vendor/assets/javascripts/flot/jquery.flot.browser.js +0 -98
  69. data/vendor/assets/javascripts/flot/jquery.flot.drawSeries.js +0 -662
  70. data/vendor/assets/javascripts/flot/jquery.flot.hover.js +0 -359
  71. data/vendor/assets/javascripts/flot/jquery.flot.js +0 -2818
  72. data/vendor/assets/javascripts/flot/jquery.flot.saturated.js +0 -43
  73. data/vendor/assets/javascripts/flot/jquery.flot.selection.js +0 -527
  74. data/vendor/assets/javascripts/flot/jquery.flot.uiConstants.js +0 -10
  75. data/vendor/assets/stylesheets/slider.css +0 -138
@@ -0,0 +1,345 @@
1
+ //import Chart from 'chart.js/auto';
2
+
3
+ // Selective import to keep package size smaller if we have a bundler that can tree-shake
4
+ // https://www.chartjs.org/docs/latest/getting-started/integration.html
5
+ import {
6
+ Chart,
7
+ LineController,
8
+ LineElement,
9
+ LinearScale,
10
+ PointElement,
11
+ Filler,
12
+ } from "chart.js";
13
+
14
+ Chart.register(
15
+ LineController,
16
+ LineElement,
17
+ LinearScale,
18
+ PointElement,
19
+ Filler
20
+ );
21
+
22
+
23
+ export default class BlacklightRangeLimit {
24
+ static init(args = {}) {
25
+ // args and defaults
26
+ const {
27
+ // a basic vanilla JS onLoad handler as default, but pass in Blacklight.onLoad please
28
+ onLoadHandler = (fn => document.readyState !== 'loading' ? fn() : document.addEventListener('DOMContentLoaded', fn)),
29
+ callback = (range_limit_obj => {}),
30
+ containerQuerySelector = ".limit_content.range_limit"
31
+ } = args;
32
+
33
+ // For turbolinks on_loads, we need to execute this on every page change, and
34
+ // count on being passed Blacklight.onLoad to take care of it. We need to do
35
+ // a new querySelector on every onLoad, because of turbo changes!
36
+ onLoadHandler( () => {
37
+ document.querySelectorAll(containerQuerySelector).forEach( container => {
38
+ const range_limit = new BlacklightRangeLimit(container);
39
+ callback(range_limit);
40
+ });
41
+ });
42
+ }
43
+
44
+ chartEnabled = true;
45
+ textualFacets = true;
46
+ textualFacetsCollapsible = true;
47
+ rangeListHeadingLocalized = undefined;
48
+
49
+ rangeBuckets = []; // array of objects with bucket range info
50
+
51
+ xTicks = []; // array of x values to use as chart ticks
52
+
53
+ lineDataPoints = [] // array of objects in Chart.js line chart data format, { x: xVal, y: yVal }
54
+
55
+ container; // div.range-limit wrapping entire facet display box
56
+ chartCanvasElement; // <canvas> DOM element
57
+
58
+ // container should be a `div.range-limit` that will have within it a `.distribution`
59
+ // with textual distributions that will be turned into a histogram chart.
60
+ constructor(container) {
61
+ this.container = container;
62
+
63
+ if (!this.container) {
64
+ throw new Error("BlacklightRangeLimit missing argument")
65
+ }
66
+
67
+ this.distributionElement = container.querySelector(".distribution")
68
+
69
+ // If there is no distribution element on page, it means we don't have data,
70
+ // nothing to do.
71
+ if (! this.distributionElement) {
72
+ return;
73
+ }
74
+
75
+ this.chartEnabled = (this.container.getAttribute("data-chart-enabled") != "false");
76
+ this.textualFacets = (this.container.getAttribute("data-textual-facets") != "false");
77
+ this.textualFacetsCollapsible = (this.container.getAttribute("data-textual-facets-collapsible") != "false")
78
+
79
+ this.rangeListHeadingLocalized = this.container.getAttribute("data-range-list-heading-localized") || "Range List";
80
+
81
+ const bounding = container.getBoundingClientRect();
82
+ if (bounding.width > 0 || bounding.height > 0) {
83
+ this.setup(); // visible, init now
84
+ } else {
85
+ // Delay setup until someone clicks to open the facet, mainly to avoid making
86
+ // extra http request to server if it will never be needed!
87
+ this.whenBecomesVisible(container, target => this.setup());
88
+ }
89
+ }
90
+
91
+ // if the range fetch link is still in DOM, fetch ranges from back-end,
92
+ // create chart element in DOM (replacing existing fetch link), chart
93
+ // with chart.js, store state in instance variables.
94
+ //
95
+ // This is idempotent in that if the items it creates appear to already have been
96
+ // created, it will skip creating them.
97
+ setup() {
98
+ // we replace this link in DOM after loaded, so if it's there, we need to load
99
+ const loadLink = this.distributionElement.querySelector("a.load_distribution");
100
+
101
+ // What we'll do to put the chart on page whether or not we need to load --
102
+ // when query has range limits, we don't need to load, it's already there.
103
+ let conditonallySetupChart = () => {
104
+ // No need to draw chart for only one or none buckets, not useful
105
+ if (this.chartEnabled && this.rangeBuckets.length > 1) {
106
+ this.chartCanvasElement = this.setupDomForChart();
107
+ this.drawChart(this.chartCanvasElement);
108
+ }
109
+ }
110
+
111
+ if (loadLink) {
112
+ loadLink.innerHTML = loadLink.getAttribute("data-loading-message-html") || "Loading...";
113
+
114
+ fetch(loadLink["href"]).
115
+ then( response => response.ok ? response.text() : Promise.reject(response)).
116
+ then( responseBody => new DOMParser().parseFromString(responseBody, "text/html")).
117
+ then( responseDom => responseDom.querySelector(".facet-values")).
118
+ then( element => this.extractBucketData(element)).
119
+ then( element => this.placeFacetValuesListElement(element)).
120
+ then( _ => { conditonallySetupChart() }).
121
+ catch( error => {
122
+ console.error(error);
123
+ });
124
+ } else {
125
+ const listElement = this.distributionElement.querySelector(".facet-values");
126
+ this.extractBucketData(listElement);
127
+ this.placeFacetValuesListElement(listElement);
128
+ conditonallySetupChart();
129
+ }
130
+ }
131
+
132
+ // Extract our bucket ranges from HTML DOM, and store in our instance variables
133
+ extractBucketData(facetListDom = this.distributionElement.querySelector(".facet-values")) {
134
+ this.rangeBuckets = Array.from(facetListDom.querySelectorAll("ul.facet-values li")).map( li => {
135
+ const from = this.parseNum(li.querySelector("span.from")?.getAttribute("data-blrl-begin") || li.querySelector("span.single")?.getAttribute("data-blrl-single"));
136
+ const to = this.parseNum(li.querySelector("span.to")?.getAttribute("data-blrl-end") || li.querySelector("span.single")?.getAttribute("data-blrl-single"));
137
+ const count = this.parseNum(li.querySelector("span.facet-count,span.count").textContent);
138
+ const avg = (count / (to - from + 1));
139
+
140
+ return {
141
+ from: from,
142
+ to: to,
143
+ count: count,
144
+ avg: avg,
145
+ }
146
+ });
147
+
148
+ this.lineDataPoints = [];
149
+ this.xTicks = [];
150
+
151
+ // Points to graph on our line chart to make it look like a histogram.
152
+ // We use the avg as the y-coord, to make the area of each
153
+ // segment proportional to how many documents it holds.
154
+ this.rangeBuckets.forEach(bucket => {
155
+ this.lineDataPoints.push({ x: bucket.from, y: bucket.avg });
156
+ this.lineDataPoints.push({ x: bucket.to + 1, y: bucket.avg });
157
+
158
+ this.xTicks.push(bucket.from);
159
+ });
160
+
161
+ // Try to even up end point ticks
162
+ if (this.xTicks.length > 3 && (this.xTicks[1] - this.xTicks[0]) != (this.xTicks[2] - this.xTicks[1])) {
163
+ this.xTicks.shift();
164
+ }
165
+ if (this.xTicks[this.xTicks.length - 1] - this.xTicks[this.xTicks.length - 2] == 1) {
166
+ this.xTicks.push(this.rangeBuckets[this.rangeBuckets.length - 1].to + 1);
167
+ }
168
+
169
+ return facetListDom;
170
+ }
171
+
172
+ // Take HTML element with facet list values
173
+ //
174
+ // Possibly hide or wrap it with open/close disclosure, depending on
175
+ // configuration.
176
+ //
177
+ // Place it onto page.
178
+ placeFacetValuesListElement(listElement) {
179
+ if (!listElement) {
180
+ return;
181
+ }
182
+
183
+ listElement.classList.add("mt-3");
184
+
185
+ // No need to show if only 1 or none categories, not useful
186
+ if (!this.textualFacets || this.rangeBuckets.length <= 1) {
187
+ listElement.style["display"] = "none"
188
+ } else if (this.textualFacetsCollapsible) {
189
+ const detailsEl = this.container.ownerDocument.createElement("details");
190
+ detailsEl.innerHTML = "<summary>" + this.rangeListHeadingLocalized + "</summary>";
191
+ detailsEl.classList.add("mt-4", "text-muted");
192
+ detailsEl.appendChild( listElement );
193
+ listElement = detailsEl;
194
+ }
195
+
196
+ this.distributionElement.innerHTML = listElement.outerHTML;
197
+ }
198
+
199
+ setupDomForChart() {
200
+ if(this.chartCanvasElement) {
201
+ // already there, we're good.
202
+ return this.chartCanvasElement;
203
+ }
204
+
205
+ const listDiv = this.distributionElement.querySelector(".facet-values");
206
+ const wrapperDiv = this.container.querySelector("*[data-chart-wrapper=true]");
207
+
208
+
209
+
210
+ // if (this.chartReplacesText) {
211
+ // // We keep the textual facet data as accessible screen-reader, add .sr-only to it though
212
+ // listDiv.classList.add("sr-only")
213
+ // listDiv.classList.add("visually-hidden");
214
+ // }
215
+
216
+ // We create a <chart>, insert it into DOM in wrapper
217
+ this.chartCanvasElement = this.container.ownerDocument.createElement("canvas");
218
+ this.chartCanvasElement.setAttribute("aria-hidden", "true"); // textual facets sr-only are alternative
219
+ this.chartCanvasElement.classList.add("blacklight-range-limit-chart");
220
+ // We set inline-block for compatibility with container-fluid layouts, e.g. when
221
+ // Blacklight's config.full_width_layout = true
222
+ // See: https://github.com/projectblacklight/blacklight_range_limit/pull/269
223
+ this.chartCanvasElement.style.display = 'inline-block';
224
+ wrapperDiv.style.display = "block"; // un-hide it
225
+ wrapperDiv.prepend(this.chartCanvasElement);
226
+
227
+ return this.chartCanvasElement;
228
+ }
229
+
230
+ // Draw chart to a <canvas> element
231
+ //
232
+ // Somehow this method should be locally over-rideable if you want to change parameters for chart, just
233
+ // override and draw the chart how you want?
234
+ drawChart(chartCanvasElement) {
235
+ const minX = this.lineDataPoints[0].x;
236
+ const maxX = this.lineDataPoints[this.lineDataPoints.length - 1].x;
237
+
238
+ // Get aspect ratio from CSS on wrapper element, has to match.
239
+ // Getting responsive chart.js to work was a pain! https://github.com/chartjs/Chart.js/issues/11005
240
+ const wrapper = chartCanvasElement.closest("*[data-chart-wrapper=true]");
241
+ const aspectRatio = window.getComputedStyle(wrapper)?.getPropertyValue("aspect-ratio") || 2;
242
+
243
+ const segmentBorderColor = this.container.getAttribute("data-chart-segment-border-color") || 'rgb(54, 162, 235)';
244
+ const segmentBgColor = this.container.getAttribute("data-chart-segment-bg-color") || 'rgba(54, 162, 235, 0.5)';
245
+
246
+ new Chart(chartCanvasElement.getContext("2d"), {
247
+ type: 'line',
248
+ options: {
249
+ // disable all animations
250
+ animation: {
251
+ duration: 0 // general animation time
252
+ },
253
+ hover: {
254
+ animationDuration: 0 // duration of animations when hovering an item
255
+ },
256
+ responsiveAnimationDuration: 0,
257
+ aspectRatio: aspectRatio,
258
+ resizeDelay: 15, // to debounce a bit
259
+ plugins: {
260
+ legend: false,
261
+ tooltip: { enabled: false} // tooltips don't currently show anything useful for our
262
+ },
263
+ elements: {
264
+ // hide points, and hide hover tooltip, which is not useful in our simulated histogram
265
+ point: {
266
+ radius: 0
267
+ }
268
+ },
269
+ scales: {
270
+ x: {
271
+ // scale should go from our actual min and max x values, we need min/max here and in ticks
272
+ min: minX,
273
+ max: maxX,
274
+ type: 'linear',
275
+ afterBuildTicks: axis => {
276
+ // will autoskip to remove ticks that don't fit, but give it our segment boundaries
277
+ // to start with
278
+ axis.ticks = this.xTicks.map(v => ({ value: v }))
279
+ },
280
+ ticks: {
281
+ min: minX,
282
+ max: maxX,
283
+ autoSkip: true, // supposed to skip when can't fit, but does not always work
284
+ maxRotation: 0,
285
+ maxTicksLimit: 5, // try a number that should fit
286
+ callback: (val, index) => {
287
+ // Don't format for locale, these are years, just display as years.
288
+ return val;
289
+ //
290
+ }
291
+ }
292
+ },
293
+ y: {
294
+ beginAtZero: true,
295
+ // hide axis labels and grid lines on y, to save space and
296
+ // because it's kind of meant to be relative?
297
+ ticks: {
298
+ display: false,
299
+ },
300
+ grid: {
301
+ display: false
302
+ }
303
+ }
304
+ },
305
+ },
306
+ data: {
307
+ datasets: [
308
+ {
309
+ data: this.lineDataPoints,
310
+ stepped: true,
311
+ fill: true,
312
+ // hide segments tha just go y 0 to 0 along the bottom
313
+ segment: {
314
+ borderColor: ctx => {
315
+ return (ctx.p0.parsed.y == 0 && ctx.p1.parsed.y == 0) ? 'transparent' : segmentBorderColor;
316
+ },
317
+ },
318
+ // Fill color under line:
319
+ backgroundColor: segmentBgColor
320
+ }
321
+ ]
322
+ }
323
+ });
324
+ }
325
+
326
+ // takes a string and parses into an integer, but throws away commas first, to avoid truncation when there is a comma
327
+ // use in place of javascript's native parseInt
328
+ parseNum(str) {
329
+ return parseInt( String(str).replace(/[^0-9-]/g, ''), 10);
330
+ }
331
+
332
+ // https://stackoverflow.com/a/70019478/307106
333
+ whenBecomesVisible(element, callback) {
334
+ const resizeWatcher = new ResizeObserver((entries, observer) => {
335
+ for (const entry of entries) {
336
+ if (entry.contentRect.width !== 0 && entry.contentRect.height !== 0) {
337
+ callback.call(entry.target);
338
+ // turn off observing, we only fire once
339
+ observer.unobserve(entry.target);
340
+ }
341
+ }
342
+ });
343
+ resizeWatcher.observe(element);
344
+ }
345
+ }
@@ -4,43 +4,45 @@
4
4
  <% end %>
5
5
 
6
6
  <% component.with_body do %>
7
- <div class="limit_content range_limit <%= @facet_field.key %>-config blrl-plot-config">
7
+ <div class="limit_content range_limit <%= @facet_field.key %>-config blrl-plot-config"
8
+ data-chart-enabled="<%= !! range_config[:chart_js] %>"
9
+ data-chart-segment-border-color="<%= range_config[:chart_segment_border_color] %>"
10
+ data-chart-segment-bg-color="<%= range_config[:chart_segment_bg_color] %>"
11
+ data-textual-facets="<%= !! range_config[:textual_facets] %>"
12
+ data-textual-facets-collapsible="<%= !! range_config[:textual_facets_collapsible] %>"
13
+ data-range-list-heading-localized="<%= t('blacklight.range_limit.range_list_heading') %>"
14
+ >
8
15
  <% if @facet_field.selected_range_facet_item %>
9
- <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field, facet_items: [@facet_field.selected_range_facet_item], classes: ['current']) %>
16
+ <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field, facet_items: [@facet_field.selected_range_facet_item], classes: ['current', 'mb-3']) %>
10
17
  <% end %>
11
18
 
12
19
  <!-- no results profile if missing is selected -->
13
20
  <% unless @facet_field.missing_selected? %>
14
- <!-- you can hide this if you want, but it has to be on page if you want
15
- JS slider and calculated facets to show up, JS sniffs it. -->
16
- <div class="profile">
17
- <% if (min = @facet_field.min) &&
18
- (max = @facet_field.max) %>
19
-
20
- <% if range_config[:segments] != false %>
21
- <div class="distribution subsection <%= 'chart_js' unless range_config[:chart_js] == false %>">
22
- <!-- if we already fetched segments from solr, display them
23
- here. Otherwise, display a link to fetch them, which JS
24
- will AJAX fetch. -->
25
- <% if @facet_field.range_queries.any? %>
26
- <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field) %>
27
- <% else %>
28
- <%= link_to(t('blacklight.range_limit.view_distribution'), range_limit_url(range_start: min, range_end: max), class: "load_distribution") %>
29
- <% end %>
30
- </div>
31
- <% end %>
32
- <p class="range subsection <%= "slider_js" unless range_config[:slider_js] == false %>">
33
- <%= t('blacklight.range_limit.results_range_html', min: min, max: max) %>
34
- </p>
35
- <% end %>
21
+ <%# this has to be on page if you want calculated facets to show up, JS sniffs it.
22
+ it was very hard to get chart.js to be succesfully resonsive, required this wrapper!
23
+ https://github.com/chartjs/Chart.js/issues/11005 -%>
24
+ <div class="chart-wrapper mb-3" data-chart-wrapper="true" style="display: none; position: relative; width: 100%; aspect-ratio: <%= range_config[:chart_aspect_ratio] %>;">
36
25
  </div>
37
26
 
38
27
  <%= render BlacklightRangeLimit::RangeFormComponent.new(facet_field: @facet_field, classes: @classes) %>
39
28
 
40
- <%= with_more_link(key: @facet_field.key, label: @facet_field.label) unless @facet_field.in_modal? %>
29
+ <% if uses_distribution? &&
30
+ (min = @facet_field.min) &&
31
+ (max = @facet_field.max) %>
32
+ <div class="distribution <%= 'chart_js' unless range_config[:chart_js] == false %>">
33
+ <!-- if we already fetched segments from solr, display them
34
+ here. Otherwise, display a link to fetch them, which JS
35
+ will AJAX fetch. -->
36
+ <% if @facet_field.range_queries.any? %>
37
+ <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field) %>
38
+ <% else %>
39
+ <%= link_to(t('blacklight.range_limit.view_distribution'), range_limit_url(range_start: min, range_end: max), class: "load_distribution", "data-loading-message-html": t('blacklight.range_limit.loading_html')) %>
40
+ <% end %>
41
+ </div>
42
+ <% end %>
41
43
 
42
- <% if @facet_field.missing_facet_item && !request.xhr? %>
43
- <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field, facet_items: [@facet_field.missing_facet_item], classes: ['missing', 'subsection']) %>
44
+ <% if @facet_field.missing_facet_item && !request.xhr? && uses_distribution? %>
45
+ <%= render BlacklightRangeLimit::RangeSegmentsComponent.new(facet_field: @facet_field, facet_items: [@facet_field.missing_facet_item], classes: ['missing', 'mt-3']) %>
44
46
  <% end %>
45
47
  <% end %>
46
48
  </div>
@@ -2,14 +2,6 @@
2
2
 
3
3
  module BlacklightRangeLimit
4
4
  class RangeFacetComponent < Blacklight::Component
5
- renders_one :more_link, ->(key:, label:) do
6
- tag.div class: 'more_facets' do
7
- link_to t('blacklight.range_limit.view_larger', field_name: label),
8
- search_facet_path(id: key),
9
- data: { blacklight_modal: 'trigger' }
10
- end
11
- end
12
-
13
5
  delegate :search_action_path, :search_facet_path, to: :helpers
14
6
 
15
7
  def initialize(facet_field:, layout: nil, classes: BlacklightRangeLimit.classes)
@@ -18,12 +10,26 @@ module BlacklightRangeLimit
18
10
  @classes = classes
19
11
  end
20
12
 
13
+ # Don't render if we have no values at all -- most commonly on a zero results page.
14
+ # Normally we'll have at least a min and a max (of values in result set, solr returns),
15
+ # OR a count of objects missing a value -- if we don't have ANY of that, there is literally
16
+ # nothing we can display, and we're probably in a zero results situation.
17
+ def render?
18
+ (@facet_field.min.present? && @facet_field.max.present?) ||
19
+ @facet_field.missing_facet_item.present?
20
+ end
21
+
21
22
  def range_config
22
23
  @facet_field.range_config
23
24
  end
24
25
 
25
26
  def range_limit_url(options = {})
26
- helpers.main_app.url_for(@facet_field.search_state.to_h.merge(range_field: @facet_field.key, action: 'range_limit').merge(options))
27
+ helpers.main_app.url_for(@facet_field.search_state.to_h.merge(range_field: @facet_field.key,
28
+ action: 'range_limit').merge(options))
29
+ end
30
+
31
+ def uses_distribution?
32
+ range_config[:chart_js] || range_config[:textual_facets]
27
33
  end
28
34
  end
29
35
  end
@@ -1,12 +1,20 @@
1
1
  <%= form_tag search_action_path, method: :get, class: [@classes[:form], "range_#{@facet_field.key} d-flex justify-content-center"].join(' ') do %>
2
2
  <%= render hidden_search_state %>
3
3
 
4
- <div class="input-group input-group-sm mb-3 flex-nowrap range-limit-input-group">
5
- <%= render_range_input(:begin, begin_label) %>
6
- <%= render_range_input(:end, end_label) %>
7
- <div class="input-group-append visually-hidden">
4
+ <div class="range-limit-input-group">
5
+ <div class="d-flex justify-content-between align-items-end">
6
+ <div class="d-flex flex-column mr-1 me-1">
7
+ <%= label_tag(begin_input_name, t("blacklight.range_limit.range_begin_short"), class: 'text-muted small mb-1') %>
8
+ <%= number_field_tag(begin_input_name, begin_value_default, class: "form-control form-control-sm range_begin") %>
9
+ </div>
10
+
11
+ <div class="d-flex flex-column ml-1 ms-1">
12
+ <%= label_tag(end_input_name, t("blacklight.range_limit.range_end_short"), class: 'text-muted small mb-1') %>
13
+ <%= number_field_tag(end_input_name, end_value_default, class: "form-control form-control-sm range_end") %>
14
+ </div>
15
+ </div>
16
+ <div class="d-flex justify-content-end mt-2">
8
17
  <%= submit_tag t('blacklight.range_limit.submit_limit'), class: @classes[:submit], name: nil %>
9
18
  </div>
10
- <%= submit_tag t('blacklight.range_limit.submit_limit'), class: @classes[:submit] + " sr-only", "aria-hidden": "true", name: nil %>
11
19
  </div>
12
20
  <% end %>
@@ -9,32 +9,20 @@ module BlacklightRangeLimit
9
9
  @classes = classes
10
10
  end
11
11
 
12
- def begin_label
13
- range_config[:input_label_range_begin] || t("blacklight.range_limit.range_begin", field_label: @facet_field.label)
12
+ def begin_value_default
13
+ @facet_field.selected_range.is_a?(Range) ? @facet_field.selected_range.begin : @facet_field.min
14
14
  end
15
15
 
16
- def end_label
17
- range_config[:input_label_range_end] || t("blacklight.range_limit.range_end", field_label: @facet_field.label)
16
+ def end_value_default
17
+ @facet_field.selected_range.is_a?(Range) ? @facet_field.selected_range.end : @facet_field.max
18
18
  end
19
19
 
20
- def maxlength
21
- range_config[:maxlength]
20
+ def begin_input_name
21
+ "range[#{@facet_field.key}][begin]"
22
22
  end
23
23
 
24
- # type is 'begin' or 'end'
25
- def render_range_input(type, input_label = nil, maxlength_override = nil)
26
- type = type.to_s
27
-
28
- default = if @facet_field.selected_range.is_a?(Range)
29
- case type
30
- when 'begin' then @facet_field.selected_range.first
31
- when 'end' then @facet_field.selected_range.last
32
- end
33
- end
34
-
35
- html = number_field_tag("range[#{@facet_field.key}][#{type}]", default, maxlength: maxlength_override || maxlength, class: "form-control text-center range_#{type}")
36
- html += label_tag("range[#{@facet_field.key}][#{type}]", input_label, class: 'sr-only visually-hidden') if input_label.present?
37
- html
24
+ def end_input_name
25
+ "range[#{@facet_field.key}][end]"
38
26
  end
39
27
 
40
28
  private
@@ -38,7 +38,7 @@ module BlacklightRangeLimit
38
38
  def selected_range_facet_item
39
39
  return unless selected_range
40
40
 
41
- Blacklight::Solr::Response::Facets::FacetItem.new(value: selected_range, hits: response.total)
41
+ Blacklight::Solr::Response::Facets::FacetItem.new(value: selected_range, hits: selected_range_hits)
42
42
  end
43
43
 
44
44
  def missing_facet_item
@@ -77,9 +77,20 @@ module BlacklightRangeLimit
77
77
  return nil unless stats.key? type
78
78
  # StatsComponent returns weird min/max when there are in
79
79
  # fact no values
80
- return nil if response.total == stats['missing']
80
+ return nil if selected_range_hits == stats['missing']
81
81
 
82
82
  stats[type].to_s.gsub(/\.0+/, '')
83
83
  end
84
+
85
+ def selected_range_hits
86
+ return response.total unless response.grouped?
87
+
88
+ # The total doc count when results are *grouped* is located at a
89
+ # different key path than in a normal ungrouped response.
90
+ # If a config.index.group field is set via blacklight_config, use that.
91
+ # Otherwise, use the first (and likely only) group key in the response.
92
+ group_key = blacklight_config.view_config(action_name: :index).group || response.grouped.first.key
93
+ response.dig('grouped', group_key, 'matches')
94
+ end
84
95
  end
85
96
  end
@@ -15,15 +15,15 @@ module BlacklightRangeLimit
15
15
 
16
16
  view_context.t(
17
17
  range_limit_label_key,
18
- begin: format_range_display_value(value.first),
19
- begin_value: value.first,
20
- end: format_range_display_value(value.last),
21
- end_value: value.last
18
+ begin: format_range_display_value(value.begin),
19
+ begin_value: value.begin,
20
+ end: format_range_display_value(value.end),
21
+ end_value: value.end
22
22
  )
23
23
  end
24
24
 
25
25
  def range_limit_label_key
26
- if value.first == value.last
26
+ if value.begin == value.end
27
27
  'blacklight.range_limit.single_html'
28
28
  else
29
29
  'blacklight.range_limit.range_html'
@@ -34,12 +34,7 @@ module BlacklightRangeLimit
34
34
  # A method that is meant to be overridden downstream to format how a range
35
35
  # label might be displayed to a user. By default it just returns the value.
36
36
  def format_range_display_value(value)
37
- if view_context.method(:format_range_display_value).owner == RangeLimitHelper
38
- value
39
- else
40
- Deprecation.warn(BlacklightRangeLimit, 'Helper method #format_range_display_value has been overridden; implement a custom FacetItemPresenter instead')
41
- view_context.format_range_display_value(value, key)
42
- end
37
+ value
43
38
  end
44
39
  end
45
40
  end
@@ -22,7 +22,7 @@ module BlacklightRangeLimit
22
22
  if value.is_a? Range
23
23
  param_key = filters_key
24
24
  params[param_key] = (params[param_key] || {}).dup
25
- params[param_key][config.key] = { begin: value.first, end: value.last }
25
+ params[param_key][config.key] = { begin: value.begin, end: value.end }
26
26
  new_state.reset(params)
27
27
  else
28
28
  super
@@ -55,7 +55,7 @@ module BlacklightRangeLimit
55
55
  elsif params.dig(param_key, config.key).is_a? Hash
56
56
  b_bound = params.dig(param_key, config.key, :begin).presence
57
57
  e_bound = params.dig(param_key, config.key, :end).presence
58
- Range.new(b_bound&.to_i, e_bound&.to_i) if b_bound && e_bound
58
+ Range.new(b_bound&.to_i, e_bound&.to_i) if b_bound || e_bound
59
59
  end
60
60
 
61
61
  f = except.include?(:filters) ? [] : [range].compact
@@ -19,11 +19,11 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_dependency 'blacklight', '>= 7.25.2', '< 9'
21
21
  s.add_dependency 'view_component', ">= 2.54", "< 4"
22
- s.add_dependency 'deprecation'
23
22
 
24
23
  s.add_development_dependency 'rspec', '~> 3.0'
25
24
  s.add_development_dependency 'rspec-rails'
26
25
  s.add_development_dependency 'capybara', '~> 3'
26
+ s.add_development_dependency 'capybara-screenshot', "~> 1.0"
27
27
  s.add_development_dependency 'sqlite3'
28
28
  s.add_development_dependency 'launchy'
29
29
  s.add_development_dependency 'solr_wrapper'
@@ -0,0 +1,9 @@
1
+ # our local js
2
+ pin_all_from File.expand_path("../app/assets/javascripts/blacklight-range-limit", __dir__), under: "blacklight-range-limit", to: "blacklight-range-limit"
3
+
4
+
5
+ # our dependencies also need to be pinned -- chart.js and it's single dependenchy.
6
+ # But instead of including here, we generate into local app, so they can update version
7
+ # numbers themselves if they want to, seems preferable.
8
+ #
9
+ # Chart.js will not work as a vendored pin at present, it has to be pin to "live" CDN.