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