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