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