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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +117 -26
- data/.gitignore +2 -1
- data/.solr_wrapper.yml +8 -0
- data/Gemfile +7 -1
- data/README.md +77 -86
- data/Rakefile +31 -0
- data/VERSION +1 -1
- data/app/assets/javascripts/blacklight-range-limit/index.js +345 -0
- data/app/components/blacklight_range_limit/range_facet_component.html.erb +29 -27
- data/app/components/blacklight_range_limit/range_facet_component.rb +15 -9
- data/app/components/blacklight_range_limit/range_form_component.html.erb +13 -5
- data/app/components/blacklight_range_limit/range_form_component.rb +8 -20
- data/app/presenters/blacklight_range_limit/facet_field_presenter.rb +13 -2
- data/app/presenters/blacklight_range_limit/facet_item_presenter.rb +6 -11
- data/app/presenters/blacklight_range_limit/filter_field.rb +2 -2
- data/blacklight_range_limit.gemspec +1 -1
- data/config/importmap.rb +9 -0
- data/config/locales/blacklight_range_limit.ar.yml +13 -8
- data/config/locales/blacklight_range_limit.de.yml +13 -8
- data/config/locales/blacklight_range_limit.en.yml +5 -1
- data/config/locales/blacklight_range_limit.es.yml +18 -0
- data/config/locales/blacklight_range_limit.it.yml +13 -8
- data/lib/blacklight_range_limit/controller_override.rb +1 -25
- data/lib/blacklight_range_limit/engine.rb +48 -0
- data/lib/blacklight_range_limit/range_limit_builder.rb +40 -3
- data/lib/blacklight_range_limit.rb +8 -31
- data/lib/generators/blacklight_range_limit/assets_generator.rb +82 -18
- data/lib/generators/blacklight_range_limit/install_generator.rb +1 -1
- data/lib/generators/blacklight_range_limit/jsbundling_bl7_fixup_generator.rb +98 -0
- data/package-lock.json +8 -7
- data/package.json +10 -14
- data/spec/components/range_facet_component_spec.rb +41 -32
- data/spec/components/range_form_component_spec.rb +2 -2
- data/spec/components/range_segments_component_spec.rb +64 -0
- data/spec/features/blacklight_range_limit_spec.rb +21 -13
- data/spec/features/run_through_spec.rb +157 -0
- data/spec/presenters/facet_field_presenter_spec.rb +72 -0
- data/spec/presenters/filter_field_spec.rb +36 -1
- data/spec/spec_helper.rb +10 -0
- data/spec/test_app_templates/Gemfile.extra +11 -1
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +37 -6
- metadata +25 -51
- data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.esm.js +0 -534
- data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.esm.js.map +0 -1
- data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.umd.js +0 -542
- data/app/assets/javascripts/blacklight_range_limit/blacklight_range_limit.umd.js.map +0 -1
- data/app/assets/javascripts/blacklight_range_limit.js +0 -27
- data/app/assets/stylesheets/blacklight_range_limit/blacklight_range_limit.css +0 -60
- data/app/assets/stylesheets/blacklight_range_limit.css +0 -7
- data/app/helpers/range_limit_helper.rb +0 -130
- data/app/javascript/blacklight_range_limit/index.js +0 -11
- data/app/javascript/blacklight_range_limit/range_limit_distro_facets.js +0 -76
- data/app/javascript/blacklight_range_limit/range_limit_plotting.js +0 -186
- data/app/javascript/blacklight_range_limit/range_limit_shared.js +0 -112
- data/app/javascript/blacklight_range_limit/range_limit_slider.js +0 -163
- data/app/javascripts/blacklight_range_limit/index.js +0 -13
- data/app/views/blacklight_range_limit/_range_segments.html.erb +0 -8
- data/app/views/blacklight_range_limit/range_segments.html.erb +0 -1
- data/app/views/catalog/range_limit_panel.html.erb +0 -1
- data/rollup.config.js +0 -37
- data/spec/features/a_javascript_spec.rb +0 -70
- data/spec/helpers/blacklight_range_limit_helper_spec.rb +0 -37
- data/vendor/assets/javascripts/bootstrap-slider.js +0 -388
- data/vendor/assets/javascripts/flot/jquery.canvaswrapper.js +0 -549
- data/vendor/assets/javascripts/flot/jquery.colorhelpers.js +0 -199
- data/vendor/assets/javascripts/flot/jquery.event.drag.js +0 -145
- data/vendor/assets/javascripts/flot/jquery.flot.browser.js +0 -98
- data/vendor/assets/javascripts/flot/jquery.flot.drawSeries.js +0 -662
- data/vendor/assets/javascripts/flot/jquery.flot.hover.js +0 -359
- data/vendor/assets/javascripts/flot/jquery.flot.js +0 -2818
- data/vendor/assets/javascripts/flot/jquery.flot.saturated.js +0 -43
- data/vendor/assets/javascripts/flot/jquery.flot.selection.js +0 -527
- data/vendor/assets/javascripts/flot/jquery.flot.uiConstants.js +0 -10
- 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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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', '
|
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,
|
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="
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
13
|
-
|
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
|
17
|
-
|
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
|
21
|
-
|
20
|
+
def begin_input_name
|
21
|
+
"range[#{@facet_field.key}][begin]"
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
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:
|
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
|
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.
|
19
|
-
begin_value: value.
|
20
|
-
end: format_range_display_value(value.
|
21
|
-
end_value: value.
|
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.
|
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
|
-
|
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.
|
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
|
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'
|
data/config/importmap.rb
ADDED
@@ -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.
|