blacklight_range_limit 8.5.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.
- 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.
|