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