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