001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://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: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://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        m_currentHeight = -1;
077        m_rpc = getRpcProxy(I_CmsMaxHeightServerRpc.class);
078    }
079
080    /**
081     * @see com.vaadin.client.ui.AbstractConnector#getState()
082     */
083    @Override
084    public CmsMaxHeightState getState() {
085
086        return (CmsMaxHeightState)super.getState();
087    }
088
089    /**
090     * @see com.vaadin.client.ui.AbstractConnector#onUnregister()
091     */
092    @Override
093    public void onUnregister() {
094
095        super.onUnregister();
096        m_unregistered = true;
097        removeObserver();
098    }
099
100    /**
101     * @see com.vaadin.client.extensions.AbstractExtensionConnector#extend(com.vaadin.client.ServerConnector)
102     */
103    @Override
104    protected void extend(ServerConnector target) {
105
106        // Get the extended widget
107        m_widget = ((ComponentConnector)target).getWidget();
108        addMutationObserver(m_widget.getElement());
109        addStateChangeHandler(new StateChangeHandler() {
110
111            /** Serial version id. */
112            private static final long serialVersionUID = 1L;
113
114            @SuppressWarnings("synthetic-access")
115            public void onStateChanged(StateChangeEvent stateChangeEvent) {
116
117                m_currentHeight = -1;
118                handleMutation();
119            }
120        });
121    }
122
123    /**
124     * Handles the widget mutation.<p>
125     */
126    protected void handleMutation() {
127
128        if (m_unregistered) {
129            removeObserver();
130            return;
131        }
132
133        int maxHeight = getState().getMaxHeight();
134        if (m_currentHeight > 0) {
135            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)
136            JavaScriptObject scrollPositionData = saveScrollPositions(m_widget.getElement());
137            String classToAdd = "o-measuring-height";
138            m_widget.addStyleName(classToAdd);
139            m_widget.getElement().getStyle().clearHeight();
140            int computedHeight = m_widget.getOffsetHeight();
141            m_widget.removeStyleName(classToAdd);
142            if ((computedHeight + 10) < m_currentHeight) {
143                m_currentHeight = -1;
144                m_rpc.fixHeight(m_currentHeight);
145            } else {
146                m_widget.getElement().getStyle().setHeight(m_currentHeight, Unit.PX);
147            }
148            restoreScrollPositions(scrollPositionData);
149            addMutationObserver(m_widget.getElement());
150        } else if ((maxHeight > 0) && (m_widget.getOffsetHeight() > maxHeight)) {
151            m_currentHeight = maxHeight;
152            m_rpc.fixHeight(m_currentHeight);
153        }
154    }
155
156    /**
157     * Adds a native mutation observer to the widget element.<p>
158     *
159     * @param element the element
160     */
161    private native void addMutationObserver(Element element)/*-{
162        var self = this;
163        var observer = new MutationObserver(
164                function(mutations) {
165                    self.@org.opencms.ui.client.CmsMaxHeightConnector::handleMutation()();
166                });
167        this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver = observer;
168
169        // configuration of the observer:
170        var config = {
171            attributes : true,
172            childList : true,
173            characterData : true,
174            subtree : true
175        };
176
177        // pass in the target node, as well as the observer options
178        observer.observe(element, config);
179    }-*/;
180
181    /**
182     * Removes the mutation observer.<p>
183     */
184    private native void removeObserver()/*-{
185        if (this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver != null) {
186            this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver
187                    .disconnect();
188            this.@org.opencms.ui.client.CmsMaxHeightConnector::m_mutationObserver = null;
189        }
190    }-*/;
191
192    /**
193     * Restores saved scroll positions of scrollable containers.<p>
194     *
195     * @param capturedScrollPositions the saved scroll positions
196     */
197    private native void restoreScrollPositions(JavaScriptObject capturedScrollPositions) /*-{
198        for (var i = 0; i < capturedScrollPositions.length; i++) {
199            var entry = capturedScrollPositions[i];
200            try {
201                var element = entry[0];
202                var st = entry[1];
203                element.scrollTop = st;
204            } catch (e) {
205                // ignore
206            }
207        }
208    }-*/;
209
210    /**
211     * Creates a list of pairs of v-scrollable elements and their respective scrollTop attribute beneath the given DOM element.<p>
212     *
213     * @param element the root element
214     * @return a list of pairs [[element1, scrolltop1], [element2, scrolltop2], ...]
215     */
216    private native JavaScriptObject saveScrollPositions(Element element) /*-{
217        var result = [];
218        try {
219            var elems = element.querySelectorAll(".v-scrollable");
220            for (var i = 0; i < elems.length; i++) {
221                var elem = elems[i];
222                if (elem.scrollHeight > elem.clientHeight) {
223                    result.push([ elem, elem.scrollTop ]);
224                }
225            }
226        } catch (e) {
227        }
228        return result;
229    }-*/;
230
231}