blacklight_range_limit 8.4.0 → 9.0.0.beta1

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +117 -23
  3. data/.gitignore +2 -0
  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 +51 -0
  32. data/package.json +10 -11
  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 +26 -44
  44. data/app/assets/javascripts/blacklight_range_limit/range_limit_distro_facets.js +0 -77
  45. data/app/assets/javascripts/blacklight_range_limit/range_limit_plotting.js +0 -185
  46. data/app/assets/javascripts/blacklight_range_limit/range_limit_shared.js +0 -73
  47. data/app/assets/javascripts/blacklight_range_limit/range_limit_slider.js +0 -161
  48. data/app/assets/javascripts/blacklight_range_limit.js +0 -25
  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/views/blacklight_range_limit/_range_segments.html.erb +0 -8
  53. data/app/views/blacklight_range_limit/range_segments.html.erb +0 -1
  54. data/app/views/catalog/range_limit_panel.html.erb +0 -1
  55. data/spec/features/a_javascript_spec.rb +0 -70
  56. data/spec/helpers/blacklight_range_limit_helper_spec.rb +0 -37
  57. data/vendor/assets/javascripts/bootstrap-slider.js +0 -388
  58. data/vendor/assets/javascripts/flot/jquery.canvaswrapper.js +0 -549
  59. data/vendor/assets/javascripts/flot/jquery.colorhelpers.js +0 -199
  60. data/vendor/assets/javascripts/flot/jquery.event.drag.js +0 -145
  61. data/vendor/assets/javascripts/flot/jquery.flot.browser.js +0 -98
  62. data/vendor/assets/javascripts/flot/jquery.flot.drawSeries.js +0 -662
  63. data/vendor/assets/javascripts/flot/jquery.flot.hover.js +0 -359
  64. data/vendor/assets/javascripts/flot/jquery.flot.js +0 -2818
  65. data/vendor/assets/javascripts/flot/jquery.flot.saturated.js +0 -43
  66. data/vendor/assets/javascripts/flot/jquery.flot.selection.js +0 -527
  67. data/vendor/assets/javascripts/flot/jquery.flot.uiConstants.js +0 -10
  68. 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.