View Javadoc

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