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.field.RichTextFieldDefinition;
46  import info.magnolia.ui.chooser.SingleItemWorkbenchChooser;
47  import info.magnolia.ui.chooser.definition.AppAwareWorkbenchChooserDefinition;
48  import info.magnolia.ui.chooser.definition.ChooserDefinition;
49  import info.magnolia.ui.chooser.definition.SingleItemWorkbenchChooserDefinition;
50  import info.magnolia.ui.framework.overlay.ChooserController;
51  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorConfig;
52  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorConfig.ToolbarGroup;
53  import info.magnolia.ui.vaadin.ckeditor.MagnoliaCKEditorTextField;
54  
55  import java.util.List;
56  
57  import javax.inject.Inject;
58  import javax.jcr.Node;
59  import javax.jcr.RepositoryException;
60  
61  import org.apache.commons.lang3.StringUtils;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  import com.google.gson.Gson;
66  import com.vaadin.server.Sizeable.Unit;
67  import com.vaadin.server.VaadinService;
68  import com.vaadin.ui.Component;
69  
70  import lombok.SneakyThrows;
71  
72  
73  /**
74   * Creates and configures a CKEditor rich text field based on its definition.
75   */
76  public class RichTextFieldFactory extends AbstractFieldFactory<String, RichTextFieldDefinition> {
77  
78      public static final String PLUGIN_NAME_MAGNOLIALINK = "magnolialink";
79      public static final String PLUGIN_PATH_MAGNOLIALINK = "/VAADIN/js/magnolialink/";
80  
81      private static final Logger log = LoggerFactory.getLogger(RichTextFieldFactory.class);
82  
83      private final SimpleTranslator i18n;
84      private final ChooserController chooserController;
85      private final AppDescriptorRegistry appDescriptorRegistry;
86  
87      private MagnoliaCKEditorTextField richTextEditor;
88  
89      @Inject
90      public RichTextFieldFactory(RichTextFieldDefinition definition, ComponentProvider componentProvider,
91                                  SimpleTranslator i18n, ChooserController chooserController,
92                                  AppDescriptorRegistry appDescriptorRegistry) {
93          super(definition, componentProvider);
94          this.i18n = i18n;
95          this.chooserController = chooserController;
96          this.appDescriptorRegistry = appDescriptorRegistry;
97      }
98  
99      @Override
100     protected Component createFieldComponent() {
101         MagnoliaCKEditorConfig config = initializeCKEditorConfig();
102         richTextEditor = new MagnoliaCKEditorTextField(config);
103 
104         if (getDefinition().getHeight() > 0) {
105             richTextEditor.setHeight(getDefinition().getHeight(), Unit.PIXELS);
106         }
107 
108         richTextEditor.addListener((eventName, value) -> {
109             if (eventName.equals(EVENT_GET_MAGNOLIA_LINK)) {
110                 try {
111                     Gson gson = new Gson();
112                     PluginData pluginData = gson.fromJson(value, PluginData.class);
113                     openLinkDialog(pluginData.path, pluginData.workspace);
114                 } catch (Exception e) {
115                     log.error("openLinkDialog failed", e);
116                     richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, i18n.translate("ui-framework.richtexteditorexception.opentargetappfailure"));
117                 }
118             }
119         });
120 
121         return richTextEditor;
122     }
123 
124     protected MagnoliaCKEditorConfig initializeCKEditorConfig() {
125 
126         MagnoliaCKEditorConfig config = new MagnoliaCKEditorConfig();
127         String path = VaadinService.getCurrentRequest().getContextPath();
128 
129         // MAGNOLIA LINK PLUGIN — may be used with/without customConfig
130         config.addExternalPlugin(PLUGIN_NAME_MAGNOLIALINK, path + PLUGIN_PATH_MAGNOLIALINK);
131         config.addListenedEvent(EVENT_GET_MAGNOLIA_LINK);
132 
133         // CUSTOM CONFIG.JS — bypass further config because it can't be overridden otherwise
134         if (StringUtils.isNotBlank(getDefinition().getConfigJsFile())) {
135             config.addExtraConfig("customConfig", "'" + path + getDefinition().getConfigJsFile() + "'");
136             return config;
137         }
138 
139         // DEFINITION
140         if (!getDefinition().isAlignment()) {
141             config.addToRemovePlugins("justify");
142         }
143         if (!getDefinition().isImages()) {
144             config.addToRemovePlugins("image");
145         }
146         if (!getDefinition().isLists()) {
147             // In CKEditor 4.1.1 enterkey depends on indent which itself depends on list
148             config.addToRemovePlugins("enterkey");
149             config.addToRemovePlugins("indent");
150             config.addToRemovePlugins("list");
151         }
152         if (!getDefinition().isSource()) {
153             config.addToRemovePlugins("sourcearea");
154         }
155         if (!getDefinition().isTables()) {
156             config.addToRemovePlugins("table");
157             config.addToRemovePlugins("tabletools");
158             config.addToRemovePlugins("tableselection");
159         }
160 
161         if (getDefinition().getColors() != null) {
162             config.addExtraConfig("colorButton_colors", "'" + getDefinition().getColors() + "'");
163             config.addExtraConfig("colorButton_enableMore", "false");
164             config.addToRemovePlugins("colordialog");
165         } else {
166             config.addToRemovePlugins("colorbutton");
167             config.addToRemovePlugins("colordialog");
168         }
169         if (getDefinition().getFonts() != null) {
170             config.addExtraConfig("font_names", "'" + getDefinition().getFonts() + "'");
171         } else {
172             config.addExtraConfig("removeButtons", "'Font'");
173         }
174         if (getDefinition().getFontSizes() != null) {
175             config.addExtraConfig("fontSize_sizes", "'" + getDefinition().getFontSizes() + "'");
176         } else {
177             config.addExtraConfig("removeButtons", "'FontSize'");
178         }
179         if (getDefinition().getFonts() == null && getDefinition().getFontSizes() == null) {
180             config.addToRemovePlugins("font");
181             config.addToRemovePlugins("fontSize");
182         }
183 
184         // MAGNOLIA EXTRA CONFIG
185         List<ToolbarGroup> toolbars = initializeToolbarConfig();
186         config.addToolbarLine(toolbars);
187 
188         config.addToExtraPlugins(PLUGIN_NAME_MAGNOLIALINK);
189         config.addToRemovePlugins("elementspath");
190         config.setBaseFloatZIndex(10000);
191         config.setResizeEnabled(false);
192 
193         return config;
194     }
195 
196     protected List<ToolbarGroup> initializeToolbarConfig() {
197         return defaultToolbar();
198     }
199 
200     private String mapWorkspaceToApp(String workspace) {
201         if (workspace.equalsIgnoreCase("dam")) {
202             return "dam";
203         } else if (workspace.equalsIgnoreCase(RepositoryConstants.WEBSITE)) {
204             return "pages-app";
205         }
206 
207         throw new IllegalArgumentException(workspace + " is not a supported workspace by rich text field");
208     }
209 
210     private void openLinkDialog(String path, String workspace) {
211         chooserController.openChooser(createChooserDefinition(workspace), SessionUtil.getNode(path, workspace))
212                 .whenComplete((ChooserController.ChooseResult<Node> result, Throwable e) -> {
213 
214                     if (!result.isChosen()) {
215                         richTextEditor.firePluginEvent(EVENT_CANCEL_LINK);
216                         return;
217                     }
218                     if (e != null) {
219                         String error = i18n.translate("ui-framework.richtexteditorexception.cannotaccessselecteditem");
220                         log.error(error, e);
221                         richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, error);
222                     }
223                     result.getChoice().ifPresent(node -> {
224                         Gson gson = new Gson();
225                         MagnoliaLink mlink = createMagnoliaLink(node);
226                         richTextEditor.firePluginEvent(EVENT_SEND_MAGNOLIA_LINK, gson.toJson(mlink));
227                     });
228                 });
229     }
230 
231     @SneakyThrows(RepositoryException.class)
232     private MagnoliaLink createMagnoliaLink(Node node) {
233         MagnoliaLink mlink = new MagnoliaLink();
234         mlink.identifier = node.getIdentifier();
235         mlink.repository = node.getSession().getWorkspace().getName();
236         mlink.path = node.getPath();
237         mlink.caption = PropertyUtil.getString(node, "title", node.getName());
238 
239         return mlink;
240     }
241 
242     private ChooserDefinition<Node, SingleItemWorkbenchChooser<Node>> createChooserDefinition(String workspace) {
243         SingleItemWorkbenchChooserDefinition<Node> definition = new SingleItemWorkbenchChooserDefinition<>();
244         AppAwareWorkbenchChooserDefinition<Node> workbenchChooserDefinition = new AppAwareWorkbenchChooserDefinition<>();
245         workbenchChooserDefinition.setAppName(mapWorkspaceToApp(workspace));
246         definition.setWorkbenchChooser(workbenchChooserDefinition);
247         return definition;
248     }
249 
250     /**
251      * Link info wrapper.
252      */
253     public static class MagnoliaLink {
254 
255         public String identifier;
256 
257         public String repository;
258 
259         public String path;
260 
261         public String caption;
262 
263     }
264 
265     /**
266      * Plugin data wrapper.
267      */
268     public static class PluginData {
269         public String workspace;
270         public String path;
271     }
272 }