001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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.acacia.client.ui;
029
030import org.opencms.ade.containerpage.client.ui.css.I_CmsLayoutBundle;
031import org.opencms.gwt.client.util.CmsClientStringUtil;
032import org.opencms.gwt.client.util.CmsDomUtil;
033import org.opencms.gwt.client.util.CmsPositionBean;
034
035import java.util.ArrayList;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Map;
039
040import com.google.gwt.core.client.GWT;
041import com.google.gwt.dom.client.Element;
042import com.google.gwt.dom.client.Style;
043import com.google.gwt.dom.client.Style.Display;
044import com.google.gwt.dom.client.Style.Unit;
045import com.google.gwt.event.dom.client.ClickEvent;
046import com.google.gwt.event.dom.client.ClickHandler;
047import com.google.gwt.event.dom.client.HasClickHandlers;
048import com.google.gwt.event.shared.HandlerRegistration;
049import com.google.gwt.uibinder.client.UiBinder;
050import com.google.gwt.uibinder.client.UiField;
051import com.google.gwt.user.client.Window;
052import com.google.gwt.user.client.ui.Composite;
053import com.google.gwt.user.client.ui.FlowPanel;
054import com.google.gwt.user.client.ui.HTMLPanel;
055import com.google.gwt.user.client.ui.RootPanel;
056import com.google.gwt.user.client.ui.Widget;
057
058import jsinterop.base.Js;
059
060/**
061 * In-line edit overlay covering rest of the page.<p>
062 */
063public class CmsInlineEditOverlay extends Composite implements HasClickHandlers {
064
065    /** The ui binder. */
066    interface I_CmsInlineEditOverlayUiBinder extends UiBinder<HTMLPanel, CmsInlineEditOverlay> {
067        // nothing to do
068    }
069
070    /** The width rquired by the button bar. */
071    private static final int BUTTON_BAR_WIDTH = 28;
072
073    /** List of present overlays. */
074    private static List<CmsInlineEditOverlay> m_overlays = new ArrayList<CmsInlineEditOverlay>();
075
076    /** The ui binder instance. */
077    private static I_CmsInlineEditOverlayUiBinder uiBinder = GWT.create(I_CmsInlineEditOverlayUiBinder.class);
078
079    /** Bottom border. */
080    @UiField
081    protected Element m_borderBottom;
082
083    /** Left border. */
084    @UiField
085    protected Element m_borderLeft;
086
087    /** Right border. */
088    @UiField
089    protected Element m_borderRight;
090
091    /** Top border. */
092    @UiField
093    protected Element m_borderTop;
094
095    /** The button bar element. */
096    @UiField
097    protected Element m_buttonBar;
098
099    /** Edit overlay. */
100    @UiField
101    protected Element m_overlayBottom;
102
103    /** Edit overlay. */
104    @UiField
105    protected Element m_overlayLeft;
106
107    /** Edit overlay. */
108    @UiField
109    protected Element m_overlayRight;
110
111    /** Edit overlay. */
112    @UiField
113    protected Element m_overlayTop;
114
115    /** The edit button panel. */
116    @UiField
117    FlowPanel m_buttonPanel;
118
119    /** Style of border. */
120    private Style m_borderBottomStyle;
121
122    /** Style of border. */
123    private Style m_borderLeftStyle;
124
125    /** Style of border. */
126    private Style m_borderRightStyle;
127
128    /** Style of border. */
129    private Style m_borderTopStyle;
130
131    /** Map of attached edit buttons and their absolute top positions. */
132    private Map<CmsInlineEntityWidget, Integer> m_buttons;
133
134    /** The current overlay position. */
135    private CmsPositionBean m_currentPosition;
136
137    /** The element to surround with the overlay. */
138    private Element m_element;
139
140    /** Flag indicating this overlay has a button bar. */
141    private boolean m_hasButtonBar;
142
143    /** The main panel. */
144    private HTMLPanel m_main;
145
146    /** The overlay offset. */
147    private int m_offset = 3;
148
149    /** Style of overlay. */
150    private Style m_overlayBottomStyle;
151
152    /** Style of overlay. */
153    private Style m_overlayLeftStyle;
154
155    /** Style of overlay. */
156    private Style m_overlayRightStyle;
157
158    /** Style of overlay. */
159    private Style m_overlayTopStyle;
160
161    /** Elements marked as non inline editable, for which an overlay is displayed. */
162    private List<elemental2.dom.Element> m_disabledElements = new ArrayList<>();
163
164    /**
165     * Constructor.<p>
166     *
167     * @param element the element to surround with the overlay
168     */
169    public CmsInlineEditOverlay(Element element) {
170
171        m_main = uiBinder.createAndBindUi(this);
172        initWidget(m_main);
173        m_element = element;
174        m_overlayLeftStyle = m_overlayLeft.getStyle();
175        m_overlayBottomStyle = m_overlayBottom.getStyle();
176        m_overlayRightStyle = m_overlayRight.getStyle();
177        m_overlayTopStyle = m_overlayTop.getStyle();
178        m_borderBottomStyle = m_borderBottom.getStyle();
179        m_borderLeftStyle = m_borderLeft.getStyle();
180        m_borderRightStyle = m_borderRight.getStyle();
181        m_borderTopStyle = m_borderTop.getStyle();
182        m_buttonBar.getStyle().setDisplay(Display.NONE);
183        m_buttonPanel.addDomHandler(new ClickHandler() {
184
185            public void onClick(ClickEvent event) {
186
187                // prevent the click event to propagated from the button panel to the main widget
188                event.stopPropagation();
189            }
190        }, ClickEvent.getType());
191        m_buttons = new HashMap<CmsInlineEntityWidget, Integer>();
192    }
193
194    /**
195     * Adds an overlay surrounding the given DOM element.<p>
196     *
197     * @param element the element
198     *
199     * @return the overlay widget
200     */
201    public static CmsInlineEditOverlay addOverlayForElement(Element element) {
202
203        CmsInlineEditOverlay overlay = new CmsInlineEditOverlay(element);
204        if (!m_overlays.isEmpty()) {
205            m_overlays.get(m_overlays.size() - 1).setVisible(false);
206        }
207        m_overlays.add(overlay);
208        RootPanel.get().add(overlay);
209        overlay.updatePosition();
210        overlay.checkZIndex();
211        return overlay;
212    }
213
214    /**
215     * Returns the root overlay if available.<p>
216     *
217     * @return the root overlay
218     */
219    public static CmsInlineEditOverlay getRootOverlay() {
220
221        return m_overlays.isEmpty() ? null : m_overlays.get(0);
222    }
223
224    /**
225     * Removes all present overlays.<p>
226     */
227    public static void removeAll() {
228
229        for (CmsInlineEditOverlay overlay : m_overlays) {
230            overlay.removeFromParent();
231        }
232        m_overlays.clear();
233    }
234
235    /**
236     * Removes the last overlay to display the previous or none.<p>
237     */
238    public static void removeLastOverlay() {
239
240        if (!m_overlays.isEmpty()) {
241            CmsInlineEditOverlay last = m_overlays.remove(m_overlays.size() - 1);
242            last.removeFromParent();
243        }
244        if (!m_overlays.isEmpty()) {
245            m_overlays.get(m_overlays.size() - 1).setVisible(true);
246        }
247    }
248
249    /**
250     * Updates the current overlay's position.<p>
251     */
252    public static void updateCurrentOverlayPosition() {
253
254        if (!m_overlays.isEmpty()) {
255            m_overlays.get(m_overlays.size() - 1).updatePosition();
256        }
257    }
258
259    /**
260     * Adds a button widget to the button panel.<p>
261     *
262     * @param widget the button widget
263     * @param absoluteTop the absolute top position
264     */
265    public void addButton(CmsInlineEntityWidget widget, int absoluteTop) {
266
267        setButtonBarVisible(true);
268        m_buttonPanel.add(widget);
269        setButtonPosition(widget, absoluteTop);
270    }
271
272    /**
273     * @see com.google.gwt.event.dom.client.HasClickHandlers#addClickHandler(com.google.gwt.event.dom.client.ClickHandler)
274     */
275    public HandlerRegistration addClickHandler(ClickHandler handler) {
276
277        return addDomHandler(handler, ClickEvent.getType());
278    }
279
280    /**
281     * Increases the overlay z-index if necessary.<p>
282     */
283    public void checkZIndex() {
284
285        int zIndex = 100000;
286        Element parent = m_element.getParentElement();
287        while (parent != null) {
288            int parentIndex = CmsDomUtil.getCurrentStyleInt(parent, CmsDomUtil.Style.zIndex);
289            if (parentIndex > zIndex) {
290                zIndex = parentIndex;
291            }
292            parent = parent.getParentElement();
293        }
294        if (zIndex > 100000) {
295            getElement().getStyle().setZIndex(zIndex);
296        }
297    }
298
299    /**
300     * Clears and hides the button panel.<p>
301     */
302    public void clearButtonPanel() {
303
304        m_buttonPanel.clear();
305        m_buttons.clear();
306        setButtonBarVisible(false);
307    }
308
309    /**
310     * Initializes the overlay for 'disabled' (not inline editable) elements.
311     */
312    public void initDisabled() {
313
314        clearDisabled();
315        elemental2.dom.Element elem = Js.cast(m_element);
316        List<elemental2.dom.Element> inactive = elem.querySelectorAll(
317            ".oc-container, .oc-not-inline-editable").asList();
318        for (elemental2.dom.Element candidate : inactive) {
319            boolean isRoot = true;
320            for (elemental2.dom.Element other : inactive) {
321                if ((other != candidate) && other.contains(candidate)) {
322                    isRoot = false;
323                    break;
324                }
325            }
326            if (isRoot) {
327                m_disabledElements.add(candidate);
328                candidate.classList.add(I_CmsLayoutBundle.INSTANCE.containerpageCss().inlineEditDisabled());
329            }
330        }
331    }
332
333    /**
334     * Updates the position of the given button widget.<p>
335     *
336     * @param widget the button widget
337     * @param absoluteTop the top absolute top position
338     */
339    public void setButtonPosition(CmsInlineEntityWidget widget, int absoluteTop) {
340
341        if (m_buttonPanel.getWidgetIndex(widget) > -1) {
342            int buttonBarTop = CmsClientStringUtil.parseInt(m_buttonBar.getStyle().getTop());
343            if (absoluteTop < buttonBarTop) {
344                absoluteTop = buttonBarTop;
345            }
346            int positionTop = getAvailablePosition(widget, absoluteTop) - buttonBarTop;
347            widget.getElement().getStyle().setTop(positionTop, Unit.PX);
348            if (CmsClientStringUtil.parseInt(m_buttonBar.getStyle().getHeight()) < (positionTop + 20)) {
349                increaseOverlayHeight(positionTop + 20);
350            }
351        }
352    }
353
354    /**
355     * Sets the overlay offset.<p>
356     *
357     * @param offset the offset
358     */
359    public void setOffset(int offset) {
360
361        m_offset = offset;
362    }
363
364    /**
365     * @see com.google.gwt.user.client.ui.UIObject#setVisible(boolean)
366     */
367    @Override
368    public void setVisible(boolean visible) {
369
370        super.setVisible(visible);
371        if (!visible && m_hasButtonBar) {
372            for (Widget widget : m_buttonPanel) {
373                if (widget instanceof CmsInlineEntityWidget) {
374                    ((CmsInlineEntityWidget)widget).setContentHighlightingVisible(false);
375                }
376            }
377        }
378    }
379
380    /**
381     * Updates the overlay position.<p>
382     */
383    public void updatePosition() {
384
385        setPosition(CmsPositionBean.getBoundingClientRect(m_element));
386        for (Widget widget : m_buttonPanel) {
387            if (widget instanceof CmsInlineEntityWidget) {
388                ((CmsInlineEntityWidget)widget).positionWidget();
389            }
390        }
391    }
392
393    /**
394     * @see com.google.gwt.user.client.ui.Composite#onDetach()
395     */
396    @Override
397    protected void onDetach() {
398
399        super.onDetach();
400        clearDisabled();
401
402    }
403
404    /**
405     * Clears the overlay for non inline editable elements.
406     */
407    private void clearDisabled() {
408
409        m_disabledElements.forEach(
410            elem2 -> elem2.classList.remove(I_CmsLayoutBundle.INSTANCE.containerpageCss().inlineEditDisabled()));
411        m_disabledElements.clear();
412    }
413
414    /**
415     * Returns the available absolute top position for the given button.<p>
416     *
417     * @param widget the button widget
418     * @param absoluteTop the proposed position
419     *
420     * @return the available position
421     */
422    private int getAvailablePosition(CmsInlineEntityWidget widget, int absoluteTop) {
423
424        m_buttons.remove(widget);
425        boolean positionBlocked = true;
426        while (positionBlocked) {
427            positionBlocked = false;
428            for (int pos : m_buttons.values()) {
429                if (((pos - 24) < absoluteTop) && (absoluteTop < (pos + 24))) {
430                    positionBlocked = true;
431                    absoluteTop = pos + 25;
432                    break;
433                }
434            }
435        }
436        m_buttons.put(widget, Integer.valueOf(absoluteTop));
437        return absoluteTop;
438    }
439
440    /**
441     * Increases the overlay height to make space for edit buttons.<p>
442     *
443     * @param height the height to set
444     */
445    private void increaseOverlayHeight(int height) {
446
447        if (m_currentPosition != null) {
448            m_currentPosition.setHeight(height);
449            setPosition(m_currentPosition);
450        }
451    }
452
453    /**
454     * Sets button bar visibility.<p>
455     *
456     * @param visible <code>true</code> to set the button bar visible
457     */
458    private void setButtonBarVisible(boolean visible) {
459
460        if (m_hasButtonBar != visible) {
461            m_hasButtonBar = visible;
462            if (m_hasButtonBar) {
463
464                m_buttonBar.getStyle().clearDisplay();
465                int width = CmsClientStringUtil.parseInt(m_borderTopStyle.getWidth()) + BUTTON_BAR_WIDTH;
466                m_borderTopStyle.setWidth(width, Unit.PX);
467                m_borderBottomStyle.setWidth(width, Unit.PX);
468                m_borderRightStyle.setLeft(
469                    CmsClientStringUtil.parseInt(m_borderRightStyle.getLeft()) + BUTTON_BAR_WIDTH,
470                    Unit.PX);
471            } else {
472                m_buttonBar.getStyle().setDisplay(Display.NONE);
473                int width = CmsClientStringUtil.parseInt(m_borderTopStyle.getWidth()) - BUTTON_BAR_WIDTH;
474                m_borderTopStyle.setWidth(width, Unit.PX);
475                m_borderBottomStyle.setWidth(width, Unit.PX);
476                m_borderRightStyle.setLeft(
477                    CmsClientStringUtil.parseInt(m_borderRightStyle.getLeft()) - BUTTON_BAR_WIDTH,
478                    Unit.PX);
479            }
480        }
481    }
482
483    /**
484     * Sets position and size of the overlay area.<p>
485     *
486     * @param position the position of highlighted area
487     */
488    private void setPosition(CmsPositionBean position) {
489
490        m_currentPosition = position;
491        setSelectPosition(position.getLeft(), position.getTop(), position.getHeight(), position.getWidth());
492    }
493
494    /**
495     * Sets position and size of the overlay area.<p>
496     *
497     * @param posX the new X position
498     * @param posY the new Y position
499     * @param height the new height
500     * @param width the new width
501     */
502    private void setSelectPosition(int posX, int posY, int height, int width) {
503
504        int useWidth = Window.getClientWidth();
505        int bodyWidth = RootPanel.getBodyElement().getClientWidth() + RootPanel.getBodyElement().getOffsetLeft();
506        if (bodyWidth > useWidth) {
507            useWidth = bodyWidth;
508        }
509        int useHeight = Window.getClientHeight();
510        int bodyHeight = RootPanel.getBodyElement().getClientHeight() + RootPanel.getBodyElement().getOffsetTop();
511        if (bodyHeight > useHeight) {
512            useHeight = bodyHeight;
513        }
514
515        m_overlayLeftStyle.setWidth(posX - m_offset, Unit.PX);
516        m_overlayLeftStyle.setHeight(useHeight, Unit.PX);
517
518        m_borderLeftStyle.setHeight(height + (4 * m_offset), Unit.PX);
519        m_borderLeftStyle.setTop(posY - (2 * m_offset), Unit.PX);
520        m_borderLeftStyle.setLeft(posX - (2 * m_offset), Unit.PX);
521
522        m_overlayTopStyle.setLeft(posX - m_offset, Unit.PX);
523        m_overlayTopStyle.setWidth(width + (2 * m_offset), Unit.PX);
524        m_overlayTopStyle.setHeight(posY - m_offset, Unit.PX);
525
526        m_borderTopStyle.setLeft(posX - m_offset, Unit.PX);
527        m_borderTopStyle.setTop(posY - (2 * m_offset), Unit.PX);
528        if (m_hasButtonBar) {
529            m_borderTopStyle.setWidth(width + (2 * m_offset) + BUTTON_BAR_WIDTH, Unit.PX);
530        } else {
531            m_borderTopStyle.setWidth(width + (2 * m_offset), Unit.PX);
532        }
533
534        m_overlayBottomStyle.setLeft(posX - m_offset, Unit.PX);
535        m_overlayBottomStyle.setWidth(width + m_offset + m_offset, Unit.PX);
536        m_overlayBottomStyle.setHeight(useHeight - posY - height - m_offset, Unit.PX);
537        m_overlayBottomStyle.setTop(posY + height + m_offset, Unit.PX);
538
539        m_borderBottomStyle.setLeft(posX - m_offset, Unit.PX);
540        m_borderBottomStyle.setTop((posY + height) + m_offset, Unit.PX);
541        if (m_hasButtonBar) {
542            m_borderBottomStyle.setWidth(width + (2 * m_offset) + BUTTON_BAR_WIDTH, Unit.PX);
543        } else {
544            m_borderBottomStyle.setWidth(width + (2 * m_offset), Unit.PX);
545        }
546
547        m_overlayRightStyle.setLeft(posX + width + m_offset, Unit.PX);
548        m_overlayRightStyle.setWidth(useWidth - posX - width - m_offset, Unit.PX);
549        m_overlayRightStyle.setHeight(useHeight, Unit.PX);
550
551        m_borderRightStyle.setHeight(height + (4 * m_offset), Unit.PX);
552        m_borderRightStyle.setTop(posY - (2 * m_offset), Unit.PX);
553        if (m_hasButtonBar) {
554            m_borderRightStyle.setLeft(posX + width + m_offset + BUTTON_BAR_WIDTH, Unit.PX);
555        } else {
556            m_borderRightStyle.setLeft(posX + width + m_offset, Unit.PX);
557        }
558
559        m_buttonBar.getStyle().setTop(posY - m_offset, Unit.PX);
560        m_buttonBar.getStyle().setHeight(height + (2 * m_offset), Unit.PX);
561        m_buttonBar.getStyle().setLeft(posX + width + m_offset + 1, Unit.PX);
562    }
563}