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.ui.editor;
35
36 import static java.util.stream.Collectors.toList;
37
38 import info.magnolia.i18nsystem.SimpleTranslator;
39 import info.magnolia.icons.MagnoliaIcons;
40 import info.magnolia.ui.contentapp.Datasource;
41 import info.magnolia.ui.framework.WithImplementation;
42 import info.magnolia.ui.theme.ResurfaceTheme;
43
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.HashMap;
47 import java.util.LinkedHashMap;
48 import java.util.List;
49 import java.util.Locale;
50 import java.util.Map;
51 import java.util.concurrent.CompletableFuture;
52 import java.util.stream.Stream;
53
54 import javax.inject.Inject;
55
56 import org.apache.commons.lang3.StringUtils;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import com.vaadin.data.Binder;
61 import com.vaadin.data.BinderValidationStatus;
62 import com.vaadin.ui.AbstractOrderedLayout;
63 import com.vaadin.ui.Alignment;
64 import com.vaadin.ui.Button;
65 import com.vaadin.ui.Component;
66 import com.vaadin.ui.CustomField;
67 import com.vaadin.ui.HorizontalLayout;
68 import com.vaadin.ui.Notification;
69 import com.vaadin.ui.VerticalLayout;
70
71 import lombok.EqualsAndHashCode;
72 import lombok.Getter;
73 import lombok.Setter;
74
75
76
77
78
79
80
81 public class MultiFormView<T> implements EditorView<T> {
82
83 private static final Logger log = LoggerFactory.getLogger(MultiFormView.class);
84
85 private final Binder<Map<?, ?>> binder = new Binder<>();
86 private final MultiFormDefinition<T> definition;
87 private final Locale locale;
88 private final Datasource<T> datasource;
89 private final MultiFormState state = new MultiFormState();
90 private final MultiField multiField;
91
92 private VerticalLayout rootLayout = new VerticalLayout();
93
94 @Inject
95 public MultiFormView(MultiFormDefinition<T> definition, LocaleContext localeContext, Datasource<T> datasource) {
96 this.definition = definition;
97 this.locale = localeContext.getDefault();
98 this.datasource = datasource;
99 this.state.entryResolution = create(definition.getEntryResolution(), this.locale);
100 this.rootLayout.setSpacing(true);
101 this.rootLayout.addStyleName("multi-form-view");
102 this.multiField = new MultiField();
103
104 layout();
105
106 Binder.BindingBuilder<Map<?, ?>, Map<?, ?>> multiFieldBinder = binder.forField(multiField);
107 if (definition.isRequired()) {
108 multiFieldBinder.asRequired(definition.getRequiredErrorMessage());
109 }
110 multiFieldBinder
111 .withValidator((new MultiFormViewItemCountValidator(definition.getSizeErrorMessage(), definition.getMinItems(), definition.getMaxItems())))
112 .withValidationStatusHandler(new ValidationStatusHandler(multiField))
113 .bind(o -> null, (o, o2) -> {
114 });
115 }
116
117
118
119
120 @Deprecated
121 public MultiFormView(MultiFormDefinition<T> definition, SimpleTranslator i18n, LocaleContext localeContext, Datasource<T> datasource) {
122 this(definition, localeContext, datasource);
123 }
124
125 @Override
126 public List<BinderValidationStatus<?>> validate() {
127 return Stream.concat(
128 Stream.of(binder.validate()),
129 getAllChildren().flatMap(view -> view.validate().stream())
130 ).collect(toList());
131 }
132
133 @Override
134 public void write(T item) {
135 Map<EditorView<T>, ItemProviderStrategy<T, T>> children = state.children;
136 state.removedItemAccessors.stream()
137 .map(accessor -> accessor.read(item, locale))
138 .forEach(itemToMaybeDelete ->
139 itemToMaybeDelete
140 .ifPresent(this.datasource::remove));
141
142 List<T> itemOrder = new ArrayList<>();
143 state.order.forEach(subForm -> writeSubForm(item, children, itemOrder, subForm));
144
145 create(this.definition.getOrderHandler(), locale).applyOrder(itemOrder);
146 }
147
148 protected void writeSubForm(T item, Map<EditorView<T>, ItemProviderStrategy<T, T>> children, List<T> itemOrder, EditorView<T> subForm) {
149 children.get(subForm).read(item, locale).ifPresent(localisedItem -> {
150 itemOrder.add(localisedItem);
151 subForm.write(localisedItem);
152 });
153 }
154
155 @Override
156 public Component asVaadinComponent() {
157 return multiField;
158 }
159
160 @Override
161 public void populate(T item) {
162 getAllChildren().forEach(subForm -> {
163 rootLayout.removeComponent(subForm.asVaadinComponent());
164 subForm.destroy();
165 });
166
167 state.clear();
168 populateSubNodes(item);
169 layout();
170 }
171
172 protected void populateSubNodes(T item) {
173 state.entryResolution.resolveForRoot(item).forEach(propertyDefinition -> {
174 if (propertyDefinition != null) {
175 EditorView<T> subForm = createSubForm(propertyDefinition);
176 addSubFormToState(subForm, propertyDefinition);
177 populate(item, subForm);
178 }
179 });
180 }
181
182 protected EditorView<T> createSubForm(ComplexPropertyDefinition<T> propertyDefinition) {
183 final EditorView<T> subForm = create("child", propertyDefinition.getEditorDefinition());
184 if (StringUtils.isEmpty(subForm.asVaadinComponent().getCaption())) {
185 subForm.asVaadinComponent().setCaption(propertyDefinition.getLabel());
186 }
187 return subForm;
188 }
189
190 protected void addSubFormToState(EditorView<T> subForm, ComplexPropertyDefinition<T> propertyDefinition) {
191 ItemProviderStrategy<T, T> itemProviderStrategy = create(propertyDefinition.getItemProvider());
192 state.children.put(subForm, itemProviderStrategy);
193 state.order.add(subForm);
194 binder.validate();
195 }
196
197 private void populate(T item, EditorView<T> subForm) {
198 state.children.get(subForm).read(item, locale).ifPresent(subForm::populate);
199 }
200
201 public void layout() {
202 rootLayout.removeAllComponents();
203 rootLayout.setMargin(false);
204 state.children.keySet().stream()
205 .map(this::wrapChildForm)
206 .forEach(this.rootLayout::addComponent);
207 attachAddButton();
208 }
209
210 protected Component wrapChildForm(EditorView<T> subForm) {
211 HorizontalLayout wrapLayout = new HorizontalLayout();
212 wrapLayout.setWidth("100%");
213 subForm.asVaadinComponent().addStyleName("multi-form-entry-content");
214
215 HorizontalLayout buttonLayout = createButtonsLayout(subForm, wrapLayout);
216 wrapLayout.addComponents(subForm.asVaadinComponent(), buttonLayout);
217 wrapLayout.setExpandRatio(subForm.asVaadinComponent(), 1f);
218 wrapLayout.setComponentAlignment(buttonLayout, Alignment.TOP_CENTER);
219 wrapLayout.addStyleName("multi-form-entry");
220
221 return wrapLayout;
222 }
223
224 protected HorizontalLayout createButtonsLayout(EditorView<T> subForm, HorizontalLayout wrapLayout) {
225 HorizontalLayout buttonLayout = new HorizontalLayout();
226 buttonLayout.setStyleName("multi-form-entry-control");
227 buttonLayout.setSpacing(false);
228
229 if (!(definition.getOrderHandler() instanceof MultiFormDefinition.OrderHandlerDefinition.Noop)) {
230 buttonLayout.addComponents(
231 new Button(MagnoliaIcons.ARROW2_N, e -> onMove(wrapLayout, true)),
232 new Button(MagnoliaIcons.ARROW2_S, e -> onMove(wrapLayout, false))
233 );
234 }
235 if (definition.isCanRemoveItems()) {
236 Button removeButton = new Button(MagnoliaIcons.TRASH, e -> onDelete(subForm));
237 removeButton.setDescription(definition.getButtonSelectRemoveLabel());
238 buttonLayout.addComponent(removeButton);
239 }
240
241 buttonLayout.forEach(button -> button.addStyleName(ResurfaceTheme.BUTTON_ICON));
242 return buttonLayout;
243 }
244
245 protected void attachAddButton() {
246 Button addButton = new Button(definition.getButtonSelectAddLabel());
247 addButton.addStyleName("add-multi-form-entry-button");
248 addButton.addClickListener(e -> {
249 state.entryResolution.pick().thenAccept(propertyDefinition -> {
250 if (propertyDefinition != null) {
251 EditorView<T> subForm = createSubForm(propertyDefinition);
252 addSubFormToState(subForm, propertyDefinition);
253 subForm.applyDefaults();
254 rootLayout.addComponent(wrapChildForm(subForm), rootLayout.getComponentCount() - 1);
255 }
256 }).exceptionally(ex -> {
257 log.warn("Failed to create a multi field entry", ex);
258 Notification.show("Failed to create multi field entry");
259 return null;
260 });
261 });
262
263 rootLayout.addComponent(addButton);
264 }
265
266 private Stream<EditorView<T>> getAllChildren() {
267 return state.children.keySet().stream();
268 }
269
270 protected void onMove(AbstractOrderedLayout movedLayout, boolean moveUp) {
271 int currentPosition = rootLayout.getComponentIndex(movedLayout);
272 int newPosition = moveUp ? currentPosition - 1 : currentPosition + 1;
273
274 if (currentPosition == 0 && moveUp) {
275 return;
276 }
277
278 if (newPosition > state.order.size() - 1) {
279 return;
280 }
281
282 Collections.swap(state.order, currentPosition, newPosition);
283
284 rootLayout.replaceComponent(markAsDirtyRecursive(currentPosition), markAsDirtyRecursive(newPosition));
285 }
286
287 private Component markAsDirtyRecursive(int position) {
288 final Component component = rootLayout.getComponent(position);
289 component.markAsDirtyRecursive();
290 return component;
291 }
292
293 protected void onDelete(EditorView<T> subForm) {
294 rootLayout.removeComponent(subForm.asVaadinComponent().getParent());
295
296 state.removedItemAccessors.add(state.children.get(subForm));
297 state.children.remove(subForm);
298 state.order.remove(subForm);
299 binder.validate();
300 }
301
302 protected MultiFormState getState() {
303 return state;
304 }
305
306 protected Locale getLocale() {
307 return locale;
308 }
309
310 private class MultiField extends CustomField<Map<?, ?>> {
311
312 MultiField() {
313 getContent();
314 }
315
316 @Override
317 protected Component initContent() {
318 return rootLayout;
319 }
320
321 @Override
322 protected void doSetValue(Map<?, ?> value) {
323 }
324
325 @Override
326 public Map<?, ?> getValue() {
327 return state.children;
328 }
329
330 @Override
331 public Map<?, ?> getEmptyValue() {
332 return new HashMap<>();
333 }
334 }
335
336 protected class MultiFormState {
337
338 Map<EditorView<T>, ItemProviderStrategy<T, T>> children = new LinkedHashMap<>();
339 List<ItemProviderStrategy<T, T>> removedItemAccessors = new ArrayList<>();
340 List<EditorView<T>> order = new ArrayList<>();
341 protected EntryResolution<T> entryResolution;
342
343 void clear() {
344 children.clear();
345 removedItemAccessors.clear();
346 order.clear();
347 }
348
349 public List<EditorView<T>> getOrder() {
350 return order;
351 }
352
353 public EntryResolution<T> getEntryResolution() {
354 return entryResolution;
355 }
356
357 public Map<EditorView<T>, ItemProviderStrategy<T, T>> getChildren() {
358 return children;
359 }
360 }
361
362
363
364
365
366
367 public interface EntryResolution<T> {
368
369 Stream<ComplexPropertyDefinition<T>> resolveForRoot(T rootDatasource);
370
371 CompletableFuture<ComplexPropertyDefinition<T>> pick();
372
373 @Getter
374 @Setter
375 @EqualsAndHashCode
376 class Definition<T> implements WithImplementation<EntryResolution<T>> {
377 private Class<? extends EntryResolution<T>> implementationClass;
378 }
379 }
380 }