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.PreviewChannelWidget.Orientation;
38  import info.magnolia.templating.editor.client.dom.CMSComment;
39  import info.magnolia.templating.editor.client.dom.Comment;
40  import info.magnolia.templating.editor.client.dom.MgnlElement;
41  import info.magnolia.templating.editor.client.jsni.LegacyJavascript;
42  import info.magnolia.templating.editor.client.model.ModelStorage;
43  
44  import java.util.Iterator;
45  import java.util.LinkedList;
46  import java.util.List;
47  
48  
49  import com.google.gwt.core.client.EntryPoint;
50  import com.google.gwt.core.client.GWT;
51  import com.google.gwt.dom.client.AnchorElement;
52  import com.google.gwt.dom.client.Document;
53  import com.google.gwt.dom.client.Element;
54  import com.google.gwt.dom.client.Node;
55  import com.google.gwt.dom.client.NodeList;
56  import com.google.gwt.event.dom.client.MouseDownEvent;
57  import com.google.gwt.event.dom.client.MouseDownHandler;
58  import com.google.gwt.event.dom.client.MouseUpEvent;
59  import com.google.gwt.event.dom.client.MouseUpHandler;
60  import com.google.gwt.http.client.Request;
61  import com.google.gwt.http.client.RequestBuilder;
62  import com.google.gwt.http.client.RequestCallback;
63  import com.google.gwt.http.client.RequestException;
64  import com.google.gwt.http.client.Response;
65  import com.google.gwt.http.client.URL;
66  import com.google.gwt.http.client.UrlBuilder;
67  import com.google.gwt.user.client.Event;
68  import com.google.gwt.user.client.Window;
69  import com.google.gwt.user.client.ui.HTML;
70  import com.google.gwt.user.client.ui.RootPanel;
71  
72  /**
73   * Client side implementation of the page editor. Outputs ui widgets inside document element (typically the {@code <html>} element).
74   * Since the DOM manipulations performed by the PageEditor (i.e. dynamic creation of edit bars) happen when all other javascripts have already been loaded
75   * (see <a href=http://code.google.com/webtoolkit/doc/latest/DevGuideOrganizingProjects.html#DevGuideBootstrap>GWT bootstrap FAQ</a>),
76   * 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>.
77   * This will ensure that your handler functions are executed when the PageEditor is actually done.
78   * <p>For example:
79   * <pre>
80   * mgnl.PageEditor.onReady( function() {
81   *    alert('hello, page editor is ready.')
82   * });
83   * </pre>
84   * Modules can register multiple callbacks this way. The order in which callbacks are fired is the same in which they were registered.
85   *<p>
86   * @version $Id$
87   *
88   * TODO clean up/refactoring: this class is getting messy.
89   */
90  public class PageEditor extends HTML implements EntryPoint {
91  
92      public static final String SKIP_PAGE_EDITOR_DOM_PROCESSING = "skipPageEditorDOMProcessing";
93  
94      private boolean pageEditBarAlreadyProcessed = false;
95      private String locale;
96      static ModelStorage model = ModelStorage.getInstance();
97      private LinkedList<MgnlElement> mgnlElements = new LinkedList<MgnlElement>();
98  
99      // In case we're in preview mode, we will stop processing the document, after the pagebar has been injected.
100     private boolean process = true;
101 
102     @Override
103     public void onModuleLoad() {
104 
105         if(Window.Location.getParameter(SKIP_PAGE_EDITOR_DOM_PROCESSING) != null) {
106             GWT.log("Found " + SKIP_PAGE_EDITOR_DOM_PROCESSING + " in request, skipping DOM processing...");
107             postProcessLinksOnMobilePreview(Document.get().getDocumentElement());
108             return;
109         }
110 
111         locale = LegacyJavascript.detectCurrentLocale();
112 
113         long startTime = System.currentTimeMillis();
114         processDocument(Document.get().getDocumentElement(), null);
115 
116         processMgnlElements();
117         GWT.log("Time spent to process cms comments: " + (System.currentTimeMillis() - startTime) + "ms");
118 
119         model.getFocusModel().reset();
120 
121         RootPanel.get().addDomHandler(new MouseUpHandler() {
122             @Override
123             public void onMouseUp(MouseUpEvent event) {
124 
125                 model.getFocusModel().onMouseUp((Element)event.getNativeEvent().getEventTarget().cast());
126                 event.stopPropagation();
127             }
128         }, MouseUpEvent.getType());
129 
130         RootPanel.get().addDomHandler(new MouseDownHandler() {
131             @Override
132             public void onMouseDown(MouseDownEvent event) {
133 
134                 model.getFocusModel().onMouseDown((Element)event.getNativeEvent().getEventTarget().cast());
135                 event.stopPropagation();
136             }
137         }, MouseDownEvent.getType());
138 
139         GWT.log("Trying to run onPageEditorReady callbacks...");
140         onPageEditorReady();
141     }
142 
143     @Override
144     public void onBrowserEvent(Event event) {
145         super.onBrowserEvent(event);
146     }
147 
148     /**
149      * TODO: rename and/or remove arguments no longer needed (collectionName, nodeName).
150      */
151     public void openDialog(String dialog, String workspace, String path, String collectionName, String nodeName) {
152         if (collectionName == null) {
153             collectionName = "";
154         }
155         if (nodeName == null) {
156             nodeName = "";
157         }
158 
159         LegacyJavascript.mgnlOpenDialog(path, collectionName, nodeName, dialog, workspace, "", "", "", locale);
160     }
161 
162     public void moveComponentStart(String id) {
163         LegacyJavascript.mgnlMoveNodeStart(id);
164     }
165 
166     public void moveComponentEnd(AbstractBarWidget source, String path) {
167         LegacyJavascript.mgnlMoveNodeEnd(source.getElement(), path);
168     }
169 
170     public void moveComponentOver(AbstractBarWidget source) {
171         LegacyJavascript.mgnlMoveNodeHigh(source.getElement());
172     }
173 
174     public void moveComponentOut(AbstractBarWidget source) {
175         LegacyJavascript.mgnlMoveNodeReset(source.getElement());
176     }
177 
178     public void deleteComponent(String path) {
179         LegacyJavascript.mgnlDeleteNode(path);
180     }
181 
182     public void addComponent(String workspace, String path, String nodeName, String availableComponents) {
183 
184         // Not used anymore. The node is passed together with the path
185         String collectionName = null;
186 
187         if (nodeName == null) {
188             nodeName = "mgnlNew";
189         }
190         if (availableComponents == null) {
191             availableComponents = "";
192         }
193         LegacyJavascript.mgnlOpenDialog(path, collectionName, nodeName, availableComponents, workspace, ".magnolia/dialogs/selectParagraph.html", "", "", locale);
194     }
195 
196     public void preview(boolean isPreview) {
197         LegacyJavascript.mgnlPreview(isPreview);
198     }
199 
200     public void showTree(String workspace, String path) {
201         LegacyJavascript.showTree(workspace, path);
202 
203     }
204 
205     public void createComponent(String workspace, String path, String itemType) {
206         GWT.log("Creating [" + itemType + "] in workspace [" + workspace + "] at path [" + path + "]");
207 
208         final StringBuilder url = new StringBuilder();
209         url.append(LegacyJavascript.getContextPath() + ".magnolia/pageeditor/PageEditorServlet");
210         url.append("?action=create");
211         url.append("&workspace=" + workspace);
212         url.append("&path=" + path);
213         url.append("&itemType=" + itemType);
214 
215         RequestBuilder req = new RequestBuilder(RequestBuilder.GET, URL.encode(url.toString()));
216         req.setCallback(new RequestCallback() {
217 
218             @Override
219             public void onResponseReceived(Request request, Response response) {
220                 int status = response.getStatusCode();
221                 String responseText = "";
222                 boolean reload = false;
223 
224                 switch (status) {
225                     case Response.SC_OK:
226                         reload = true;
227                         break;
228                     case Response.SC_UNAUTHORIZED:
229                         responseText = "Is your session expired? Please, try to login again.";
230                         break;
231                     default:
232                         responseText = "See logs for more details.";
233                 }
234 
235                 if (reload) {
236                     UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
237 
238                     urlBuilder.removeParameter("mgnlIntercept");
239                     urlBuilder.removeParameter("mgnlPath");
240 
241                     Window.Location.replace(urlBuilder.buildString());
242                 } else {
243                     Window.alert("An error occurred on the server: response status code is " + status + "\n" + responseText);
244                 }
245             }
246 
247             @Override
248             public void onError(Request request, Throwable exception) {
249                 Window.alert(exception.getMessage());
250             }
251         });
252         try {
253             req.send();
254         } catch (RequestException e) {
255             Window.alert("An error occurred while trying to send a request to the server: " + e.getMessage());
256         }
257 
258     }
259 
260     public void createChannelPreview(final String channelType, final String deviceType, final Orientation orientation) {
261         GWT.log("Creating preview for channel type [" + channelType + "] ");
262         final UrlBuilder urlBuilder = Window.Location.createUrlBuilder();
263         urlBuilder.setParameter("mgnlChannel", channelType);
264         urlBuilder.setParameter(SKIP_PAGE_EDITOR_DOM_PROCESSING, "true");
265         final PreviewChannelWidget previewChannelWidget = new PreviewChannelWidget(urlBuilder.buildString(), orientation, deviceType);
266         //this causes the pop up to show
267         previewChannelWidget.center();
268     }
269 
270     private void processDocument(Node node, MgnlElement mgnlElement) {
271         if(this.process) {
272             for (int i = 0; i < node.getChildCount(); i++) {
273                 Node childNode = node.getChild(i);
274                 if (childNode.getNodeType() == Comment.COMMENT_NODE) {
275 
276                     try {
277                         mgnlElement = processCmsComment(childNode, mgnlElement);
278 
279                     }
280                     catch (IllegalArgumentException e) {
281                         GWT.log("Not CMSComment element, skipping: " + e.toString());
282 
283                     }
284                     catch (Exception e) {
285                         GWT.log("Caught undefined exception: " + e.toString());
286                     }
287                 }
288                 else if (childNode.getNodeType() == Node.ELEMENT_NODE && mgnlElement != null) {
289                     processElement(childNode, mgnlElement);
290                 }
291 
292                 processDocument(childNode, mgnlElement);
293             }
294         }
295     }
296 
297     private MgnlElement processCmsComment(Node node, MgnlElement mgnlElement) throws Exception {
298 
299         CMSComment comment = new CMSComment((Comment)node.cast());
300 
301         GWT.log("processing comment " + comment);
302 
303         if (!comment.isClosing()) {
304 
305             if (comment.getTagName().equals("cms:page")) {
306                 GWT.log("element was detected as page edit bar. Injecting it...");
307                 PageBarWidget pageBarWidget = new PageBarWidget(this, comment);
308                 pageBarWidget.attach();
309                 pageEditBarAlreadyProcessed = true;
310 
311                 if (pageBarWidget.isPreviewState()) {
312                     //we just need the preview bar here
313                     GWT.log("We're in preview mode, stop processing DOM.");
314                     this.process  = false;
315                 }
316             }
317 
318             else {
319                 try {
320                     mgnlElement = new MgnlElement(comment, mgnlElement);
321 
322                     if (mgnlElement.getParent() == null) {
323                         model.addRoot(mgnlElement);
324                     }
325                     else {
326                         mgnlElement.getParent().getChildren().add(mgnlElement);
327                     }
328 
329                 }
330                 catch (IllegalArgumentException e) {
331                     GWT.log("Not MgnlElement, skipping: " + e.toString());
332                 }
333             }
334         }
335 
336         else if (mgnlElement != null) {
337             mgnlElement = mgnlElement.getParent();
338         }
339 
340         return mgnlElement;
341 
342     }
343 
344     private void processElement(Node node, MgnlElement mgnlElement) {
345         Element element = node.cast();
346         if (element.hasTagName("A")) {
347             disableLink(element);
348         }
349         model.addElement(mgnlElement, element);
350 
351             if (mgnlElement.getFirstElement() == null) {
352                 mgnlElement.setFirstElement(element);
353             }
354 
355             if (mgnlElement.getLastElement() == null || !mgnlElement.getLastElement().isOrHasChild(element)) {
356                 mgnlElement.setLastElement(element);
357             }
358 
359             if (mgnlElement.isComponent()) {
360                 MgnlElement area = mgnlElement.getParentArea();
361 
362                 if (area != null) {
363                     if (area.getFirstElement() == null) {
364                         area.setFirstElement(element);
365                     }
366                     if (area.getLastElement() == null || !area.getLastElement().isOrHasChild(element)) {
367                         area.setLastElement(element);
368                     }
369                 }
370             }
371 
372 /*        if (element.hasAttribute("cms:add")) {
373 
374             GWT.log("element is component invitation placeholder. Injecting it...");
375 
376             MgnlElement area = mgnlElement;
377             if (!mgnlElement.isArea()) {
378                 area = mgnlElement.getParentArea();
379             }
380             ComponentPlaceHolderWidget placeHolder = new ComponentPlaceHolderWidget(this, area);
381 
382             model.addComponentPlaceHolder(mgnlElement, placeHolder);
383             placeHolder.attach(node);
384         }
385 
386         else if (element.hasAttribute("cms:placeholder")) {
387 
388             // this will probably not work, as it shoiuldn't be shown when there are components.. or should it?
389             GWT.log("element is area placeholder. Injecting it...");
390 
391             AreaPlaceHolderWidget placeHolder = new AreaPlaceHolderWidget(this, mgnlElement);
392 
393             model.addAreaPlaceHolder(mgnlElement, placeHolder);
394             placeHolder.attach(node);
395         }
396 
397         else if (element.hasAttribute("cms:edit")) {
398 
399             GWT.log("element is edit bar placeholder. Injecting it...");
400             EditBarWidget editBarWidget = new EditBarWidget(mgnlElement, this);
401 
402             editBarWidget.attach(node);
403             model.addEditBar(mgnlElement, editBarWidget);
404 
405         }*/
406 
407     }
408 
409     private void processMgnlElements() {
410         List<MgnlElement> deleteElements = new LinkedList<MgnlElement>();
411         List<MgnlElement> addElements = new LinkedList<MgnlElement>();
412 
413         for (MgnlElement root :model.getRootElements()) {
414             LinkedList<MgnlElement> els = new LinkedList<MgnlElement>();
415             els.add(root);
416             els.addAll(root.getDescendants());
417             for (MgnlElement mgnlElement : els) {
418                 if (model.getEditBar(mgnlElement) == null) {
419                     if (mgnlElement.isArea()) {
420 
421                         boolean injected = AreaInjector.inject(this, mgnlElement);
422 
423 
424                         if (!injected) {
425                             // if the area has no controls we, don't want it in the structure.
426                             MgnlElement parent = mgnlElement.getParent();
427 
428                             // if area is root remove it from the roots
429                             if (parent == null) {
430                                 deleteElements.add(mgnlElement);
431                             }
432                             // set all child parents to parent
433                             for (MgnlElement child : mgnlElement.getChildren()) {
434                                 if (parent == null) {
435                                     addElements.add(child);
436                                 }
437                                 else {
438                                     parent.getChildren().add(child);
439                                 }
440                                 child.setParent(parent);
441 
442                             }
443                             model.removeMgnlElement(mgnlElement);
444                         }
445                     }
446                     else if (mgnlElement.isComponent()) {
447                         GWT.log("element is edit bar placeholder. Injecting it...");
448                         EditBarWidget editBarWidget = new EditBarWidget(mgnlElement, this);
449 
450                         model.addEditBar(mgnlElement, editBarWidget);
451                     }
452                 }
453             }
454         }
455         model.rootElements.removeAll(deleteElements);
456         model.rootElements.addAll(addElements);
457 
458 
459     }
460 
461     //FIXME submitting forms still renders website channel and edit bars
462     private void postProcessLinksOnMobilePreview(Element root) {
463         NodeList<Element> anchors = root.getElementsByTagName("a");
464         String mobilePreviewParams = "mgnlChannel=mobile&skipPageEditorDOMProcessing=true";
465         for (int i = 0; i < anchors.getLength(); i++) {
466             AnchorElement anchor = AnchorElement.as(anchors.getItem(i));
467 
468             GWT.log("Starting to process link " + anchor.getHref());
469 
470             if(LegacyJavascript.isEmpty(anchor.getHref())) {
471                 continue;
472             }
473             String manipulatedHref = anchor.getHref().replaceFirst(Window.Location.getProtocol() + "//" + Window.Location.getHost(), "");
474             String queryString = Window.Location.getQueryString() != null ? Window.Location.getQueryString() : "";
475 
476             GWT.log("query string is " + queryString);
477 
478             String queryStringRegex =  queryString.replaceFirst("\\?", "\\\\?");
479             manipulatedHref = manipulatedHref.replaceFirst(queryStringRegex, "");
480             int indexOfHash = manipulatedHref.indexOf("#");
481 
482             if(indexOfHash != -1) {
483                 manipulatedHref = manipulatedHref.substring(indexOfHash);
484             } else {
485                 if(queryString.startsWith("?")) {
486                     queryString += "&" + mobilePreviewParams;
487                 } else {
488                     queryString = "?" + mobilePreviewParams;
489                 }
490                 manipulatedHref += queryString;
491             }
492             GWT.log("Resulting link is " + manipulatedHref);
493             anchor.setHref(manipulatedHref);
494         }
495         /*NodeList<Element> forms = root.getElementsByTagName("form");
496 
497         for (int i = 0; i < forms.getLength(); i++) {
498             FormElement form = FormElement.as(forms.getItem(i));
499             form.setAction(form.getAction().concat("?"+SKIP_PAGE_EDITOR_DOM_PROCESSING+"=true&mgnlChannel=mobile"));
500         }*/
501     }
502 
503 
504     public native void disableLink(Element element) /*-{
505         if (element.onclick == null) {
506             element.onclick = function() {
507               return false;
508             };
509         }
510       }-*/;
511 
512     private void cleanRootElements() {
513 
514         List<MgnlElement> newRoots = new LinkedList<MgnlElement>();
515         GWT.log(String.valueOf(model.rootElements.size()));
516         Iterator<MgnlElement> it = model.rootElements.iterator();
517         while (it.hasNext()) {
518             MgnlElement root = it.next();
519             if (model.getEditBar(root) == null) {
520                 for (MgnlElement child : root.getChildren()) {
521                         child.setParent(null);
522                         newRoots.add(child);
523                 }
524                 it.remove();
525             }
526         }
527         GWT.log(String.valueOf(model.rootElements.size()));
528 
529         model.rootElements.addAll(newRoots);
530         GWT.log(String.valueOf(model.rootElements.size()));
531 
532     }
533 
534     private native void onPageEditorReady() /*-{
535         var callbacks = $wnd.mgnl.PageEditor.onPageEditorReadyCallbacks
536         if(typeof callbacks != 'undefined') {
537              for(var i=0; i < callbacks.length; i++) {
538                 callbacks[i].apply()
539              }
540          }
541     }-*/;
542 
543 }