View Javadoc
1   /**
2    * This file Copyright (c) 2010-2017 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.vaadin.gwt.client.connector;
35  
36  import info.magnolia.ui.vaadin.editor.PageEditor;
37  import info.magnolia.ui.vaadin.gwt.client.css.PageEditorCssProvider;
38  import info.magnolia.ui.vaadin.gwt.client.editor.dom.CmsNode;
39  import info.magnolia.ui.vaadin.gwt.client.editor.dom.Comment;
40  import info.magnolia.ui.vaadin.gwt.client.editor.dom.MgnlComponent;
41  import info.magnolia.ui.vaadin.gwt.client.editor.dom.MgnlElement;
42  import info.magnolia.ui.vaadin.gwt.client.editor.dom.processor.AbstractMgnlElementProcessor;
43  import info.magnolia.ui.vaadin.gwt.client.editor.dom.processor.CommentProcessor;
44  import info.magnolia.ui.vaadin.gwt.client.editor.dom.processor.ElementProcessor;
45  import info.magnolia.ui.vaadin.gwt.client.editor.dom.processor.MgnlElementProcessorFactory;
46  import info.magnolia.ui.vaadin.gwt.client.editor.dom.processor.ProcessException;
47  import info.magnolia.ui.vaadin.gwt.client.editor.event.ComponentActionEvent;
48  import info.magnolia.ui.vaadin.gwt.client.editor.event.ComponentActionEventHandler;
49  import info.magnolia.ui.vaadin.gwt.client.editor.event.ComponentStartMoveEvent;
50  import info.magnolia.ui.vaadin.gwt.client.editor.event.ComponentStopMoveEvent;
51  import info.magnolia.ui.vaadin.gwt.client.editor.event.EditAreaEvent;
52  import info.magnolia.ui.vaadin.gwt.client.editor.event.EditAreaEventHandler;
53  import info.magnolia.ui.vaadin.gwt.client.editor.event.EditComponentEvent;
54  import info.magnolia.ui.vaadin.gwt.client.editor.event.EditComponentEventHandler;
55  import info.magnolia.ui.vaadin.gwt.client.editor.event.FrameNavigationEvent;
56  import info.magnolia.ui.vaadin.gwt.client.editor.event.FrameNavigationEventHandler;
57  import info.magnolia.ui.vaadin.gwt.client.editor.event.NewAreaEvent;
58  import info.magnolia.ui.vaadin.gwt.client.editor.event.NewAreaEventHandler;
59  import info.magnolia.ui.vaadin.gwt.client.editor.event.NewComponentEvent;
60  import info.magnolia.ui.vaadin.gwt.client.editor.event.NewComponentEventHandler;
61  import info.magnolia.ui.vaadin.gwt.client.editor.event.SelectElementEvent;
62  import info.magnolia.ui.vaadin.gwt.client.editor.event.SelectElementEventHandler;
63  import info.magnolia.ui.vaadin.gwt.client.editor.event.SortComponentEvent;
64  import info.magnolia.ui.vaadin.gwt.client.editor.event.SortComponentEventHandler;
65  import info.magnolia.ui.vaadin.gwt.client.editor.jsni.event.FrameLoadedEvent;
66  import info.magnolia.ui.vaadin.gwt.client.editor.model.Model;
67  import info.magnolia.ui.vaadin.gwt.client.editor.model.ModelImpl;
68  import info.magnolia.ui.vaadin.gwt.client.editor.model.focus.FocusModel;
69  import info.magnolia.ui.vaadin.gwt.client.editor.model.focus.FocusModelImpl;
70  import info.magnolia.ui.vaadin.gwt.client.rpc.PageEditorClientRpc;
71  import info.magnolia.ui.vaadin.gwt.client.rpc.PageEditorServerRpc;
72  import info.magnolia.ui.vaadin.gwt.client.shared.AbstractElement;
73  import info.magnolia.ui.vaadin.gwt.client.shared.AreaElement;
74  import info.magnolia.ui.vaadin.gwt.client.shared.ComponentElement;
75  import info.magnolia.ui.vaadin.gwt.client.shared.PageElement;
76  import info.magnolia.ui.vaadin.gwt.client.widget.PageEditorView;
77  import info.magnolia.ui.vaadin.gwt.client.widget.PageEditorViewImpl;
78  import info.magnolia.ui.vaadin.gwt.client.widget.dnd.MoveWidget;
79  
80  import java.util.List;
81  import java.util.logging.Logger;
82  
83  import com.google.gwt.core.client.GWT;
84  import com.google.gwt.core.client.JavaScriptException;
85  import com.google.gwt.dom.client.Document;
86  import com.google.gwt.dom.client.Element;
87  import com.google.gwt.dom.client.HeadElement;
88  import com.google.gwt.dom.client.LinkElement;
89  import com.google.gwt.dom.client.Node;
90  import com.google.gwt.event.shared.EventBus;
91  import com.google.gwt.event.shared.SimpleEventBus;
92  import com.google.gwt.regexp.shared.MatchResult;
93  import com.google.gwt.regexp.shared.RegExp;
94  import com.google.gwt.user.client.ui.Widget;
95  import com.vaadin.client.BrowserInfo;
96  import com.vaadin.client.communication.RpcProxy;
97  import com.vaadin.client.communication.StateChangeEvent;
98  import com.vaadin.client.ui.AbstractComponentConnector;
99  import com.vaadin.shared.ui.Connect;
100 
101 /**
102  * Client side connector which connects to {@link PageEditor}.
103  */
104 @Connect(PageEditor.class)
105 public class PageEditorConnector extends AbstractComponentConnector implements PageEditorView.Listener {
106 
107     private Logger log = Logger.getLogger(getClass().getName());
108 
109     private static final String IFRAME_PRELOADER_STYLE = "iframe-preloader";
110 
111     private final PageEditorServerRpc rpc = RpcProxy.create(PageEditorServerRpc.class, this);
112 
113     private final EventBus eventBus = new SimpleEventBus();
114 
115     private PageEditorView view;
116     private MoveWidget moveWidget;
117 
118     private Model model;
119 
120     private FocusModel focusModel;
121     private ElementProcessor elementProcessor;
122     private CommentProcessor commentProcessor;
123 
124     // This indicates whether the page-editor is about to open a new page, by following a link inside the iframe.
125     // As the selected element read from the state, won't be up-to-date until the server-side has been notified about
126     // the page-change and pushed by the state to the client, we set the selected element to null when the page is loaded
127     // to make the client fall back to the page-element.
128     // See FrameNavigationEventHandler#onFrameUrlChanged() and FrameLoadedEvent.Handler#handle()
129     private boolean pageChange = false;
130 
131     @Override
132     protected void init() {
133         super.init();
134         this.model = new ModelImpl();
135         this.focusModel = new FocusModelImpl(eventBus, model);
136         this.elementProcessor = new ElementProcessor(eventBus, model);
137         this.commentProcessor = new CommentProcessor();
138 
139         addStateChangeHandler("url", new StateChangeEvent.StateChangeHandler() {
140 
141             @Override
142             public void onStateChanged(StateChangeEvent stateChangeEvent) {
143                 view.setUrl(getState().getUrl());
144                 if (getState().isPreview()) {
145                     view.getFrame().addStyleName(IFRAME_PRELOADER_STYLE);
146                 }
147             }
148         });
149         registerRpc(PageEditorClientRpc.class, new PageEditorClientRpc() {
150 
151             @Override
152             public void refresh() {
153                 view.reload();
154             }
155 
156             @Override
157             public void startMoveComponent() {
158                 MgnlElement selectedElement = model.getMgnlElement(getState().getSelectedElement());
159                 if (selectedElement != null && selectedElement.isComponent()) {
160                     MgnlComponent component = (MgnlComponent) selectedElement;
161                     component.doStartMove(false);
162                     model.setMoving(true);
163                     if (!BrowserInfo.get().isTouchDevice()) {
164                         PageEditorConnector.this.moveWidget = component.getMoveWidget();
165                         moveWidget.attach(view.getFrame());
166                     }
167                 }
168             }
169 
170             @Override
171             public void cancelMoveComponent() {
172                 eventBus.fireEvent(new ComponentStopMoveEvent(null, true));
173             }
174         });
175 
176         /**
177          * The {@link FrameLoadedEvent} is thrown in {@link info.magnolia.ui.vaadin.gwt.client.editor.jsni.AbstractFrameEventHandler#onFrameReady()}
178          * and indicates that the iFrame has been loaded and the document inside it is ready to be parsed.
179          */
180         eventBus.addHandler(FrameLoadedEvent.TYPE, new FrameLoadedEvent.Handler() {
181             @Override
182             public void handle(FrameLoadedEvent event) {
183                 model.reset();
184                 focusModel.resetFocus();
185                 Document document = null;
186                 try {
187                     document = event.getFrame().getContentDocument();
188                 } catch (JavaScriptException e) {
189                     GWT.log("Error getting content document from iframe: " + e.getMessage());
190                 }
191                 if (document == null) {
192                     rpc.selectExternalPage();
193                 } else {
194                     process(document);
195 
196                     view.initKeyEventListeners();
197 
198                     if (!getState().isPreview()) {
199                         view.initDomEventListeners();
200                         focusModel.init();
201                     }
202                     AbstractElement elementToSelect = pageChange ? null : getState().getSelectedElement();
203                     focusModel.selectElement(elementToSelect);
204                 }
205                 PageEditorConnector.this.pageChange = false;
206 
207                 if (event.getFrame() != null) {
208                     rpc.onNavigation(removePageEditorParameters(event.getFrame().getUrl()));
209                 }
210             }
211         });
212 
213         eventBus.addHandler(FrameNavigationEvent.TYPE, new FrameNavigationEventHandler() {
214             @Override
215             public void onFrameUrlChanged(FrameNavigationEvent frameUrlChangedEvent) {
216                 String path = frameUrlChangedEvent.getPath();
217                 RegExp regExp = RegExp.compile("((?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*))(?:(\\?[^#]*))?(?:(#.*))?");
218                 MatchResult matchResult = regExp.exec(path);
219                 if (matchResult != null) {
220                     path = matchResult.getGroup(1);
221 
222                     final String platformId = getState().getPlatformType().getId();
223                     final boolean isPreview = getState().isPreview();
224                     String query = matchResult.getGroup(5);
225                     query = query != null ? removePageEditorParameters(query) : query;
226                     query = query != null && query != "" ? query + "&" : "?";
227                     query += "mgnlChannel=" + platformId;
228                     query += "&mgnlPreview=" + isPreview;
229                     path += query;
230 
231                     String fragment = matchResult.getGroup(6);
232                     if (fragment != null) {
233                         path += fragment;
234                     }
235                 }
236 
237                 PageEditorConnector.this.pageChange = true;
238                 view.setUrl(path);
239             }
240         });
241 
242         eventBus.addHandler(SelectElementEvent.TYPE, new SelectElementEventHandler() {
243             @Override
244             public void onSelectElement(SelectElementEvent selectElementEvent) {
245                 AbstractElement selectedElement = selectElementEvent.getElement();
246                 if (selectedElement instanceof PageElement) {
247                     rpc.selectPage((PageElement) selectedElement);
248                 } else if (selectedElement instanceof AreaElement) {
249                     rpc.selectArea((AreaElement) selectedElement);
250                 } else if (selectedElement instanceof ComponentElement) {
251                     rpc.selectComponent((ComponentElement) selectedElement);
252                 }
253                 view.resetScrollTop();
254             }
255         });
256 
257         eventBus.addHandler(NewAreaEvent.TYPE, new NewAreaEventHandler() {
258             @Override
259             public void onNewArea(NewAreaEvent newAreaEvent) {
260                 if (!getConnection().getMessageSender().hasActiveRequest()) {
261                     rpc.newArea(newAreaEvent.getAreaElement());
262                 }
263             }
264         });
265 
266         eventBus.addHandler(NewComponentEvent.TYPE, new NewComponentEventHandler() {
267             @Override
268             public void onNewComponent(NewComponentEvent newComponentEvent) {
269                 if (!getConnection().getMessageSender().hasActiveRequest()) {
270                     rpc.newComponent(newComponentEvent.getParentAreaElement());
271                 }
272             }
273         });
274 
275         eventBus.addHandler(EditAreaEvent.TYPE, new EditAreaEventHandler() {
276             @Override
277             public void onEditArea(EditAreaEvent editAreaEvent) {
278                 if (!getConnection().getMessageSender().hasActiveRequest()) {
279                     rpc.editArea(editAreaEvent.getAreaElement());
280                 }
281             }
282         });
283 
284         eventBus.addHandler(EditComponentEvent.TYPE, new EditComponentEventHandler() {
285             @Override
286             public void onEditComponent(EditComponentEvent editComponentEvent) {
287                 if (!getConnection().getMessageSender().hasActiveRequest()) {
288                     rpc.editComponent(editComponentEvent.getComponentElement());
289                 }
290             }
291         });
292 
293         eventBus.addHandler(SortComponentEvent.TYPE, new SortComponentEventHandler() {
294             @Override
295             public void onSortComponent(SortComponentEvent sortComponentEvent) {
296                 rpc.sortComponent(sortComponentEvent.getAreaElement());
297             }
298         });
299 
300         eventBus.addHandler(ComponentStartMoveEvent.TYPE, new ComponentStartMoveEvent.CompnentStartMoveEventHandler() {
301             @Override
302             public void onStart(ComponentStartMoveEvent componentStartMoveEvent) {
303                 rpc.startMoveComponent();
304             }
305         });
306 
307         eventBus.addHandler(ComponentStopMoveEvent.TYPE, new ComponentStopMoveEvent.ComponentStopMoveEventHandler() {
308             @Override
309             public void onStop(ComponentStopMoveEvent componentStopMoveEvent) {
310                 if (!componentStopMoveEvent.isServerSide()) {
311                     rpc.stopMoveComponent();
312                 }
313                 if (moveWidget != null && moveWidget.isAttached()) {
314                     moveWidget.detach();
315                 }
316                 model.setMoving(false);
317             }
318         });
319 
320         eventBus.addHandler(ComponentActionEvent.TYPE, new ComponentActionEventHandler() {
321             @Override
322             public void onAction(ComponentActionEvent actionEvent) {
323                 rpc.onClientAction(actionEvent.getElement(), actionEvent.getActionName(), actionEvent.getParameters());
324             }
325         });
326     }
327 
328     @Override
329     protected Widget createWidget() {
330         this.view = new PageEditorViewImpl(eventBus);
331         this.view.setListener(this);
332         return view.asWidget();
333     }
334 
335     @Override
336     public PageEditorState getState() {
337         return (PageEditorState) super.getState();
338     }
339 
340     @Override
341     public void selectElement(Element element) {
342         focusModel.selectElement(element);
343     }
344 
345     private void process(final Document document) {
346         try {
347             injectEditorStyles(document);
348             long startTime = System.currentTimeMillis();
349             processDocument(document, null);
350             processMgnlElements();
351             GWT.log("Time spent to process cms comments: " + (System.currentTimeMillis() - startTime) + "ms");
352         } catch (ProcessException e) {
353             rpc.onError(e.getErrorType(), e.getTagName());
354             GWT.log("Error while processing comment: " + e.getTagName() + " due to " + e.getErrorType());
355             consoleLog("Error while processing comment: " + e.getTagName() + " due to " + e.getErrorType()); // log also into browser console
356         }
357     }
358 
359     private void injectEditorStyles(final Document document) {
360         HeadElement head = HeadElement.as(document.getElementsByTagName("head").getItem(0));
361         for (String uri: PageEditorCssProvider.INSTANCE.getCssLinks()) {
362             LinkElement cssLink = createLinkElement(document, getState().getContextPath() + uri);
363             head.insertFirst(cssLink);
364         }
365     }
366 
367     private LinkElement createLinkElement(final Document document, final String href) {
368         LinkElement cssLink = document.createLinkElement();
369         cssLink.setType("text/css");
370         cssLink.setRel("stylesheet");
371         cssLink.setHref(href);
372         return cssLink;
373     }
374 
375     private void processDocument(Node node, MgnlElement mgnlElement) throws ProcessException {
376         if (mgnlElement == null && model.getRootPage() != null) {
377             mgnlElement = model.getRootPage();
378         }
379         for (int i = 0; i < node.getChildCount(); i++) {
380             Node childNode = node.getChild(i);
381             if (childNode.getNodeType() == Comment.COMMENT_NODE) {
382                 try {
383                     mgnlElement = commentProcessor.process(model, eventBus, childNode, mgnlElement);
384                 } catch (ProcessException e) {
385                     throw e;
386                 } catch (IllegalArgumentException e) {
387                     GWT.log("Not CMSComment element, skipping: " + e.toString());
388                 } catch (Exception e) {
389                     GWT.log("Caught undefined exception: " + e.toString());
390                     consoleLog("Caught undefined exception: " + e.toString()); // log also into browser console
391                 }
392             } else if (childNode.getNodeType() == Node.ELEMENT_NODE) {
393                 Element element = childNode.cast();
394                 elementProcessor.process(element, mgnlElement, getState().isPreview());
395             }
396             processDocument(childNode, mgnlElement);
397         }
398     }
399 
400     private void processMgnlElements() {
401         CmsNode root = model.getRootPage();
402         if (model.getRootPage() == null) {
403             log.warning("Could not find any Magnolia cms:page tag, this might be a static page; not injecting page-editor bars.");
404             return;
405         }
406 
407         List<CmsNode> elements = root.getDescendants();
408         elements.add(root);
409         for (CmsNode element : elements) {
410             try {
411                 AbstractMgnlElementProcessor processor = MgnlElementProcessorFactory.getProcessor(model, element.asMgnlElement());
412                 processor.process();
413             } catch (IllegalArgumentException e) {
414                 GWT.log("MgnlFactory could not instantiate class. The element is neither an area nor component.");
415             } catch (Exception e) {
416                 final String errorMessage = "Error when processing editor components for '" + element.asMgnlElement().getAttribute("path") +
417                         "'. It's possible that the template script for this area or for some subcomponent is incorrect. Please check that all HTML tags are closed properly.\n"
418                         + e.toString();
419                 GWT.log(errorMessage);
420                 consoleLog(errorMessage); // log also into browser console
421             }
422         }
423     }
424 
425     void consoleLog(String message) {
426         log.info("PageEditor: " + message);
427     }
428 
429     /**
430      * Clean up mgnlChannel parameter & mgnlPreview parameter, those param used for page editor only.
431      * The algorithm is to firstly remove mgnlChannel and mgnlPreview without '?', then remove '?' if it stands alone or remove '&' if '?' combine with '&'.
432      * <ul>
433      * This is example:
434      * <li> Input: http://localhost:8185/g.html?mgnlChannel=ee&a=a&mgnlPreview=pp
435      * <li> Replace steps:
436      * <ul>
437      * <li> Step 1: http://localhost:8185/g.html?&a=a
438      * <li> Step 2: http://localhost:8185/g.html?&a=a
439      * <li> Step 3: http://localhost:8185/g.html?a=a
440      * </ul>
441      * </ul>
442      */
443     private String removePageEditorParameters(String url) {
444         return url.replaceAll("(\\&{0,1})(mgnlChannel|mgnlPreview)\\=([^&]+)", "").replaceAll("(\\?$)", "").replaceAll("(\\?\\&)", "?");
445     }
446 }