View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 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.ui.vaadin.gwt.client.editor.model.focus;
35  
36  import info.magnolia.ui.vaadin.gwt.client.editor.dom.MgnlArea;
37  import info.magnolia.ui.vaadin.gwt.client.editor.dom.MgnlComponent;
38  import info.magnolia.ui.vaadin.gwt.client.editor.dom.MgnlElement;
39  import info.magnolia.ui.vaadin.gwt.client.editor.event.ComponentStopMoveEvent;
40  import info.magnolia.ui.vaadin.gwt.client.editor.event.SelectElementEvent;
41  import info.magnolia.ui.vaadin.gwt.client.editor.jsni.scroll.ElementScrollPositionPreserver;
42  import info.magnolia.ui.vaadin.gwt.client.editor.model.Model;
43  import info.magnolia.ui.vaadin.gwt.client.shared.AbstractElement;
44  
45  import com.google.gwt.dom.client.Element;
46  import com.google.web.bindery.event.shared.EventBus;
47  
48  /**
49   * Takes care of toggling the visibility of the control-bars in the page-editor based on selections of the elements.
50   * This is a fairly optimized and complex implementation considering lots of special corner-cases and side-effects. Don't
51   * apply changes here lightly. Seriously.
52   * This class should remain state-less. Ideally it should only contain the logic for switching the visibility of
53   * control-bars inside the iframe.
54   */
55  public class FocusModelImpl implements FocusModel {
56  
57      private final Model model;
58      private final EventBus eventBus;
59  
60      // These elements are only tracked for the toggling of visibility.
61      // They should not survive a reload of the page and should not be exposed.
62      private MgnlArea selectedAreaElement;
63      private MgnlComponent selectedComponentElement;
64  
65      public FocusModelImpl(EventBus eventBus, Model model) {
66          this.eventBus = eventBus;
67          this.model = model;
68      }
69  
70      @Override
71      public void selectElement(Element element) {
72          selectElement(model.getMgnlElement(element));
73      }
74  
75      @Override
76      public void selectElement(AbstractElement element) {
77          selectElement(model.getMgnlElement(element));
78      }
79  
80      /**
81       * Selects the given {@link MgnlElement element} and keeps track of the scroll position before toggling the control-bars
82       * for later re-adjustments.
83       */
84      private void selectElement(MgnlElement mgnlElement) {
85  
86          if (mgnlElement == null) {
87              mgnlElement = model.getRootPage();
88          }
89  
90          // cancel move if click was outside the moving components relatives
91          if (model.isMoving()) {
92              if (!mgnlElement.isRelated(selectedComponentElement)) {
93                  eventBus.fireEvent(new ComponentStopMoveEvent(null, false));
94              }
95              return; // just cancel the move, no selection
96          }
97  
98          // reset the scroll position unless it's a page.
99          ElementScrollPositionPreserver scrollPositionPreserver = null;
100         if (!mgnlElement.isPage()) {
101             scrollPositionPreserver = mgnlElement.preserve();
102         }
103 
104         doSelectMgnlElement(mgnlElement);
105 
106         // restoring of the position has to happen after the selection, as that's when the page-height has been changed.
107         if (scrollPositionPreserver != null) {
108             scrollPositionPreserver.restorePosition();
109         }
110 
111     }
112 
113     private void doSelectMgnlElement(MgnlElement mgnlElement) {
114         MgnlArea area = null;
115         MgnlComponent component = null;
116 
117         if (mgnlElement.isComponent()) {
118             component = (MgnlComponent) mgnlElement;
119             area = mgnlElement.getParentArea();
120         } else if (mgnlElement.isArea()) {
121             area = (MgnlArea) mgnlElement;
122         }
123         // first set the component, then set the area. the selected component is used for setting
124         // the current area class.
125         setComponentSelection(component);
126         setAreaSelection(area);
127 
128         // if we're selecting the page, we want this light-greenish look on the root areas, if not, then not.
129         togglePageFocus(mgnlElement.isPage());
130 
131         dispatchElementSelection(mgnlElement);
132     }
133 
134     /**
135      * Creates the initial selection of the page-editor when in editing-mode:
136      * <pre>
137      *     <ul>
138      *         <li>Root-areas are visible</li>
139      *         <li>Component-placeholders in the root-areas are visible.</li>
140      *     </ul>
141      * </pre>
142      *
143      * Opposed to {@link #togglePageFocus(boolean)} this method only needs to be called once, when the iframe is loaded.
144      * The reason is that these settings will never be reset by any selections done afterwards.
145      */
146     @Override
147     public void init() {
148         for (MgnlArea root : model.getRootAreas()) {
149             root.setVisible(true);
150 
151             if (root.getComponents().isEmpty()) {
152                 root.setPlaceHolderVisible(true);
153             }
154         }
155     }
156 
157     /**
158      * Resets the last selected elements. This needs to happen as lot's of the toggling-logic of the displayed bars and
159      * their status is based on these. It has no visual effect. This method should ideally go, as the selected elements
160      * should automatically be disposed, when the dom changes, e.g. when the iframe reloads.
161      */
162     @Override
163     public void resetFocus() {
164         this.selectedAreaElement = null;
165         this.selectedComponentElement = null;
166     }
167 
168     /**
169      * Takes care of the selection of components. keeps track of last selected element and toggles
170      * the focus. If a null-value is passed it will reset the currently selected component.
171      *
172      * @param component the MgnlElement component, can be null.
173      */
174     private void setComponentSelection(MgnlComponent component) {
175         if (selectedComponentElement == component) {
176             return;
177         }
178         if (selectedComponentElement != null) {
179             selectedComponentElement.removeFocus();
180         }
181         if (component != null) {
182             component.setFocus();
183         }
184         this.selectedComponentElement = component;
185     }
186 
187     /**
188      * This method takes care of selecting and deselecting areas.
189      *
190      * @param area selected area, can be null.
191      */
192     private void setAreaSelection(MgnlArea area) {
193 
194         if (selectedAreaElement != null) {
195 
196             selectedAreaElement.removeFocus();
197 
198             // always reset current area selection unless area and current area are related
199             if (!selectedAreaElement.isRelated(area)) {
200 
201                 toggleChildComponentVisibility(selectedAreaElement, false);
202                 toggleAreaVisibility(selectedAreaElement, false);
203             }
204 
205             // hide child components if area is an ascendant of current area or selectedArea is not
206             // a descendant
207             else if (selectedAreaElement.getAscendants().contains(area) || (area != null && !area.getDescendants().contains(selectedAreaElement))) {
208                 toggleChildComponentVisibility(selectedAreaElement, false);
209             }
210         }
211 
212         // set focus on new selected area
213         if (area != null) {
214             toggleAreaVisibility(area, true);
215             toggleChildComponentVisibility(area, true);
216 
217             area.setFocus(selectedComponentElement != null);
218         }
219         this.selectedAreaElement = area;
220     }
221 
222     private void toggleAreaVisibility(MgnlArea area, boolean visible) {
223         MgnlArea parentArea = area.getParentArea();
224         if (parentArea != null) {
225             toggleAreaVisibility(parentArea, visible);
226             toggleChildComponentVisibility(parentArea, visible);
227 
228         }
229         // root areas are always visible
230         if (!model.getRootAreas().contains(area)) {
231             area.setVisible(visible);
232         }
233 
234         toggleNestedAreasVisibility(area, visible);
235     }
236 
237     private void toggleChildComponentVisibility(MgnlArea area, boolean visible) {
238         // do not hide empty root areas placeholder
239         if (!model.getRootAreas().contains(area) || !area.getComponents().isEmpty()) {
240             area.setPlaceHolderVisible(visible);
241         }
242 
243         // hide
244         if (!visible && !area.getComponents().isEmpty()) {
245             area.setPlaceHolderVisible(false);
246         }
247 
248         for (MgnlComponent component : area.getComponents()) {
249             // toggle all child-components editbar visibility - does this case occur?
250             component.setVisible(visible);
251 
252             // toggle all child-components-area visibility
253             for (MgnlArea childArea : component.getAreas()) {
254                 childArea.setVisible(visible);
255                 toggleNestedAreasVisibility(childArea, visible);
256             }
257         }
258     }
259 
260     private void toggleNestedAreasVisibility(MgnlArea area, boolean visible) {
261         for (MgnlArea childArea : area.getAreas()) {
262             childArea.setVisible(visible);
263             toggleNestedAreasVisibility(childArea, visible);
264         }
265     }
266 
267     /**
268      * This toggles the 'initial' focus on the pages root-area elements. That's the light-greenish coloring when the page
269      * is loaded or the page gets selected.
270      */
271     private void togglePageFocus(boolean visible) {
272         for (MgnlArea root : model.getRootAreas()) {
273             root.toggleInitFocus(visible);
274         }
275     }
276 
277     private void dispatchElementSelection(MgnlElement mgnlElement) {
278         mgnlElement = (mgnlElement != null) ? mgnlElement : model.getRootPage();
279         if (mgnlElement != null) {
280             eventBus.fireEvent(new SelectElementEvent(mgnlElement.getTypedElement()));
281         }
282     }
283 }