View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.field.factory;
35  
36  import static info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorConfig.defaultToolbar;
37  import static info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorTextFieldEvents.*;
38  
39  import info.magnolia.i18nsystem.SimpleTranslator;
40  import info.magnolia.jcr.util.PropertyUtil;
41  import info.magnolia.jcr.util.SessionUtil;
42  import info.magnolia.objectfactory.ComponentProvider;
43  import info.magnolia.repository.RepositoryConstants;
44  import info.magnolia.ui.api.app.registry.AppDescriptorRegistry;
45  import info.magnolia.ui.chooser.SingleItemWorkbenchChooser;
46  import info.magnolia.ui.chooser.definition.AppAwareWorkbenchChooserDefinition;
47  import info.magnolia.ui.chooser.definition.ChooserDefinition;
48  import info.magnolia.ui.chooser.definition.SingleItemWorkbenchChooserDefinition;
49  import info.magnolia.ui.dialog.DialogBuilder;
50  import info.magnolia.ui.field.RichTextFieldDefinition;
51  import info.magnolia.ui.framework.overlay.ChooserController;
52  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorConfig;
53  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorConfig.ToolbarGroup;
54  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorTextField;
55  
56  import java.util.Collections;
57  import java.util.List;
58  
59  import javax.inject.Inject;
60  import javax.jcr.Node;
61  import javax.jcr.RepositoryException;
62  
63  import org.apache.commons.lang3.StringUtils;
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  
67  import com.google.gson.Gson;
68  import com.vaadin.server.Sizeable.Unit;
69  import com.vaadin.server.VaadinService;
70  import com.vaadin.ui.Button;
71  import com.vaadin.ui.Component;
72  import com.vaadin.ui.Window;
73  
74  import lombok.SneakyThrows;
75  
76  
77  /**
78   * Creates and configures a CKEditor rich text field based on its definition.
79   */
80  public class RichTextFieldFactory extends AbstractFieldFactory<String, RichTextFieldDefinition> {
81  
82      public static final String PLUGIN_NAME_MAGNOLIALINK = "magnolialink";
83      public static final String PLUGIN_PATH_MAGNOLIALINK = "/VAADIN/js/magnolialink/";
84      public static final String PLUGIN_NAME_MAGNOLIAEXPAND = "magnoliaexpand";
85      public static final String PLUGIN_PATH_MAGNOLIAEXPAND = "/VAADIN/js/magnoliaexpand/";
86      /**
87       * Magic number: z-index of Vaadin's modal Window (see also VAADIN/themes/resurface/dialogs.scss).
88       * We need to force CKEditor elements to the same z-index. This should ensure ckeditor dialogs and
89       * custom config elements in panels (e.g. custom style drop-downs) won't be hidden behind Vaadin's modal windows.
90       * @see #initializeCKEditorConfig()
91       */
92      protected static final int CKEDITOR_BASE_Z_INDEX = 10000;
93  
94      private static final Logger log = LoggerFactory.getLogger(RichTextFieldFactory.class);
95  
96      private final SimpleTranslator i18n;
97      private final ChooserController chooserController;
98  
99      @Inject
100     public RichTextFieldFactory(RichTextFieldDefinition definition, ComponentProvider componentProvider,
101                                 SimpleTranslator i18n, ChooserController chooserController,
102                                 AppDescriptorRegistry appDescriptorRegistry) {
103         super(definition, componentProvider);
104         this.i18n = i18n;
105         this.chooserController = chooserController;
106     }
107 
108     @Override
109     protected Component createFieldComponent() {
110         MagnoliaCKEditorConfig config = initializeCKEditorConfig();
111         return createCkEditorField(config);
112     }
113 
114     protected MagnoliaCKEditorTextField createCkEditorField(MagnoliaCKEditorConfig config) {
115         MagnoliaCKEditorTextField richTextEditor = new MagnoliaCKEditorTextField(config);
116 
117         if (getDefinition().getHeight() > 0) {
118             richTextEditor.setHeight(getDefinition().getHeight(), Unit.PIXELS);
119         }
120 
121         richTextEditor.addListener((eventName, value) -> {
122             if (eventName.equals(EVENT_GET_MAGNOLIA_LINK)) {
123                 try {
124                     Gson gson = new Gson();
125                     PluginData pluginData = gson.fromJson(value, PluginData.class);
126                     openLinkDialog(richTextEditor, pluginData.path, pluginData.workspace);
127                 } catch (Exception e) {
128                     log.error("openLinkDialog failed", e);
129                     richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, i18n.translate("ui-framework.richtexteditorexception.opentargetappfailure"));
130                 }
131             }
132             if (eventName.equals(EVENT_EXPAND_TO_DIALOG)) {
133                 expandEditorToDialog(richTextEditor);
134             }
135         });
136 
137         return richTextEditor;
138     }
139 
140     protected ChooserController getChooserController() {
141         return chooserController;
142     }
143 
144     protected SimpleTranslator getI18n() {
145         return i18n;
146     }
147 
148     private void expandEditorToDialog(MagnoliaCKEditorTextField richTextEditor) {
149         MagnoliaCKEditorConfig config = initializeCKEditorConfig();
150         config.addToRemovePlugins(PLUGIN_NAME_MAGNOLIAEXPAND);
151 
152         MagnoliaCKEditorTextField tobeExpandedRichTextEditor = this.createCkEditorField(config);
153         tobeExpandedRichTextEditor.setValue(richTextEditor.getValue());
154 
155         buildAndOpenExpandDialog(richTextEditor, tobeExpandedRichTextEditor);
156     }
157 
158     private void buildAndOpenExpandDialog(MagnoliaCKEditorTextField richTextEditor, MagnoliaCKEditorTextField tempRichTextEditor) {
159         Button backToForm = new Button(i18n.translate("ui-framework.richTextEditor.expand.dialog.button.backToForm"));
160         backToForm.addStyleName("commit");
161 
162         Window dialog = new DialogBuilder().withContent(tempRichTextEditor)
163                                            .withTitle(richTextEditor.getCaption())
164                                            .wide(true)
165                                            .withCloseListener(e -> richTextEditor.setValue(tempRichTextEditor.getValue()))
166                                            .withActions(Collections.singletonList(backToForm))
167                                            .buildAndOpen();
168         dialog.addStyleName("expand");
169         backToForm.addClickListener(e -> dialog.close());
170     }
171 
172     protected MagnoliaCKEditorConfig initializeCKEditorConfig() {
173 
174         MagnoliaCKEditorConfig config = new MagnoliaCKEditorConfig();
175         String path = VaadinService.getCurrentRequest().getContextPath();
176 
177         // MAGNOLIA LINK PLUGIN — may be used with/without customConfig
178         config.addExternalPlugin(PLUGIN_NAME_MAGNOLIALINK, path + PLUGIN_PATH_MAGNOLIALINK);
179         config.addExternalPlugin(PLUGIN_NAME_MAGNOLIAEXPAND, path + PLUGIN_PATH_MAGNOLIAEXPAND);
180         config.addListenedEvent(EVENT_GET_MAGNOLIA_LINK);
181         config.addListenedEvent(EVENT_EXPAND_TO_DIALOG);
182 
183         config.setBaseFloatZIndex(CKEDITOR_BASE_Z_INDEX);
184 
185         // CUSTOM CONFIG.JS — bypass further config because it can't be overridden otherwise
186         if (StringUtils.isNotBlank(getDefinition().getConfigJsFile())) {
187             config.addExtraConfig("customConfig", "'" + path + getDefinition().getConfigJsFile() + "'");
188             return config;
189         }
190 
191         // DEFINITION
192         if (!getDefinition().isAlignment()) {
193             config.addToRemovePlugins("justify");
194         }
195         if (!getDefinition().isImages()) {
196             config.addToRemovePlugins("image");
197         }
198         if (!getDefinition().isLists()) {
199             // In CKEditor 4.1.1 enterkey depends on indent which itself depends on list
200             config.addToRemovePlugins("enterkey");
201             config.addToRemovePlugins("indent");
202             config.addToRemovePlugins("list");
203         }
204         if (!getDefinition().isSource()) {
205             config.addToRemovePlugins("sourcearea");
206         }
207         if (!getDefinition().isTables()) {
208             config.addToRemovePlugins("table");
209             config.addToRemovePlugins("tabletools");
210             config.addToRemovePlugins("tableselection");
211         }
212 
213         if (getDefinition().getColors() != null) {
214             config.addExtraConfig("colorButton_colors", "'" + getDefinition().getColors() + "'");
215             config.addExtraConfig("colorButton_enableMore", "false");
216             config.addToRemovePlugins("colordialog");
217         } else {
218             config.addToRemovePlugins("colorbutton");
219             config.addToRemovePlugins("colordialog");
220         }
221         if (getDefinition().getFonts() != null) {
222             config.addExtraConfig("font_names", "'" + getDefinition().getFonts() + "'");
223         } else {
224             config.addExtraConfig("removeButtons", "'Font'");
225         }
226         if (getDefinition().getFontSizes() != null) {
227             config.addExtraConfig("fontSize_sizes", "'" + getDefinition().getFontSizes() + "'");
228         } else {
229             config.addExtraConfig("removeButtons", "'FontSize'");
230         }
231         if (getDefinition().getFonts() == null && getDefinition().getFontSizes() == null) {
232             config.addToRemovePlugins("font");
233             config.addToRemovePlugins("fontSize");
234         }
235 
236         // MAGNOLIA EXTRA CONFIG
237         List<ToolbarGroup> toolbars = initializeToolbarConfig();
238         config.addToolbarLine(toolbars);
239 
240         config.addToExtraPlugins(PLUGIN_NAME_MAGNOLIALINK);
241         config.addToExtraPlugins(PLUGIN_NAME_MAGNOLIAEXPAND);
242         config.addToRemovePlugins("elementspath");
243         config.setResizeEnabled(false);
244         config.setContentsCss(path + "/VAADIN/themes/resurface/richtextfield-contents/styles.css");
245 
246         return config;
247     }
248 
249     protected List<ToolbarGroup> initializeToolbarConfig() {
250         return defaultToolbar();
251     }
252 
253     private String mapWorkspaceToChooserDialog(String workspace) {
254         if (workspace.equalsIgnoreCase("dam")) {
255             return "dam";
256         } else if (workspace.equalsIgnoreCase(RepositoryConstants.WEBSITE)) {
257             return "pages-app";
258         }
259 
260         throw new IllegalArgumentException(workspace + " is not a supported workspace by rich text field");
261     }
262 
263     private void openLinkDialog(MagnoliaCKEditorTextField richTextEditor, String path, String workspace) {
264         chooserController.openChooser(createChooserDefinition(workspace), SessionUtil.getNode(workspace, path))
265                 .whenComplete((ChooserController.ChooseResult<Node> result, Throwable e) -> {
266 
267                     if (!result.isChosen()) {
268                         richTextEditor.firePluginEvent(EVENT_CANCEL_LINK);
269                         return;
270                     }
271                     if (e != null) {
272                         String error = i18n.translate("ui-framework.richtexteditorexception.cannotaccessselecteditem");
273                         log.error(error, e);
274                         richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, error);
275                     }
276                     result.getChoice().ifPresent(node -> {
277                         Gson gson = new Gson();
278                         MagnoliaLink mlink = createMagnoliaLink(node);
279                         richTextEditor.firePluginEvent(EVENT_SEND_MAGNOLIA_LINK, gson.toJson(mlink));
280                     });
281                 });
282     }
283 
284     @SneakyThrows(RepositoryException.class)
285     private MagnoliaLink createMagnoliaLink(Node node) {
286         MagnoliaLink mlink = new MagnoliaLink();
287         mlink.identifier = node.getIdentifier();
288         mlink.repository = node.getSession().getWorkspace().getName();
289         mlink.path = node.getPath();
290         mlink.caption = PropertyUtil.getString(node, "title", node.getName());
291 
292         return mlink;
293     }
294 
295     /**
296      * @deprecated since 6.2.3. This will possibly be replaced by retrieving choosers by id via dialog registry.
297      * See https://jira.magnolia-cms.com/browse/MGNLUI-6143
298      */
299     protected ChooserDefinition<Node, SingleItemWorkbenchChooser<Node>> createChooserDefinition(String workspace) {
300         SingleItemWorkbenchChooserDefinition<Node> definition = new SingleItemWorkbenchChooserDefinition<>();
301         final AppAwareWorkbenchChooserDefinition<Node> workbenchChooser = new AppAwareWorkbenchChooserDefinition<>();
302         workbenchChooser.setAppName(mapWorkspaceToChooserDialog(workspace));
303         definition.setWorkbenchChooser(workbenchChooser);
304         return definition;
305     }
306 
307     /**
308      * Link info wrapper.
309      */
310     public static class MagnoliaLink {
311 
312         public String identifier;
313 
314         public String repository;
315 
316         public String path;
317 
318         public String caption;
319 
320     }
321 
322     /**
323      * Plugin data wrapper.
324      */
325     public static class PluginData {
326         public String workspace;
327         public String path;
328     }
329 }