View Javadoc
1   /**
2    * This file Copyright (c) 2012-2015 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.form.field.factory;
35  
36  import info.magnolia.i18nsystem.SimpleTranslator;
37  import info.magnolia.repository.RepositoryConstants;
38  import info.magnolia.ui.api.app.AppController;
39  import info.magnolia.ui.api.app.ChooseDialogCallback;
40  import info.magnolia.ui.api.context.UiContext;
41  import info.magnolia.ui.form.field.definition.RichTextFieldDefinition;
42  import info.magnolia.ui.vaadin.integration.jcr.JcrItemId;
43  import info.magnolia.ui.vaadin.integration.jcr.JcrItemUtil;
44  import info.magnolia.ui.vaadin.richtext.MagnoliaRichTextField;
45  import info.magnolia.ui.vaadin.richtext.MagnoliaRichTextFieldConfig;
46  import info.magnolia.ui.vaadin.richtext.MagnoliaRichTextFieldConfig.ToolbarGroup;
47  
48  import java.util.ArrayList;
49  import java.util.List;
50  
51  import javax.jcr.Node;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import com.google.gson.Gson;
58  import com.google.inject.Inject;
59  import com.vaadin.data.Item;
60  import com.vaadin.server.ClientConnector.AttachEvent;
61  import com.vaadin.server.ClientConnector.AttachListener;
62  import com.vaadin.server.Page;
63  import com.vaadin.server.Sizeable.Unit;
64  import com.vaadin.server.VaadinService;
65  import com.vaadin.server.WebBrowser;
66  import com.vaadin.ui.Field;
67  
68  /**
69   * Creates and configures a rich-text field based on its definition.
70   */
71  public class RichTextFieldFactory extends AbstractFieldFactory<RichTextFieldDefinition, String> {
72  
73      private static final Logger log = LoggerFactory.getLogger(RichTextFieldFactory.class);
74  
75      private static final String PLUGIN_NAME_MAGNOLIALINK = "magnolialink";
76  
77      private static final String PLUGIN_PATH_MAGNOLIALINK = "/VAADIN/js/magnolialink/";
78  
79      /**
80       * Event is transmitted from server to client when link has been selected.
81       */
82      public static final String EVENT_SEND_MAGNOLIA_LINK = "mgnlLinkSelected";
83  
84      /**
85       * Event is transmitted from server to client when link dialog has been
86       * canceled or exception has occurred. In case of exception
87       * the event will carry error message.
88       */
89      public static final String EVENT_CANCEL_LINK = "mgnlLinkCancel";
90  
91      /**
92       * Event is transmitted from client to server when user requests a link dialog.
93       * Event carries optional link that should be treated as default link value.
94       */
95      public static final String EVENT_GET_MAGNOLIA_LINK = "mgnlGetLink";
96  
97      protected final AppController appController;
98      protected final UiContext uiContext;
99      protected final SimpleTranslator i18n;
100     protected MagnoliaRichTextField richTextEditor;
101 
102     @Inject
103     public RichTextFieldFactory(RichTextFieldDefinition definition, Item relatedFieldItem, AppController appController, UiContext uiContext, SimpleTranslator i18n) {
104         super(definition, relatedFieldItem);
105         this.appController = appController;
106         this.uiContext = uiContext;
107         this.i18n = i18n;
108     }
109 
110     @Override
111     protected Field<String> createFieldComponent() {
112 
113         MagnoliaRichTextFieldConfig config = initializeCKEditorConfig();
114         richTextEditor = new MagnoliaRichTextField(config);
115         if (definition.getHeight() > 0) {
116             richTextEditor.setHeight(definition.getHeight(), Unit.PIXELS);
117         }
118 
119         richTextEditor.addAttachListener(new AttachListener() {
120             @Override
121             public void attach(AttachEvent event) {
122                 WebBrowser browser = Page.getCurrent().getWebBrowser();
123                 if (browser.isIOS() || browser.isAndroid()) {
124                     // MGNLUI-1582: Workaround disabling non-operational ckeditor on the iPad or on android devices.
125                     richTextEditor.setEnabled(false);
126                     richTextEditor.setReadOnly(true);
127                     richTextEditor.addStyleName("richtextfield-disabled");
128                 }
129             }
130         });
131 
132         richTextEditor.addListener(new MagnoliaRichTextField.PluginListener() {
133             @Override
134             public void onPluginEvent(String eventName, String value) {
135                 if (eventName.equals(EVENT_GET_MAGNOLIA_LINK)) {
136                     try {
137                         Gson gson = new Gson();
138                         PluginData pluginData = gson.fromJson(value, PluginData.class);
139                         openLinkDialog(pluginData.path, pluginData.workspace);
140                     } catch (Exception e) {
141                         log.error("openLinkDialog failed", e);
142                         richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, i18n.translate("ui-form.richtexteditorexception.opentargetappfailure"));
143                     }
144                 }
145             }
146         });
147 
148         return richTextEditor;
149     }
150 
151     protected MagnoliaRichTextFieldConfig initializeCKEditorConfig() {
152 
153         MagnoliaRichTextFieldConfig config = new MagnoliaRichTextFieldConfig();
154         String path = VaadinService.getCurrentRequest().getContextPath();
155 
156         // MAGNOLIA LINK PLUGIN — may be used with/without customConfig
157         config.addExternalPlugin(PLUGIN_NAME_MAGNOLIALINK, path + PLUGIN_PATH_MAGNOLIALINK);
158         config.addListenedEvent(EVENT_GET_MAGNOLIA_LINK);
159 
160         // CUSTOM CONFIG.JS — bypass further config because it can't be overridden otherwise
161         if (StringUtils.isNotBlank(definition.getConfigJsFile())) {
162             config.addExtraConfig("customConfig", "'" + path + definition.getConfigJsFile() + "'");
163             return config;
164         }
165 
166         // DEFINITION
167         if (!definition.isAlignment()) {
168             config.addToRemovePlugins("justify");
169         }
170         if (!definition.isImages()) {
171             config.addToRemovePlugins("image");
172         }
173         if (!definition.isLists()) {
174             // In CKEditor 4.1.1 enterkey depends on indent which itself depends on list
175             config.addToRemovePlugins("enterkey");
176             config.addToRemovePlugins("indent");
177             config.addToRemovePlugins("list");
178         }
179         if (!definition.isSource()) {
180             config.addToRemovePlugins("sourcearea");
181         }
182         if (!definition.isTables()) {
183             config.addToRemovePlugins("table");
184             config.addToRemovePlugins("tabletools");
185         }
186 
187         if (definition.getColors() != null) {
188             config.addExtraConfig("colorButton_colors", "'" + definition.getColors() + "'");
189             config.addExtraConfig("colorButton_enableMore", "false");
190             config.addToRemovePlugins("colordialog");
191         } else {
192             config.addToRemovePlugins("colorbutton");
193             config.addToRemovePlugins("colordialog");
194         }
195         if (definition.getFonts() != null) {
196             config.addExtraConfig("font_names", "'" + definition.getFonts() + "'");
197         } else {
198             config.addExtraConfig("removeButtons", "'Font'");
199         }
200         if (definition.getFontSizes() != null) {
201             config.addExtraConfig("fontSize_sizes", "'" + definition.getFontSizes() + "'");
202         } else {
203             config.addExtraConfig("removeButtons", "'FontSize'");
204         }
205         if (definition.getFonts() == null && definition.getFontSizes() == null) {
206             config.addToRemovePlugins("font");
207             config.addToRemovePlugins("fontSize");
208         }
209 
210         // MAGNOLIA EXTRA CONFIG
211         List<ToolbarGroup> toolbars = initializeToolbarConfig();
212         config.addToolbarLine(toolbars);
213 
214         config.addToExtraPlugins(PLUGIN_NAME_MAGNOLIALINK);
215         config.addToRemovePlugins("elementspath");
216         config.setBaseFloatZIndex(150);
217         config.setResizeEnabled(false);
218 
219         return config;
220     }
221 
222     protected List<ToolbarGroup> initializeToolbarConfig() {
223         List<ToolbarGroup> toolbars = new ArrayList<ToolbarGroup>();
224         toolbars.add(new ToolbarGroup("basicstyles", new String[] { "Bold", "Italic", "Underline", "SpecialChar" }));
225         toolbars.add(new ToolbarGroup("paragraph", new String[] { "NumberedList", "BulletedList", "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock", "Image", "Table" }));
226         toolbars.add(new ToolbarGroup("links", new String[] { "Link", "InternalLink", "DamLink", "Unlink" }));
227         toolbars.add(new ToolbarGroup("styles", new String[] { "Font", "FontSize", "TextColor" }));
228         toolbars.add(new ToolbarGroup("clipboard", new String[] { "Cut", "Copy", "Paste", "PasteText", "PasteFromWord" }));
229         toolbars.add(new ToolbarGroup("undo", new String[] { "Undo", "Redo" }));
230         toolbars.add(new ToolbarGroup("tools", new String[] { "Source" }));
231         return toolbars;
232     }
233 
234     private String mapWorkSpaceToApp(String workspace) {
235         if (workspace.equalsIgnoreCase("dam")) {
236             return "assets";
237         } else if (workspace.equalsIgnoreCase(RepositoryConstants.WEBSITE)) {
238             return "pages";
239         }
240 
241         return "";
242     }
243 
244     private void openLinkDialog(String path, String workspace) {
245         appController.openChooseDialog(mapWorkSpaceToApp(workspace), uiContext, null, new ChooseDialogCallback() {
246             @Override
247             public void onItemChosen(String actionName, Object chosenValue) {
248                 if (!(chosenValue instanceof JcrItemId)) {
249                     richTextEditor.firePluginEvent(EVENT_CANCEL_LINK);
250                     return;
251                 }
252                 try {
253                     javax.jcr.Item jcrItem = JcrItemUtil.getJcrItem((JcrItemId) chosenValue);
254                     if (!jcrItem.isNode()) {
255                         return;
256                     }
257                     final Node selected = (Node) jcrItem;
258                     Gson gson = new Gson();
259                     MagnoliaLink mlink = new MagnoliaLink();
260                     mlink.identifier = selected.getIdentifier();
261                     mlink.repository = selected.getSession().getWorkspace().getName();
262                     mlink.path = selected.getPath();
263                     if (selected.hasProperty("title")) {
264                         mlink.caption = selected.getProperty("title").getString();
265                     } else {
266                         mlink.caption = selected.getName();
267                     }
268 
269                     richTextEditor.firePluginEvent(EVENT_SEND_MAGNOLIA_LINK, gson.toJson(mlink));
270                 } catch (Exception e) {
271                     String error = i18n.translate("ui-form.richtexteditorexception.cannotaccessselecteditem");
272                     log.error(error, e);
273                     richTextEditor.firePluginEvent(EVENT_CANCEL_LINK, error);
274                 }
275             }
276 
277             @Override
278             public void onCancel() {
279                 richTextEditor.firePluginEvent(EVENT_CANCEL_LINK);
280             }
281         });
282     }
283 
284     /**
285      * Link info wrapper.
286      */
287     protected static class MagnoliaLink {
288 
289         public String identifier;
290 
291         public String repository;
292 
293         public String path;
294 
295         public String caption;
296 
297     }
298 
299     /**
300      * Plugin data wrapper.
301      */
302     protected static class PluginData {
303         public String workspace;
304         public String path;
305     }
306 }