1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.admincentral.findbar;
35
36 import info.magnolia.admincentral.findbar.search.apps.AppSearchResultSupplier;
37 import info.magnolia.i18nsystem.I18nizer;
38 import info.magnolia.i18nsystem.SimpleTranslator;
39 import info.magnolia.icons.MagnoliaAppIcon;
40 import info.magnolia.icons.MagnoliaIcons;
41 import info.magnolia.periscope.SupplierAwareSearchResult;
42 import info.magnolia.periscope.search.SearchResultSupplier;
43 import info.magnolia.periscope.search.SearchResultSupplierDefinitionRegistry;
44 import info.magnolia.ui.vaadin.grid.IconRenderer;
45
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Optional;
51 import java.util.function.Consumer;
52
53 import org.apache.commons.lang3.StringUtils;
54 import org.vaadin.extension.gridscroll.GridScrollExtension;
55
56 import com.vaadin.data.SelectionModel;
57 import com.vaadin.server.Resource;
58 import com.vaadin.ui.CheckBox;
59 import com.vaadin.ui.CustomComponent;
60 import com.vaadin.ui.Grid;
61 import com.vaadin.ui.Grid.SelectionMode;
62 import com.vaadin.ui.HorizontalLayout;
63 import com.vaadin.ui.Label;
64 import com.vaadin.ui.renderers.HtmlRenderer;
65
66
67
68
69 class SearchResultsGrid extends CustomComponent {
70
71 private static final int SUPPLIER_MAX_WIDTH = 380;
72 private static final int COL_MIN_WIDTH = 80;
73
74 private final List<Consumer<SupplierAwareSearchResult>> resultPickCallbacks = new ArrayList<>();
75 private final Grid<SupplierAwareSearchResult> grid = new Grid<>();
76 private GridScrollExtension<SupplierAwareSearchResult> gridExt = new GridScrollExtension<>(grid);
77 private final SearchResultSupplierDefinitionRegistry resultSupplierRegistry;
78 private final I18nizer i18nizer;
79 private final SimpleTranslator i18n;
80 private List<SupplierAwareSearchResult> searchResults;
81 private int selection = -1;
82 private Consumer<Boolean> scrollingCallback;
83
84
85
86 private final Map<String, SupplierAwareSearchResult> firstResultsPerSupplier = new HashMap<>();
87
88
89
90 private final Map<String, SupplierAwareSearchResult> lastResultsPerSupplier = new HashMap<>();
91
92 private SearchResultSupplier currentAppSupplier;
93 private String currentQuery;
94
95 SearchResultsGrid(SearchResultSupplierDefinitionRegistry resultSupplierRegistry, I18nizer i18nizer, SimpleTranslator i18n, CheckBox appBiasToggle) {
96 this.resultSupplierRegistry = resultSupplierRegistry;
97 this.i18nizer = i18nizer;
98 this.i18n = i18n;
99 this.setStyleName("search-results-grid");
100
101 grid.addStyleName("findbar-results");
102 SelectionModel<SupplierAwareSearchResult> selectionModel = grid.setSelectionMode(SelectionMode.SINGLE);
103
104 ((SelectionModel.Single<?>) selectionModel).setDeselectAllowed(false);
105
106 this.grid.addItemClickListener(event -> {
107
108 if (event.getItem() != null) {
109 this.triggerResult(event.getItem());
110 }
111 });
112 this.grid.addSelectionListener(event -> {
113 if (event.isUserOriginated()) {
114 this.dropSelection();
115 }
116 });
117
118 this.buildAppColumn(appBiasToggle)
119 .setSortable(false)
120 .setResizable(false)
121 .setExpandRatio(3)
122 .setMinimumWidth(COL_MIN_WIDTH)
123 .setMinimumWidthFromContent(false)
124 .setMaximumWidth(SUPPLIER_MAX_WIDTH)
125 .setStyleGenerator(item -> {
126 String styleName = "supplier";
127 if (firstResultsPerSupplier.get(item.getSupplierName()) == item) {
128 styleName += " first";
129 }
130 if (lastResultsPerSupplier.get(item.getSupplierName()) == item) {
131 styleName += " last";
132 }
133 return styleName;
134 });
135
136 this.grid.addColumn(this::resolveIconResource, new IconRenderer<>())
137 .setCaption(i18n.translate("findbar.type"))
138 .setSortable(false)
139 .setResizable(false)
140 .setExpandRatio(0)
141 .setMinimumWidth(COL_MIN_WIDTH)
142 .setMinimumWidthFromContent(false)
143 .setStyleGenerator(item -> "type");
144
145 this.grid.addColumn(this::buildTitleCellHtml, new HtmlRenderer())
146 .setCaption(i18n.translate("findbar.title"))
147 .setSortable(false)
148 .setResizable(false)
149 .setExpandRatio(4)
150 .setMinimumWidthFromContent(false)
151 .setStyleGenerator(item -> "title-path-and-excerpt");
152
153 this.grid.addColumn(SupplierAwareSearchResult::getLastModifiedBy)
154 .setCaption(i18n.translate("findbar.lasteditor"))
155 .setSortable(false)
156 .setResizable(false)
157 .setExpandRatio(2)
158 .setMinimumWidth(COL_MIN_WIDTH)
159 .setMinimumWidthFromContent(false)
160 .setStyleGenerator(item -> "editors");
161
162 this.grid.addColumn(SupplierAwareSearchResult::getLastModified,
163 lastModifiedOptional -> lastModifiedOptional
164 .map(zonedDateTime -> zonedDateTime.toLocalDate().toString())
165 .orElse(null))
166 .setCaption(i18n.translate("findbar.lastedited"))
167 .setSortable(false)
168 .setResizable(false)
169 .setExpandRatio(2)
170 .setMinimumWidth(COL_MIN_WIDTH)
171 .setMinimumWidthFromContent(false)
172 .setStyleGenerator(item -> "dates");
173
174 this.grid.setStyleGenerator(result -> {
175 String styleName = (StringUtils.isBlank(result.getPath()) && StringUtils.isBlank(result.getContent())) ? "" : "has-path-or-excerpt";
176 if (currentAppSupplier != null && currentAppSupplier.getName().equals(result.getSupplierName())) {
177 styleName += " current-app";
178 }
179 return styleName;
180 });
181
182 gridExt.addGridScrolledListener(event -> {
183 Integer yPos = gridExt.getLastYPosition();
184 if (scrollingCallback != null) {
185 scrollingCallback.accept(yPos != 0);
186 }
187 });
188
189 this.grid.setWidth(100, Unit.PERCENTAGE);
190
191 setSizeFull();
192 setCompositionRoot(grid);
193 }
194
195 private void triggerResult(SupplierAwareSearchResult searchResult) {
196 resultPickCallbacks.forEach(callback -> callback.accept(searchResult));
197 }
198
199 private Grid.Column<SupplierAwareSearchResult, ?> buildAppColumn(CheckBox appBiasToggle) {
200 Grid.Column<SupplierAwareSearchResult, ?> appColumn = this.grid.addColumn(result -> Optional.of(getI18nizedSupplierLabel(result))
201 .filter(StringUtils::isNotEmpty)
202 .orElse(result.getSupplierName()));
203
204 HorizontalLayout appBiasToggleLayout = new HorizontalLayout();
205 appBiasToggleLayout.setStyleName("app-bias-toggle");
206 appBiasToggleLayout.setVisible(false);
207 appBiasToggleLayout.addComponent(new Label(i18n.translate("findbar.filter.appbias")));
208 appBiasToggleLayout.addComponent(appBiasToggle);
209
210 HorizontalLayout columnLayout = new HorizontalLayout();
211 columnLayout.setStyleName("v-grid-column-header-content");
212 columnLayout.addComponent(new Label(i18n.translate("findbar.app")));
213 columnLayout.addComponent(appBiasToggleLayout);
214
215 this.grid.getDefaultHeaderRow().getCell(appColumn).setComponent(columnLayout);
216
217 return appColumn;
218 }
219
220
221 private String getI18nizedSupplierLabel(SupplierAwareSearchResult result) {
222 return i18nizer.decorate(resultSupplierRegistry.getProvider(result.getSupplierName()).get()).getLabel();
223 }
224
225 private String buildTitleCellHtml(SupplierAwareSearchResult result) {
226 String html = String.format("<div class='title'>%s</div>", HtmlHighlighter.highlight(result.getTitle(), currentQuery));
227
228 String excerpt = result.getContent();
229 String path = result.getPath();
230 boolean renderPath = StringUtils.isNotBlank(path) && (StringUtils.isBlank(excerpt) || !excerpt.equals(path));
231 if (renderPath) {
232 html += String.format("<div class='path'>%s</div>", path);
233 }
234
235 if (StringUtils.isNotBlank(excerpt)) {
236 String highlightedExcerpt = HtmlHighlighter.highlight(ExcerptProvider.getExcerpt(currentQuery, excerpt), currentQuery);
237 html += String.format("<div class='excerpt'>%s</div>", highlightedExcerpt);
238 }
239 return html;
240 }
241
242 void clear() {
243 this.grid.setItems();
244 }
245
246 void addSelectionCallback(Consumer<SupplierAwareSearchResult> pickCallback) {
247 this.resultPickCallbacks.add(pickCallback);
248 }
249
250 void setScrollingCallback(Consumer<Boolean> callback) {
251 this.scrollingCallback = callback;
252 }
253
254 private Resource resolveIconResource(SupplierAwareSearchResult result) {
255 final String iconName = result.getType();
256
257 if (AppSearchResultSupplier.APPS_SUPPLIER_NAME.equals(result.getSupplierName())) {
258 return MagnoliaAppIcon.forName(iconName)
259 .or(MagnoliaIcons::forCssClass)
260 .or(MagnoliaIcons.APP)
261 .resolve()
262 .orElse(null);
263 }
264
265 return MagnoliaIcons.forCssClass(iconName).orElse(MagnoliaIcons.APP);
266 }
267
268 void setSearchResults(List<SupplierAwareSearchResult> searchResults, String query) {
269 this.currentQuery = query;
270 this.searchResults = searchResults;
271 this.selection = -1;
272 populateFirstAndLasts(searchResults);
273 this.grid.setItems(searchResults);
274 }
275
276 private void populateFirstAndLasts(List<SupplierAwareSearchResult> results) {
277 this.firstResultsPerSupplier.clear();
278 this.lastResultsPerSupplier.clear();
279 results.forEach(result -> {
280 final String supplierName = result.getSupplierName();
281 if (!this.firstResultsPerSupplier.containsKey(supplierName)) {
282 this.firstResultsPerSupplier.put(supplierName, result);
283 }
284 this.lastResultsPerSupplier.put(supplierName, result);
285 });
286 }
287
288
289
290
291 void triggerSelected() {
292
293 grid.focus();
294
295 Optional<SupplierAwareSearchResult> selected = grid.getSelectedItems().stream().findFirst();
296 if (selected.isPresent()) {
297 triggerResult(selected.get());
298 } else {
299
300 searchResults.stream().findFirst().ifPresent(this::triggerResult);
301 }
302 }
303
304 void selectFirstResult() {
305 this.selection = -1;
306 selectNextResult();
307 }
308
309 void selectNextResult() {
310 if (selection >= searchResults.size() - 1) {
311 return;
312 }
313 selection++;
314 grid.select(searchResults.get(selection));
315 grid.scrollTo(selection);
316 }
317
318 void selectPreviousResult() {
319 if (selection <= 0) {
320 return;
321 }
322 selection--;
323 grid.select(searchResults.get(selection));
324 grid.scrollTo(selection);
325 }
326
327 void dropSelection() {
328 grid.deselectAll();
329 selection = -1;
330 }
331
332 boolean hasSelection() {
333 return selection >= 0;
334 }
335
336 void setCurrentAppSupplier(SearchResultSupplier currentAppSupplier) {
337 this.currentAppSupplier = currentAppSupplier;
338 }
339 }