View Javadoc

1   /**
2    * This file Copyright (c) 2011-2014 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.templating.editor.client;
35  
36  
37  import info.magnolia.templating.editor.client.dom.Comment;
38  import info.magnolia.templating.editor.client.dom.MgnlElement;
39  import info.magnolia.templating.editor.client.dom.processor.CommentProcessor;
40  import info.magnolia.templating.editor.client.dom.processor.ElementProcessor;
41  import info.magnolia.templating.editor.client.dom.processor.MgnlElementProcessor;
42  import info.magnolia.templating.editor.client.dom.processor.MgnlElementProcessorFactory;
43  import info.magnolia.templating.editor.client.jsni.JavascriptUtils;
44  import info.magnolia.templating.editor.client.model.ModelStorage;
45  import info.magnolia.templating.editor.client.widget.PreviewChannel;
46  import info.magnolia.templating.editor.client.widget.PreviewChannel.Orientation;
47  import info.magnolia.templating.editor.client.widget.dnd.LegacyDragAndDrop;
48  
49  import java.util.LinkedList;
50  import java.util.List;
51  
52  import com.google.gwt.core.client.EntryPoint;
53  import com.google.gwt.core.client.GWT;
54  import com.google.gwt.dom.client.AnchorElement;
55  import com.google.gwt.dom.client.Document;
56  import com.google.gwt.dom.client.Element;
57  import com.google.gwt.dom.client.FormElement;
58  import com.google.gwt.dom.client.Node;
59  import com.google.gwt.dom.client.NodeList;
60  import com.google.gwt.dom.client.Style.Unit;
61  import com.google.gwt.event.dom.client.ClickEvent;
62  import com.google.gwt.event.dom.client.ClickHandler;
63  import com.google.gwt.event.dom.client.KeyCodes;
64  import com.google.gwt.event.dom.client.KeyDownEvent;
65  import com.google.gwt.event.dom.client.KeyDownHandler;
66  import com.google.gwt.event.dom.client.MouseMoveEvent;
67  import com.google.gwt.event.dom.client.MouseMoveHandler;
68  import com.google.gwt.http.client.Request;
69  import com.google.gwt.http.client.RequestBuilder;
70  import com.google.gwt.http.client.RequestCallback;
71  import com.google.gwt.http.client.RequestException;
72  import com.google.gwt.http.client.Response;
73  import com.google.gwt.http.client.URL;
74  import com.google.gwt.http.client.UrlBuilder;
75  import com.google.gwt.user.client.Window;
76  import com.google.gwt.user.client.Window.ScrollEvent;
77  import com.google.gwt.user.client.Window.ScrollHandler;
78  import com.google.gwt.user.client.ui.HTML;
79  import com.google.gwt.user.client.ui.RootPanel;
80  
81  /**
82   * Client side implementation of the page editor. Outputs ui widgets inside document element (typically the {@code <html>} element).
83   * Since the DOM manipulations performed by the PageEditor (i.e. dynamic creation of edit bars) happen when all other javascripts have already been loaded
84   * (see <a href=http://code.google.com/webtoolkit/doc/latest/DevGuideOrganizingProjects.html#DevGuideBootstrap>GWT bootstrap FAQ</a>),
85   * if you have some custom javascript which needs to operate on elements added by the PageEditor, you will have to use the utility javascript method <code>mgnl.PageEditor.onReady(callback)</code>.
86   * This will ensure that your handler functions are executed when the PageEditor is actually done.
87   * <p>For example:
88   * <pre>
89   * mgnl.PageEditor.onReady( function() {
90   *    alert('hello, page editor is ready.')
91   * });
92   * </pre>
93   * Modules can register multiple callbacks this way. The order in which callbacks are fired is the same in which they were registered.
94   *<p>
95   * @version $Id$
96   *
97   * TODO clean up/refactoring.
98   */
99  public class PageEditor extends HTML implements EntryPoint {
100 
101     private static final String MGNL_CHANNEL_PARAMETER = "mgnlChannel";
102     private static final String MGNL_PREVIEW_PARAMETER = "mgnlPreview";
103     private static final String MGNL_INTERCEPT_PARAMETER = "mgnlIntercept";
104     private static final String MGNL_VERSION_PARAMETER = "mgnlVersion";
105 
106     private static String locale;
107     public final static ModelStorage model = ModelStorage.getInstance();
108     private final LinkedList<MgnlElement> mgnlElements = new LinkedList<MgnlElement>();
109 
110     // In case we're in preview mode, we will stop processing the document, after the pagebar has been injected.
111     public static boolean process = true;
112     private static boolean isPreview = false;
113 
114 
115     @Override
116     public void onModuleLoad() {
117 
118         String mgnlVersion = Window.Location.getParameter(MGNL_VERSION_PARAMETER);
119         if(mgnlVersion != null) {
120             return;
121         }
122 
123         String preview = Window.Location.getParameter(MGNL_PREVIEW_PARAMETER);
124         if(preview != null) {
125             setPreview(Boolean.parseBoolean(preview));
126         }
127 
128         String mgnlChannel = Window.Location.getParameter(MGNL_CHANNEL_PARAMETER);
129         boolean isMobile = "smartphone".equals(mgnlChannel) || "tablet".equals(mgnlChannel);
130 
131         if(isMobile) {
132             GWT.log("Found " + mgnlChannel + " in request, post processing links...");
133             postProcessLinksOnMobilePreview(Document.get().getDocumentElement(), mgnlChannel);
134             return;
135         }
136 
137         JavascriptUtils.setWindowLocation(Window.Location.getPath());
138         // save x/y positon
139         Window.addWindowScrollHandler(new ScrollHandler() {
140 
141             @Override
142             public void onWindowScroll(ScrollEvent event) {
143                 String value = event.getScrollLeft() + ":" + event.getScrollTop();
144                 JavascriptUtils.setEditorPositionCookie(value);
145             }
146         });
147 
148         JavascriptUtils.getCookiePosition();
149 
150         locale = JavascriptUtils.detectCurrentLocale();
151 
152         long startTime = System.currentTimeMillis();
153         processDocument(Document.get().getDocumentElement(), null);
154         processMgnlElements();
155 
156         GWT.log("Time spent to process cms comments: " + (System.currentTimeMillis() - startTime) + "ms");
157 
158         JavascriptUtils.getCookieContentId();
159 
160         if (!isPreview()) {
161             RootPanel.get().addDomHandler(new ClickHandler() {
162                 @Override
163                 public void onClick(ClickEvent event) {
164 
165                     model.getFocusModel().onMouseUp((Element) event.getNativeEvent().getEventTarget().cast());
166                     event.stopPropagation();
167                 }
168             }, ClickEvent.getType());
169 
170             RootPanel.get().addDomHandler(new MouseMoveHandler() {
171 
172                 @Override
173                 public void onMouseMove(MouseMoveEvent event) {
174 
175                     Element moveElement = Document.get().getElementById("mgnlEditorMoveDiv");
176 
177                     if (moveElement != null) {
178                         int x = event.getClientX() + Window.getScrollLeft();
179                         int y = event.getClientY() + 15 + Window.getScrollTop();
180                         moveElement.getStyle().setTop(y, Unit.PX);
181                         moveElement.getStyle().setLeft(x, Unit.PX);
182                     }
183                 }
184             }, MouseMoveEvent.getType());
185         }
186 
187         RootPanel.get().addDomHandler(new KeyDownHandler() {
188             @Override
189             public void onKeyDown(KeyDownEvent event) {
190                 if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
191                     //if we're moving an element abort move
192                     if(LegacyDragAndDrop.isMoving()) {
193                         LegacyDragAndDrop.moveComponentReset();
194                     } else {
195                         PageEditor.enablePreview(!isPreview);
196                     }
197                     event.preventDefault();
198                 }
199             }
200         }, KeyDownEvent.getType());
201 
202         JavascriptUtils.resetEditorCookies();
203 
204         GWT.log("Running onPageEditorReady callbacks...");
205         onPageEditorReady();
206     }
207 
208     public static void openDialog(String dialog, String workspace, String path) {
209         JavascriptUtils.mgnlOpenDialog(path, "", "", dialog, workspace, "", "", "", locale);
210     }
211 
212     public static void deleteComponent(String path) {
213         JavascriptUtils.mgnlDeleteNode(path);
214     }
215 
216     public static void addComponent(String workspace, String path, String nodeName, String availableComponents) {
217         // Not used anymore. The node is passed together with the path
218         String collectionName = null;
219 
220         if (nodeName == null) {
221             nodeName = "mgnlNew";
222         }
223         if (availableComponents == null) {
224             availableComponents = "";
225         }
226         if (availableComponents.contains(",")) {
227             JavascriptUtils.mgnlOpenDialog(path, collectionName, nodeName, availableComponents, workspace, ".magnolia/dialogs/selectParagraph.html", "", "", locale);
228         } else if (!availableComponents.isEmpty()) {
229             JavascriptUtils.mgnlOpenDialog(path, collectionName, nodeName, availableComponents, workspace, ".magnolia/dialogs/editParagraph.html", "", "", locale);
230         }
231     }
232 
233     public static void showTree(String workspace, String path) {
234         JavascriptUtils.showTree(workspace, path);
235 
236     }
237 
238     public static void createComponent(String workspace, String path, String itemType) {
239         GWT.log("Creating [" + itemType + "] in workspace [" + workspace + "] at path [" + path + "]");
240 
241         final StringBuilder url = new StringBuilder();
242         url.append(JavascriptUtils.getContextPath() + ".magnolia/pageeditor/PageEditorServlet");
243         url.append("?action=create");
244         url.append("&workspace=" + workspace);
245         url.append("&path=" + path);
246         url.append("&itemType=" + itemType);
247 
248         RequestBuilder req = new RequestBuilder(RequestBuilder.GET, URL.encode(url.toString()));
249         req.setCallback(new RequestCallback() {
250 
251             @Override
252             public void onResponseReceived(Request request, Response response) {
253                 int status = response.getStatusCode();
254                 String responseText = "";
255                 boolean reload = false;
256 
257                 switch (status) {
258                 case Response.SC_OK:
259                     reload = true;
260                     break;
261                 case Response.SC_UNAUTHORIZED:
262                     responseText = "Is your session expired? Please, try to login again.";
263                     break;
264                 default:
265                     responseText = "See logs for more details.";
266                 }
267 
268                 if (reload) {
269                     UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
270 
271                     urlBuilder.removeParameter("mgnlIntercept");
272                     urlBuilder.removeParameter("mgnlPath");
273 
274                     Window.Location.replace(urlBuilder.buildString());
275                 } else {
276                     Window.alert("An error occurred on the server: response status code is " + status + "\n" + responseText);
277                 }
278             }
279 
280             @Override
281             public void onError(Request request, Throwable exception) {
282                 Window.alert(exception.getMessage());
283             }
284         });
285         try {
286             req.send();
287         } catch (RequestException e) {
288             Window.alert("An error occurred while trying to send a request to the server: " + e.getMessage());
289         }
290 
291     }
292 
293     public static void createChannelPreview(final String channelName, final Orientation orientation) {
294         setPreview(true);
295         GWT.log("Creating preview for channel type [" + channelName + "] ");
296 
297         final UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
298 
299         //always cleanup the url from preview params
300         urlBuilder.removeParameter(MGNL_PREVIEW_PARAMETER);
301         urlBuilder.removeParameter(MGNL_INTERCEPT_PARAMETER);
302         urlBuilder.removeParameter(MGNL_CHANNEL_PARAMETER);
303 
304         urlBuilder.setParameter(MGNL_PREVIEW_PARAMETER, String.valueOf(isPreview()));
305         urlBuilder.setParameter(MGNL_CHANNEL_PARAMETER, channelName);
306         final PreviewChannel previewChannelWidget = new PreviewChannel(urlBuilder.buildString(), orientation, channelName);
307         //this causes the pop up to show
308         previewChannelWidget.center();
309     }
310 
311     private void processDocument(Node node, MgnlElement mgnlElement) {
312         if(!process) {
313             return;
314         }
315         boolean proceed = true;
316         for (int i = 0; i < node.getChildCount(); i++) {
317             Node childNode = node.getChild(i);
318             if (childNode.getNodeType() == Comment.COMMENT_NODE) {
319                 try {
320                     mgnlElement = CommentProcessor.process(childNode, mgnlElement);
321                 }
322                 catch (IllegalArgumentException e) {
323                     GWT.log("Not CMSComment element, skipping: " + e.toString());
324                 }
325                 catch (Exception e) {
326                     GWT.log("Caught undefined exception: " + e.toString());
327                     consoleLog(e.getMessage()); // log also into browser console
328                 }
329             }
330             else if (childNode.getNodeType() == Node.ELEMENT_NODE && mgnlElement != null) {
331                 proceed = ElementProcessor.process(childNode, mgnlElement);
332             }
333             if (proceed) {
334                 processDocument(childNode, mgnlElement);
335             }
336         }
337     }
338 
339     private void processMgnlElements() {
340         List<MgnlElement> rootElements = new LinkedList<MgnlElement>(model.getRootElements());
341         for (MgnlElement root : rootElements) {
342             LinkedList<MgnlElement> elements = new LinkedList<MgnlElement>();
343             elements.add(root);
344             elements.addAll(root.getDescendants());
345 
346             for (MgnlElement mgnlElement : elements) {
347                 try {
348                     MgnlElementProcessor processor = MgnlElementProcessorFactory.getProcessor(mgnlElement);
349                     processor.process();
350                 } catch (IllegalArgumentException e) {
351                     GWT.log("MgnlFactory could not instantiate class. The element is neither an area nor component.");
352                 } catch (IllegalStateException e) {
353                     GWT.log(e.getMessage());
354                     consoleLog(e.getMessage()); // log also into browser console
355                 } catch (Exception e) {
356                     GWT.log("Caught undefined exception: " + e.toString());
357                     consoleLog("Caught undefined exception: " + e.toString()); // log also into browser console
358                 }
359             }
360         }
361 
362     }
363 
364     //FIXME submitting forms still renders website channel and edit bars
365     private void postProcessLinksOnMobilePreview(Element root, String channel) {
366         NodeList<Element> anchors = root.getElementsByTagName("a");
367 
368         final String mobilePreviewParams = MGNL_CHANNEL_PARAMETER+"="+channel+"&"+ MGNL_PREVIEW_PARAMETER+"=true";
369 
370         for (int i = 0; i < anchors.getLength(); i++) {
371             AnchorElement anchor = AnchorElement.as(anchors.getItem(i));
372 
373             GWT.log("Starting to process link " + anchor.getHref());
374 
375             if(JavascriptUtils.isEmpty(anchor.getHref())) {
376                 continue;
377             }
378             String manipulatedHref = anchor.getHref().replaceFirst(Window.Location.getProtocol() + "//" + Window.Location.getHost(), "");
379             String queryString = Window.Location.getQueryString() != null ? Window.Location.getQueryString() : "";
380 
381             GWT.log("query string is " + queryString);
382 
383             String queryStringRegex =  queryString.replaceFirst("\\?", "\\\\?");
384             manipulatedHref = manipulatedHref.replaceFirst(queryStringRegex, "");
385             int indexOfHash = manipulatedHref.indexOf("#");
386 
387             if(indexOfHash != -1) {
388                 manipulatedHref = manipulatedHref.substring(indexOfHash);
389             } else {
390                 if(!queryString.contains(mobilePreviewParams)) {
391                     if(queryString.startsWith("?")) {
392                         queryString += "&" + mobilePreviewParams;
393                     } else {
394                         queryString = "?" + mobilePreviewParams;
395                     }
396                 }
397                 manipulatedHref += queryString;
398             }
399             GWT.log("Resulting link is " + manipulatedHref);
400             anchor.setHref(manipulatedHref);
401         }
402         NodeList<Element> forms = root.getElementsByTagName("form");
403 
404         for (int i = 0; i < forms.getLength(); i++) {
405             FormElement form = FormElement.as(forms.getItem(i));
406             form.setAction(form.getAction().concat("?"+ mobilePreviewParams));
407         }
408     }
409 
410 
411     private native void onPageEditorReady() /*-{
412         var callbacks = $wnd.mgnl.PageEditor.onPageEditorReadyCallbacks
413         if(typeof callbacks != 'undefined') {
414              for(var i=0; i < callbacks.length; i++) {
415                 callbacks[i].apply()
416              }
417          }
418     }-*/;
419 
420     /**
421      * Enables/disables default (desktop) preview.
422      */
423     public static void enablePreview(boolean preview) {
424         setPreview(preview);
425         final UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
426         GWT.log("Current url is [" + urlBuilder.buildString() + "], setting preview to " + isPreview());
427 
428         //always cleanup the url
429         urlBuilder.removeParameter(MGNL_PREVIEW_PARAMETER);
430         urlBuilder.removeParameter(MGNL_INTERCEPT_PARAMETER);
431         urlBuilder.removeParameter(MGNL_CHANNEL_PARAMETER);
432 
433         urlBuilder.setParameter(MGNL_INTERCEPT_PARAMETER, "PREVIEW");
434         urlBuilder.setParameter(MGNL_PREVIEW_PARAMETER, String.valueOf(isPreview()));
435 
436         if(isPreview()) {
437             urlBuilder.setParameter(MGNL_CHANNEL_PARAMETER, "desktop");
438         } else {
439             urlBuilder.setParameter(MGNL_CHANNEL_PARAMETER, "all");
440         }
441 
442         // if url was encoded before it would be double encoded now, if it wasn't encoded before, we should not force encoding either
443         final String newUrl = URL.decode(urlBuilder.buildString());
444         GWT.log("New url is [" + newUrl + "]");
445 
446         Window.Location.replace(newUrl);
447     }
448 
449     /**
450      * @return <code>true</code> if the current page is in the default (desktop) preview mode, <code>false</code> otherwise.
451      */
452     public static boolean isPreview() {
453         return isPreview;
454     }
455 
456     public static void setPreview(boolean preview) {
457         isPreview = preview;
458     }
459 
460     native void consoleLog(String message) /*-{
461                                             if (typeof console == "object") {
462                                                 console.log( "PageEditor: " + message );
463                                             }
464                                            }-*/;
465 }