001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.client;
029
030import org.opencms.ui.components.extensions.CmsMaxHeightExtension;
031import org.opencms.ui.shared.components.CmsMaxHeightState;
032import org.opencms.ui.shared.rpc.I_CmsMaxHeightServerRpc;
033
034import com.google.gwt.core.client.JavaScriptObject;
035import com.google.gwt.dom.client.Element;
036import com.google.gwt.dom.client.Style.Unit;
037import com.google.gwt.user.client.ui.Widget;
038import com.vaadin.client.ComponentConnector;
039import com.vaadin.client.ServerConnector;
040import com.vaadin.client.communication.StateChangeEvent;
041import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
042import com.vaadin.client.extensions.AbstractExtensionConnector;
043import com.vaadin.shared.ui.Connect;
044
045/**
046 * This connector will manipulate the CSS classes of the extended widget depending on the scroll position.<p>
047 */
048@Connect(CmsMaxHeightExtension.class)
049public class CmsMaxHeightConnector extends AbstractExtensionConnector {
050
051    /** The serial version id. */
052    private static final long serialVersionUID = -3661096843568550285L;
053
054    /** The currently set height. */
055    private int m_currentHeight;
056
057    /** Flag indicating the required height is currently being evaluated. */
058    private boolean m_evaluating;
059
060    /** The native mutation observer. */
061    private JavaScriptObject m_mutationObserver;
062
063    /** The RPC proxy. */
064    private I_CmsMaxHeightServerRpc m_rpc;
065
066    /** Flag which is set when this connector is unregistered. */
067    private boolean m_unregistered;
068
069    /** The widget to enhance. */
070    private Widget m_widget;
071
072    /**
073     * Constructor.<p>
074     */
075    public CmsMaxHeightConnector() {
076
077        m_currentHeight = -1;
078        m_rpc = getRpcProxy(I_CmsMaxHeightServerRpc.class);
079    }
080
081    /**
082     * @see com.vaadin.client.ui.AbstractConnector#getState()
083     */
084    @Override
085    public CmsMaxHeightState getState() {
086
087        return (CmsMaxHeightState)super.getState();
088    }
089
090    /**
091     * @see com.vaadin.client.ui.AbstractConnector#onUnregister()
092     */
093    @Override
094    public void onUnregister() {
095
096        super.onUnregister();
097        m_unregistered = true;
098        removeObserver();
099    }
100
101    /**
102     * @see com.vaadin.client.extensions.AbstractExtensionConnector#extend(com.vaadin.client.ServerConnector)
103     */
104    @Override
105    protected void extend(ServerConnector target) {
106
107        // Get the extended widget
108        m_widget = ((ComponentConnector)target).getWidget();
109        addMutationObserver(m_widget.getElement());
110        addStateChangeHandler(new StateChangeHandler() {
111
112            /** Serial version id. */
113            private static final long serialVersionUID = 1L;
114
115            @SuppressWarnings("synthetic-access")
116            public void onStateChanged(StateChangeEvent stateChangeEvent) {
117
118                m_currentHeight = -1;
119                handleMutation();
120            }
121        });
122    }
123
124    /**
125     * Handles the widget mutation.<p>
126     */
127    protected void handleMutation() {
128
129        if (m_unregistered) {
130            removeObserver();
131            return;
132        }
133
134        int maxHeight = getState().getMaxHeight();
135        if (m_currentHeight > 0) {
136            removeObserver(); // prevent 'recursive' call of handleMutation (it's not actually recursive since it's async, but would still lead to an infinite number of calls)
137            JavaScriptObject scrollPositionData = saveScrollPositions(m_widget.getElement());
138            String classToAdd = "o-measuring-height";
139            m_widget.addStyleName(classToAdd);
140            m_widget.getElement().getStyle().clearHeight();
141            int computedHeight = m_widget.getOffsetHeight();
142            m_widget.removeStyleName(classToAdd);
143            if ((computedHeight + 10) < m_currentHeight) {
144                m_currentHeight = -1;
145                m_rpc.fixHeight(m_currentHeight);
146            } else {
147                m_widget.getElement().getStyle().setHeight(m_currentHeight, Unit.PX);
148            }
149            restoreScrollPositions(scrollPositionData);
150            addMutationObserver(m_widget.getElement());
151        } else if ((maxHeight > 0) && (m_widget.getOffsetHeight() > maxHeight)) {
152            m_currentHeight = maxHeight;
153            m_rpc.fixHeight(m_currentHeight);
154        }
155    }
156
157    /**
158     * Adds a native mutation observer to the widget element.<p>
159     *
160     * @param element the element
161     */
162    private native void addMutationObserver(Element element)/*-{
163        var self = this;
164        var observer = new MutationObserver(
165                function(mutations) {
166                    self.@org.opencms.ui.client.CmsMaxHeightConnector::handleMutation()();
167                });
168        this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver = observer;
169
170        // configuration of the observer:
171        var config = {
172            attributes : true,
173            childList : true,
174            characterData : true,
175            subtree : true
176        };
177
178        // pass in the target node, as well as the observer options
179        observer.observe(element, config);
180    }-*/;
181
182    /**
183     * Removes the mutation observer.<p>
184     */
185    private native void removeObserver()/*-{
186        if (this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver != null) {
187            this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver
188                    .disconnect();
189            this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver = null;
190        }
191    }-*/;
192
193    /**
194     * Restores saved scroll positions of scrollable containers.<p>
195     *
196     * @param capturedScrollPositions the saved scroll positions
197     */
198    private native void restoreScrollPositions(JavaScriptObject capturedScrollPositions) /*-{
199        for (var i = 0; i < capturedScrollPositions.length; i++) {
200            var entry = capturedScrollPositions[i];
201            try {
202                var element = entry[0];
203                var st = entry[1];
204                element.scrollTop = st;
205            } catch (e) {
206                // ignore
207            }
208        }
209    }-*/;
210
211    /**
212     * Creates a list of pairs of v-scrollable elements and their respective scrollTop attribute beneath the given DOM element.<p>
213     *
214     * @param element the root element
215     * @return a list of pairs [[element1, scrolltop1], [element2, scrolltop2], ...]
216     */
217    private native JavaScriptObject saveScrollPositions(Element element) /*-{
218        var result = [];
219        try {
220            var elems = element.querySelectorAll(".v-scrollable");
221            for (var i = 0; i < elems.length; i++) {
222                var elem = elems[i];
223                if (elem.scrollHeight > elem.clientHeight) {
224                    result.push([ elem, elem.scrollTop ]);
225                }
226            }
227        } catch (e) {
228        }
229        return result;
230    }-*/;
231
232}