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