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.ade.containerpage.client;
029
030import org.opencms.ade.containerpage.client.ui.CmsContainerPageContainer;
031import org.opencms.ade.containerpage.client.ui.CmsContainerPageElementPanel;
032import org.opencms.ade.containerpage.client.ui.CmsDroppedElementModeSelectionDialog;
033import org.opencms.ade.containerpage.client.ui.CmsElementOptionBar;
034import org.opencms.ade.containerpage.client.ui.CmsGroupContainerElementPanel;
035import org.opencms.ade.containerpage.client.ui.CmsToolbarAllGalleriesMenu;
036import org.opencms.ade.containerpage.client.ui.I_CmsDropContainer;
037import org.opencms.ade.containerpage.client.ui.css.I_CmsLayoutBundle;
038import org.opencms.ade.containerpage.shared.CmsCntPageData.ElementReuseMode;
039import org.opencms.ade.containerpage.shared.CmsContainer;
040import org.opencms.ade.containerpage.shared.CmsContainerElement;
041import org.opencms.ade.containerpage.shared.CmsContainerElementData;
042import org.opencms.ade.contenteditor.client.Messages;
043import org.opencms.ade.contenteditor.shared.CmsEditorConstants;
044import org.opencms.ade.galleries.client.ui.CmsResultListItem;
045import org.opencms.gwt.client.CmsCoreProvider;
046import org.opencms.gwt.client.dnd.CmsDNDHandler;
047import org.opencms.gwt.client.dnd.CmsDNDHandler.Orientation;
048import org.opencms.gwt.client.dnd.I_CmsDNDController;
049import org.opencms.gwt.client.dnd.I_CmsDraggable;
050import org.opencms.gwt.client.dnd.I_CmsDropTarget;
051import org.opencms.gwt.client.ui.CmsErrorDialog;
052import org.opencms.gwt.client.ui.CmsHighlightingBorder;
053import org.opencms.gwt.client.ui.CmsList;
054import org.opencms.gwt.client.ui.CmsListItem;
055import org.opencms.gwt.client.ui.CmsListItemWidget;
056import org.opencms.gwt.client.ui.CmsPushButton;
057import org.opencms.gwt.client.ui.CmsToolbar;
058import org.opencms.gwt.client.ui.I_CmsButton;
059import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
060import org.opencms.gwt.client.ui.I_CmsButton.Size;
061import org.opencms.gwt.client.util.CmsDebugLog;
062import org.opencms.gwt.client.util.CmsDomUtil;
063import org.opencms.gwt.client.util.CmsPositionBean;
064import org.opencms.gwt.client.util.I_CmsSimpleCallback;
065import org.opencms.gwt.shared.CmsGwtLog;
066import org.opencms.gwt.shared.CmsListInfoBean;
067import org.opencms.gwt.shared.CmsTemplateContextInfo;
068import org.opencms.util.CmsStringUtil;
069import org.opencms.util.CmsUUID;
070
071import java.util.ArrayList;
072import java.util.Comparator;
073import java.util.HashMap;
074import java.util.HashSet;
075import java.util.List;
076import java.util.Map;
077import java.util.Map.Entry;
078import java.util.Set;
079
080import com.google.common.base.Objects;
081import com.google.common.collect.ComparisonChain;
082import com.google.gwt.core.client.Scheduler;
083import com.google.gwt.core.client.Scheduler.ScheduledCommand;
084import com.google.gwt.dom.client.Document;
085import com.google.gwt.dom.client.Element;
086import com.google.gwt.dom.client.Style;
087import com.google.gwt.dom.client.Style.Display;
088import com.google.gwt.dom.client.Style.Position;
089import com.google.gwt.dom.client.Style.Unit;
090import com.google.gwt.dom.client.Style.Visibility;
091import com.google.gwt.event.dom.client.ClickEvent;
092import com.google.gwt.event.dom.client.ClickHandler;
093import com.google.gwt.event.dom.client.HasClickHandlers;
094import com.google.gwt.event.dom.client.HasMouseOutHandlers;
095import com.google.gwt.event.dom.client.HasMouseOverHandlers;
096import com.google.gwt.event.dom.client.KeyCodes;
097import com.google.gwt.event.dom.client.MouseOutEvent;
098import com.google.gwt.event.dom.client.MouseOutHandler;
099import com.google.gwt.event.dom.client.MouseOverEvent;
100import com.google.gwt.event.dom.client.MouseOverHandler;
101import com.google.gwt.event.shared.HandlerRegistration;
102import com.google.gwt.user.client.DOM;
103import com.google.gwt.user.client.Event;
104import com.google.gwt.user.client.Window;
105import com.google.gwt.user.client.rpc.AsyncCallback;
106import com.google.gwt.user.client.ui.FlowPanel;
107import com.google.gwt.user.client.ui.RootPanel;
108import com.google.gwt.user.client.ui.Widget;
109
110import elemental2.dom.DOMRect;
111import elemental2.dom.DomGlobal;
112import elemental2.dom.HTMLDivElement;
113import elemental2.dom.Node;
114import elemental2.dom.NodeList;
115import jsinterop.base.Any;
116import jsinterop.base.Js;
117import jsinterop.base.JsPropertyMap;
118
119/**
120 * The container-page editor drag and drop controller.<p>
121 *
122 * @since 8.0.0
123 */
124public class CmsContainerpageDNDController implements I_CmsDNDController {
125
126    /**
127     * The Class CmsPlacementModeContext.
128     */
129    class CmsPlacementModeContext {
130
131        /**
132         * Buttons used to place elements in placement mode.
133         */
134        class PlacementButton extends FlowPanel implements HasClickHandlers, HasMouseOverHandlers, HasMouseOutHandlers {
135
136            /** The associated container. */
137            private CmsContainerPageContainer m_container;
138
139            /** The height. */
140            private int m_height;
141
142            /** The internal index (used for sorting). */
143            private int m_index;
144
145            /** The left. */
146            private int m_left;
147
148            /** Thetop. */
149            private int m_top;
150
151            /** The width. */
152            private int m_width;
153
154            /**
155             * Creates a new instance.
156             *
157             * @param size the size
158             */
159            public PlacementButton(int size) {
160
161                addStyleName(OC_PLACEMENT_BUTTON);
162                String alpha = "abcdefghijklmnopqrstuvwxyz";
163                String id = "pb_";
164                for (int i = 0; i < 5; i++) {
165                    int index = (int)Math.floor(Math.random() * alpha.length());
166                    id = id + alpha.charAt(index);
167                }
168                getElement().setId(id);
169                m_width = size;
170                m_height = size;
171                m_index = m_buttonCounter++;
172
173            }
174
175            /**
176             * @see com.google.gwt.event.dom.client.HasClickHandlers#addClickHandler(com.google.gwt.event.dom.client.ClickHandler)
177             */
178            @Override
179            public HandlerRegistration addClickHandler(ClickHandler handler) {
180
181                ClickHandler handler2 = event -> {
182                    event.stopPropagation();
183                    handler.onClick(event);
184                };
185                return addDomHandler(handler2, ClickEvent.getType());
186            }
187
188            /**
189             * @see com.google.gwt.event.dom.client.HasMouseOutHandlers#addMouseOutHandler(com.google.gwt.event.dom.client.MouseOutHandler)
190             */
191            @Override
192            public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
193
194                return addDomHandler(handler, MouseOutEvent.getType());
195            }
196
197            /**
198             * @see com.google.gwt.event.dom.client.HasMouseOverHandlers#addMouseOverHandler(com.google.gwt.event.dom.client.MouseOverHandler)
199             */
200            @Override
201            public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
202
203                return addDomHandler(handler, MouseOverEvent.getType());
204
205            }
206
207            /**
208             * Gets the associated container.
209             *
210             * @return the associated container
211             */
212            public CmsContainerPageContainer getContainer() {
213
214                return m_container;
215            }
216
217            /**
218             * Gets the height.
219             *
220             * @return the height
221             */
222            public int getHeight() {
223
224                return m_height;
225            }
226
227            /**
228             * Gets the index.
229             *
230             * @return the index
231             */
232            public int getIndex() {
233
234                return m_index;
235            }
236
237            /**
238             * Gets the left.
239             *
240             * @return the left
241             */
242            public int getLeft() {
243
244                return m_left;
245            }
246
247            /**
248             * Gets the top.
249             *
250             * @return the top
251             */
252            public int getTop() {
253
254                return m_top;
255            }
256
257            /**
258             * Gets the width.
259             *
260             * @return the width
261             */
262            public int getWidth() {
263
264                return m_width;
265            }
266
267            /**
268             * Checks if other placement button's position intersects this one's.
269             *
270             * @param other the other placement button
271             * @return true if the buttons intersect
272             */
273            public boolean intersects(PlacementButton other) {
274
275                int t = m_collisionTolerance;
276                return segmentIntersect(m_left + t, m_width - (2 * t), other.m_left + t, other.m_width - (2 * t))
277                    && segmentIntersect(m_top + t, m_height - (2 * t), other.m_top + t, other.m_height - (2 * t));
278
279            }
280
281            /**
282             * Checks if this button is fully before another (taking into account the tolerance zones of both).
283             *
284             * @param second the other button
285             * @return true if this button (except tolerance zones) is fully before the other button (except for its tolerance zones)
286             *
287             */
288            public boolean isFullyBefore(PlacementButton second) {
289
290                return ((getLeft() + getWidth()) - m_collisionTolerance) <= (second.getLeft() + m_collisionTolerance);
291            }
292
293            /**
294             * Moves this button to the right of another button.
295             *
296             * @param second the other button
297             */
298            public void moveToRightOf(PlacementButton second) {
299
300                m_left = (second.getLeft() + second.getWidth()) - (2 * m_collisionTolerance);
301            }
302
303            /**
304             * Sets the container.
305             *
306             * @param container the new container
307             */
308            public void setContainer(CmsContainerPageContainer container) {
309
310                m_container = container;
311            }
312
313            /**
314             * Sets the left.
315             *
316             * @param left the new left
317             */
318            public void setLeft(int left) {
319
320                m_left = left;
321            }
322
323            /**
324             * Actually sets the position on the DOM element.
325             */
326            public void setPosition() {
327
328                getElement().getStyle().setLeft(m_left, Unit.PX);
329                getElement().getStyle().setTop(m_top, Unit.PX);
330            }
331
332            /**
333             * Sets the top.
334             *
335             * @param top the new top
336             */
337            public void setTop(int top) {
338
339                m_top = top;
340            }
341
342            /**
343             * @see com.google.gwt.user.client.ui.UIObject#toString()
344             */
345            public String toString() {
346
347                return getElement().getId() + ":(" + getLeft() + ", " + getTop() + ")";
348            }
349        }
350
351        /** Single-element array holding the currently visible highlighting element. */
352        private CmsHighlightingBorder m_activeBorder;
353
354        /** The list of active buttons. */
355        private List<PlacementButton> m_buttons = new ArrayList<>();
356
357        /** The placemenet button size (width or height). */
358        private int m_buttonSize;
359
360        /** The callback to call when an element is placed. */
361        private I_PlacementCallback m_callback;
362
363        /** The tolerance for button collisions. */
364        private int m_collisionTolerance;
365
366        /** Numeric rank for containers, used for resolving button collisions. */
367        private Map<String, Integer> m_containerIndexes = new HashMap<>();
368
369        /** The set of available container names. */
370        private Set<String> m_containers;
371
372        /** The DND handler. */
373        private CmsDNDHandler m_dndHandler;
374
375        /** The current draggable. */
376        private I_CmsDraggable m_draggable;
377
378        /** The special layer used to display placement buttons and block click events for the rest of the page. */
379        private PlacementLayer m_layer;
380
381        /** The container page handler. */
382        private CmsContainerpageHandler m_pageHandler;
383
384        /** The placement mode toolbar. */
385        private CmsToolbar m_toolbar;
386
387        /** A widget to display in the toolbar. */
388        private Widget m_toolbarWidget;
389
390        /**
391         * Creates a new placement mode context.
392         *
393         * @param containers the set of ids of available containers
394         * @param toolbarWidget the additional widget to display in the toolbar
395         * @param dndHandler the drag/drop handler
396         * @param draggable the current draggable
397         * @param callback the callback to call when placing an element
398         */
399        public CmsPlacementModeContext(
400            Set<String> containers,
401            Widget toolbarWidget,
402            CmsDNDHandler dndHandler,
403            I_CmsDraggable draggable,
404            I_PlacementCallback callback) {
405
406            m_containers = containers;
407            m_callback = callback;
408            m_pageHandler = m_controller.getHandler();
409            m_dndHandler = dndHandler;
410            m_toolbarWidget = toolbarWidget;
411            m_draggable = draggable;
412            m_collisionTolerance = getTolerance();
413
414        }
415
416        /**
417         * Cleans up everything related to the placement mode.
418         */
419        public void destroy() {
420
421            if (m_layer != null) {
422                m_layer.removeFromParent();
423            }
424            NodeList<elemental2.dom.Element> placeholders = DomGlobal.document.querySelectorAll(
425                "." + OC_PLACEMENT_PLACEHOLDER);
426            for (int i = 0; i < placeholders.length; i++) {
427                elemental2.dom.Element placeholder = placeholders.getAt(i);
428                placeholder.remove();
429            }
430            NodeList<elemental2.dom.Element> selectedContainerElements = DomGlobal.document.querySelectorAll(
431                "." + OC_PLACEMENT_SELECTED_ELEMENT);
432            for (int i = 0; i < selectedContainerElements.length; i++) {
433                selectedContainerElements.getAt(i).classList.remove(OC_PLACEMENT_SELECTED_ELEMENT);
434            }
435            RootPanel.get().removeStyleName(OC_PLACEMENT_MODE);
436            m_pageHandler.setEditButtonsVisible(true);
437            m_controller.getHandler().enableToolbarButtons();
438            m_toolbar.removeFromParent();
439            Map<String, CmsContainerPageContainer> containerMap = m_controller.getContainerTargets();
440            for (CmsContainerPageContainer container : containerMap.values()) {
441                container.checkEmptyContainers();
442            }
443            m_controller.setPreviewHandler(null);
444        }
445
446        /**
447         * Initializes the placement mode.
448         */
449        public void init() {
450
451            m_controller.hideEditableListButtons();
452            m_controller.getHandler().disableToolbarButtons();
453            m_controller.getHandler().hideMenu();
454            initToolbar();
455            m_pageHandler.setEditButtonsVisible(false);
456            RootPanel.get().addStyleName(OC_PLACEMENT_MODE);
457            if (CmsCoreProvider.TOUCH_ONLY.matches()) {
458                m_buttonSize = PLACEMENT_BUTTON_BIG;
459            } else {
460                m_buttonSize = PLACEMENT_BUTTON_SMALL;
461            }
462            initPlacementLayer();
463            m_controller.setPreviewHandler(event -> {
464                Event nativeEvent = Event.as(event.getNativeEvent());
465                if (event.getTypeInt() == Event.ONKEYDOWN) {
466                    int keyCode = nativeEvent.getKeyCode();
467                    if ((keyCode == KeyCodes.KEY_CTRL)
468                        || (keyCode == KeyCodes.KEY_SHIFT)
469                        || (keyCode == KeyCodes.KEY_ALT)) {
470                        // In a VM, when the user presses Ctrl+E, the keydown event for the Ctrl event may or may not wait to be fired until the E key is pressed, depending on the settings.
471                        // To get consistent behavior, we ignore keydown events with keycodes that are just modifier keys.
472                        return;
473                    } else {
474                        stopDrag(m_dndHandler);
475                    }
476                } else if ((event.getTypeInt() == Event.ONMOUSEOVER)
477                    || (event.getTypeInt() == Event.ONMOUSEDOWN)
478                    || (event.getTypeInt() == Event.ONCLICK)
479                    || (event.getTypeInt() == Event.ONMOUSEUP)) {
480                    // ignore likely mouse events on floating headers etc.
481                    try {
482                        boolean isChildOfLayer = (m_layer != null)
483                            && m_layer.getElement().isOrHasChild(Element.as(event.getNativeEvent().getEventTarget()));
484                        boolean isChildOfToolbar = (m_toolbar != null)
485                            && m_toolbar.getElement().isOrHasChild(Element.as(event.getNativeEvent().getEventTarget()));
486                        if (!isChildOfLayer && !isChildOfToolbar) {
487                            event.cancel();
488                            event.getNativeEvent().preventDefault();
489                            event.getNativeEvent().stopPropagation();
490                        }
491                    } catch (Exception e) {
492
493                    }
494                }
495            });
496
497        }
498
499        /**
500         * Adds a new placement button.
501         *
502         * @param container the container
503         * @return the placement button
504         */
505        private PlacementButton addButton(CmsContainerPageContainer container) {
506
507            PlacementButton button = new PlacementButton(m_buttonSize);
508            m_buttons.add(button);
509            m_layer.add(button);
510            button.setContainer(container);
511            addHighlightingMouseHandlers(container, button);
512            return button;
513
514        }
515
516        /**
517         * Adds mouse handlers for showing/hiding container borders when hovering over placement buttons.
518         *
519         * @param container the container to which the button belongs
520         * @param button the button
521         */
522        private void addHighlightingMouseHandlers(CmsContainerPageContainer container, PlacementButton button) {
523
524            button.addMouseOverHandler(event -> {
525                if (m_activeBorder != null) {
526                    m_activeBorder.getElement().getStyle().setVisibility(Visibility.HIDDEN);
527                }
528                m_activeBorder = container.getHighlighting();
529                m_activeBorder.getElement().getStyle().setVisibility(Visibility.VISIBLE);
530            });
531            button.addMouseOutHandler(event -> {
532                if (m_activeBorder != null) {
533                    m_activeBorder.getElement().getStyle().setVisibility(Visibility.HIDDEN);
534                    m_activeBorder = null;
535                }
536                container.getHighlighting().getElement().getStyle().setVisibility(Visibility.HIDDEN);
537            });
538        }
539
540        /**
541         * Creates a push button for the edit tool-bar.<p>
542         *
543         * @param title the button title
544         * @param imageClass the image class
545         *
546         * @return the button
547         */
548        private CmsPushButton createButton(String title, String imageClass) {
549
550            CmsPushButton result = new CmsPushButton();
551            result.setTitle(title);
552            result.setImageClass(imageClass);
553            result.setButtonStyle(ButtonStyle.FONT_ICON, null);
554            result.setSize(Size.big);
555            return result;
556        }
557
558        /**
559         * Gets the drag-and-drop handler.
560         *
561         * @return the drag and drop handler
562         */
563        private CmsDNDHandler getDNDHandler() {
564
565            return m_dndHandler;
566        }
567
568        /**
569         * Gets the placement button size (Width or height).
570         *
571         * @return the placement button size
572         */
573        private int getPlacementButtonSize() {
574
575            return m_buttonSize;
576        }
577
578        /**
579         * Gets the width of the 'tolerance zone' in pixels around the sides of the placement button which does not count for collision detection.
580         *
581         * @return the width of the tolerance zone
582         */
583        private int getTolerance() {
584
585            if (CmsCoreProvider.TOUCH_ONLY.matches()) {
586                return 0;
587            }
588            JsPropertyMap<?> window = Js.cast(DomGlobal.window);
589            Any tolerance = window.getAsAny("ocPlacementButtonTolerance");
590            if (tolerance != null) {
591                return tolerance.asInt();
592            } else {
593                return 1;
594            }
595        }
596
597        /**
598         * Does all placement mode initializations related to the button layer.
599         */
600        private void initPlacementLayer() {
601
602            if (m_layer != null) {
603                m_layer.removeFromParent();
604            }
605            m_layer = new PlacementLayer();
606            m_layer.addStyleName(OC_PLACEMENT_LAYER);
607            RootPanel.get().add(m_layer);
608            m_layer.addClickHandler(event -> {
609                stopDrag(m_dndHandler);
610            });
611            Map<String, CmsContainerPageContainer> containerMap = m_controller.getContainerTargets();
612            List<CmsContainerPageContainer> usedContainers = new ArrayList<>();
613            for (CmsContainerPageContainer container : containerMap.values()) {
614                if (m_containers.contains(container.getContainerId())) {
615                    if (container.getElement().getOffsetParent() == null) {
616                        // offsetParent == null means element or its ancestor has display: none
617                        continue;
618                    }
619                    if (container.isDetailView()) {
620                        continue;
621                    }
622                    usedContainers.add(container);
623                }
624
625            }
626            // Sort containers in DOM pre-order. That means parents come before their children.
627            // The indexes of that list are used later for adjusting positions of colliding buttons.
628            usedContainers.sort(new Comparator<CmsContainerPageContainer>() {
629
630                @Override
631                public int compare(CmsContainerPageContainer o1, CmsContainerPageContainer o2) {
632
633                    if (o1 == o2) {
634                        return 0;
635                    }
636
637                    elemental2.dom.Element e1 = Js.cast(o1.getElement());
638                    elemental2.dom.Element e2 = Js.cast(o2.getElement());
639                    int cmpResult = e1.compareDocumentPosition(e2);
640                    if ((cmpResult & Node.DOCUMENT_POSITION_PRECEDING) != 0) {
641                        return 1;
642                    } else {
643                        return -1;
644                    }
645                }
646            });
647            int containerIndex = 0;
648            for (CmsContainerPageContainer container : usedContainers) {
649                m_containerIndexes.put(container.getContainerId(), Integer.valueOf(containerIndex));
650                containerIndex += 1;
651            }
652
653            for (CmsContainerPageContainer container : usedContainers) {
654
655                List<Integer> offsets = null;
656                if (container.getHighlighting() != null) {
657                    offsets = container.getHighlighting().getClientVerticalOffsets();
658                }
659                List<CmsContainerPageElementPanel> elements = container.getAllDragElements();
660                if (elements.size() == 0) {
661                    installPlacementElement(container);
662                } else {
663                    if ((offsets == null) || (offsets.size() != (elements.size() + 1))) {
664                        for (int i = 0; i < elements.size(); i++) {
665                            if (!isMovedElement(elements.get(i))) {
666                                installButtons(container, i, elements.get(i));
667                            }
668                        }
669                    } else {
670                        installPlacementButtonsWithMidpoints(container, offsets);
671                    }
672                }
673
674            }
675            positionButtons(m_buttons);
676        }
677
678        /**
679         * Generates the button bar displayed beneath the editable fields.<p>
680         */
681        private void initToolbar() {
682
683            m_toolbar = new CmsToolbar();
684            m_toolbar.setAppTitle(
685                org.opencms.ade.containerpage.client.Messages.get().key(
686                    org.opencms.ade.containerpage.client.Messages.GUI_TOOLBAR_PLACE_ELEMENT_0));
687            m_toolbar.getToolbarCenter().clear();
688            m_toolbar.getToolbarCenter().add(m_toolbarWidget);
689
690            CmsPushButton cancelButton = createButton(
691                Messages.get().key(Messages.GUI_TOOLBAR_RESET_0),
692                I_CmsButton.ButtonData.RESET_BUTTON.getIconClass());
693            cancelButton.addClickHandler(new ClickHandler() {
694
695                public void onClick(ClickEvent event) {
696
697                    stopDrag(m_dndHandler);
698                }
699            });
700            m_toolbar.addRight(cancelButton);
701            m_toolbar.addStyleName(
702                org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.toolbarCss().toolbarPlacementMode());
703            RootPanel.get().add(m_toolbar);
704        }
705
706        /**
707         * Installs the placement buttons for a container element in the button layer.
708         *
709         * @param container the container
710         * @param position the position
711         * @param element the container element
712         */
713        private void installButtons(
714            CmsContainerPageContainer container,
715            int position,
716            CmsContainerPageElementPanel element) {
717
718            int bw = getPlacementButtonSize();
719
720            container.getHighlighting().getElement().getStyle().setVisibility(Visibility.HIDDEN);
721            elemental2.dom.Element elem = Js.cast(element.getElement());
722            elemental2.dom.Element layerElem = Js.cast(m_layer.getElement());
723            DOMRect layerRect = layerElem.getBoundingClientRect();
724            DOMRect elemRect = elem.getBoundingClientRect();
725            if ((elemRect.width == 0) || (elemRect.height == 0)) {
726                return;
727            }
728            PlacementButton before = addButton(container);
729            PlacementButton after = addButton(container);
730            before.addClickHandler(e -> m_callback.place(container, element, 0));
731            after.addClickHandler(e -> m_callback.place(container, element, 1));
732            before.addStyleName(OC_PLACEMENT_BUTTON);
733            after.addStyleName(OC_PLACEMENT_BUTTON);
734            boolean leftRight = !CmsDomUtil.hasClass(OC_PLACEMENT_BUTTONS_VERTICAL, container.getElement());
735            if (leftRight) {
736                before.addStyleName(OC_PLACEMENT_LEFT);
737                after.addStyleName(OC_PLACEMENT_RIGHT);
738                double top = ((elemRect.top - layerRect.top) + (elemRect.height / 2.0)) - (bw / 2.0);
739                double left = elemRect.left - layerRect.left;
740                before.setLeft((int)left);
741                before.setTop((int)top);
742                after.setLeft((int)Math.round((left + elemRect.width) - m_buttonSize));
743                after.setTop((int)top);
744            } else {
745                before.addStyleName(OC_PLACEMENT_UP);
746                after.addStyleName(OC_PLACEMENT_DOWN);
747                int top = (int)Math.round(elemRect.top - layerRect.top);
748                int left = (int)Math.round(((elemRect.left - layerRect.left) + (0.5 * elemRect.width)) - (0.5 * bw));
749                before.setLeft(left);
750                before.setTop(top);
751
752                after.setLeft(left);
753                after.setTop((int)Math.round(((top + elemRect.height) - bw)));
754            }
755        }
756
757        /**
758         * Installs placement buttons aligned with the highlighting separators between container elements (except for the first and last button).
759         *
760         * @param container the container for which to install the buttons
761         * @param offsets the offsets relative to the viewport of the midpoints
762         */
763        private void installPlacementButtonsWithMidpoints(CmsContainerPageContainer container, List<Integer> offsets) {
764
765            int bw = getPlacementButtonSize();
766
767            List<CmsContainerPageElementPanel> elements = container.getAllDragElements();
768            elemental2.dom.Element layerElem = Js.cast(m_layer.getElement());
769            DOMRect layerRect = layerElem.getBoundingClientRect();
770            container.getHighlighting().getElement().getStyle().setVisibility(Visibility.HIDDEN);
771
772            elemental2.dom.Element containerElem = Js.cast(container.getElement());
773            DOMRect containerRect = containerElem.getBoundingClientRect();
774            double middle = (containerRect.left - layerRect.left) + (0.5 * containerRect.width);
775            for (int j = 0; j < offsets.size(); j++) {
776                if (j == 0) {
777                    CmsContainerPageElementPanel element = elements.get(0);
778                    if (!isMovedElement(element)) {
779                        PlacementButton button = addButton(container);
780                        elemental2.dom.Element realElement = Js.cast(element.getElement());
781                        button.addClickHandler(e -> m_callback.place(container, element, 0));
782                        button.addStyleName(OC_PLACEMENT_UP);
783                        DOMRect elemRect = realElement.getBoundingClientRect();
784
785                        int top = (int)Math.round(elemRect.top - layerRect.top);
786                        int left = (int)Math.round(middle - (0.5 * bw));
787                        button.setLeft(left);
788                        button.setTop(top);
789                    }
790                } else if (j == (offsets.size() - 1)) {
791                    CmsContainerPageElementPanel element = elements.get(elements.size() - 1);
792                    if (!isMovedElement(element)) {
793                        PlacementButton button = addButton(container);
794                        elemental2.dom.Element realElement = Js.cast(element.getElement());
795                        button.addClickHandler(e -> m_callback.place(container, element, 1));
796                        button.addStyleName(OC_PLACEMENT_DOWN);
797                        DOMRect elemRect = realElement.getBoundingClientRect();
798                        int top = (int)Math.round((elemRect.top + elemRect.height) - layerRect.top - bw);
799                        int left = (int)Math.round(middle - (0.5 * bw));
800                        button.setLeft(left);
801                        button.setTop(top);
802                    }
803                } else {
804                    CmsContainerPageElementPanel element = elements.get(j);
805                    CmsContainerPageElementPanel previousElement = elements.get(j - 1);
806                    if (!isMovedElement(element) && !isMovedElement(previousElement)) {
807                        PlacementButton button = addButton(container);
808                        button.addClickHandler(e -> m_callback.place(container, element, 0));
809                        button.addStyleName(OC_PLACEMENT_MIDDLE);
810                        int top = (int)Math.round(offsets.get(j) - layerRect.top - (0.5 * bw));
811                        int left = (int)Math.round(middle - (0.5 * bw));
812                        button.setLeft(left);
813                        button.setTop(top);
814                    }
815                }
816            }
817        }
818
819        /**
820         * Installs the placement button for an empty container in the button layer.
821         *
822         * @param container the container
823         */
824        private void installPlacementElement(CmsContainerPageContainer container) {
825
826            int bw = getPlacementButtonSize();
827
828            HTMLDivElement placeholder = Js.cast(DomGlobal.document.createElement("div"));
829            placeholder.classList.add(OC_PLACEMENT_PLACEHOLDER);
830            elemental2.dom.Element layerElem = Js.cast(m_layer.getElement());
831            elemental2.dom.Element containerElem = Js.cast(container.getElement());
832            container.getHighlighting().getElement().getStyle().setVisibility(Visibility.HIDDEN);
833            DOMRect containerRect = containerElem.getBoundingClientRect();
834            if (containerRect.height < 50.0) {
835                containerElem.appendChild(placeholder);
836            }
837            DOMRect layerRect = layerElem.getBoundingClientRect();
838            PlacementButton plus = addButton(container);
839
840            plus.addStyleName(OC_PLACEMENT_MIDDLE);
841
842            int top = (int)Math.round(
843                ((containerRect.top - layerRect.top) + (0.5 * containerRect.height)) - (0.5 * bw));
844            int left = (int)Math.round(
845                ((containerRect.left - layerRect.left) + (0.5 * containerRect.width)) - (0.5 * bw));
846            plus.setLeft(left);
847            plus.setTop(top);
848            plus.addClickHandler(e -> m_callback.place(container, null, 0));
849
850        }
851
852        /**
853         * Checks if a container element is the container element for which placement mode was initially started.
854         *
855         * @param element a container element
856         * @return true if the given element is the element for which placement mode was started
857         */
858        private boolean isMovedElement(CmsContainerPageElementPanel element) {
859
860            return element == m_draggable;
861        }
862
863        /**
864         * Positions buttons so that they don't collide.
865         *
866         * @param originalButtons the list of buttons to position
867         */
868        private void positionButtons(List<PlacementButton> originalButtons) {
869
870            // First split buttons into vertically separated groups. Since we only move things around horizontally, we can do it independently for each of these groups, reducing the total amount of collision tests.
871            originalButtons.sort((a, b) -> Integer.compare(a.getTop(), b.getTop()));
872            List<List<PlacementButton>> groups = new ArrayList<>();
873            groups.add(new ArrayList<>());
874            PlacementButton lastButton = null;
875            for (PlacementButton button : originalButtons) {
876                // ignoring collision tolerance here, because the point of collision tolerance is mostly controlling horizontal separation
877                if ((lastButton != null) && ((lastButton.getTop() + lastButton.getHeight()) <= button.getTop())) { // because all buttons have the same height, we only need to check the last one
878                    groups.add(new ArrayList<>());
879                }
880                groups.get(groups.size() - 1).add(button);
881                lastButton = button;
882            }
883            for (List<PlacementButton> group : groups) {
884                int iterations = 0;
885                while ((group.size() > 1) && (iterations < 1000)) {
886                    // In each iteration of the main loop, try to find and resolve one collision. Resolving one collision may cause further collisions in later iterations of the loop.
887
888                    iterations += 1;
889                    // Sort buttons by increasing x coordinate of left corner, so we can limit the potential candidates for collisions with a given button.
890                    group.sort((a, b) -> {
891                        return Integer.compare(a.getLeft(), b.getLeft());
892                    });
893                    int collisionIndex = -1;
894                    PlacementButton[] collisionPair = null;
895                    for (int i = 0; i < group.size(); i++) {
896                        collisionPair = null;
897                        PlacementButton first = group.get(i);
898                        int j = i + 1;
899                        for (j = i + 1; j < group.size(); j++) {
900                            PlacementButton second = group.get(j);
901                            if (first.isFullyBefore(second)) {
902                                break;
903                            }
904                            if (first.intersects(second)) {
905                                collisionPair = new PlacementButton[] {first, second};
906                                collisionIndex = i;
907                                break;
908                            }
909                        }
910                        if (collisionPair != null) {
911                            break;
912                        }
913                    }
914                    if (collisionIndex != -1) {
915
916                        PlacementButton first = collisionPair[0];
917                        PlacementButton second = collisionPair[1];
918                        int ci1 = m_containerIndexes.get(first.getContainer().getContainerId()).intValue();
919                        int ci2 = m_containerIndexes.get(second.getContainer().getContainerId()).intValue();
920                        /*
921                         * By using the combination of document position of the container and button index of the button, we impose a complete total ordering on the buttons
922                         * so that buttons which are lower in the ordering are moved when they collide with buttons that are higher in the ordering. This means that for a button
923                         * involved in any collisions, if it has the highest position in that ordering, all other buttons involved in collisions with it will move to its right, and
924                         * after that, will never collide with it again. The same reasoning can be applied to the element with next-highest position now to its right, and so on.
925                         * This means we can't run into 'infinite loops' where groups of elements keep pushing each other to the right ad infinitum.
926                         */
927                        if (ComparisonChain.start().compare(ci1, ci2).compare(
928                            second.getIndex(), /* Use reverse order for index - prefer moving 'insert after' buttons rather than 'insert before' for a single container if they collide */
929                            first.getIndex()).result() == -1) {
930                            first.moveToRightOf(second);
931                        } else {
932                            second.moveToRightOf(first);
933                        }
934                        // everything before first collision becomes irrelevant for the next iteration; we only move stuff to the right
935                        group = new ArrayList<>(group.subList(collisionIndex, group.size()));
936                    } else {
937                        // no collisions; we're done
938                        group = new ArrayList<>();
939                    }
940                }
941            }
942            for (PlacementButton button : originalButtons) {
943                button.setPosition();
944            }
945        }
946
947    }
948
949    /**
950     * Callback interface for the placement mode.
951     */
952    interface I_PlacementCallback {
953
954        /**
955         * Called to place an element at a specific position.
956         * @param container the target container
957         * @param referenceElement the reference element (may be null for an empty container)
958         * @param offset the offset (0 means insert before the reference element, 1 means after)
959         */
960        void place(CmsContainerPageContainer container, CmsContainerPageElementPanel referenceElement, int offset);
961    }
962
963    /**
964     * Layer for displaying placement buttons, covering everything else on the page.
965     */
966    static class PlacementLayer extends FlowPanel implements HasClickHandlers {
967
968        /**
969         * @see com.google.gwt.event.dom.client.HasClickHandlers#addClickHandler(com.google.gwt.event.dom.client.ClickHandler)
970         */
971        @Override
972        public HandlerRegistration addClickHandler(ClickHandler handler) {
973
974            return addDomHandler(handler, ClickEvent.getType());
975        }
976
977    };
978
979    /** The container highlighting offset. */
980    public static final int HIGHLIGHTING_OFFSET = 4;
981
982    /** CSS class. */
983    public static final String OC_PLACEMENT_BUTTON = "oc-placement-button";
984
985    /** CSS class. */
986    public static final String OC_PLACEMENT_BUTTONS_VERTICAL = "oc-placement-buttons-vertical";
987
988    /** CSS class. */
989    public static final String OC_PLACEMENT_DOWN = "oc-placement-down";
990
991    /** CSS class. */
992    public static final String OC_PLACEMENT_LAYER = "oc-placement-layer";
993
994    /** CSS class. */
995    public static final String OC_PLACEMENT_LEFT = "oc-placement-left";
996
997    /** CSS class. */
998    public static final String OC_PLACEMENT_MODE = "oc-placement-mode";
999
1000    /** CSS class. */
1001    public static final String OC_PLACEMENT_PLACEHOLDER = "oc-placement-placeholder";
1002
1003    /** CSS class. */
1004    public static final String OC_PLACEMENT_RIGHT = "oc-placement-right";
1005
1006    /** CSS class. */
1007    public static final String OC_PLACEMENT_SELECTED_ELEMENT = "oc-placement-selected-element";
1008
1009    /** CSS class. */
1010    public static final String OC_PLACEMENT_UP = "oc-placement-up";
1011
1012    /** The bigger size for the placement buttons. */
1013    public static final int PLACEMENT_BUTTON_BIG = 30;
1014
1015    /** The smaller size for the placement buttons. */
1016    public static final int PLACEMENT_BUTTON_SMALL = 20;
1017
1018    /** The button counter for placement mode. */
1019    private static int m_buttonCounter;
1020
1021    /** The minimum margin set to empty containers. */
1022    private static final int MINIMUM_CONTAINER_MARGIN = 10;
1023
1024    /** CSS class. */
1025    private static final String OC_PLACEMENT_MIDDLE = "oc-placement-middle";
1026
1027    /** The container page controller. */
1028    protected CmsContainerpageController m_controller;
1029
1030    /** The id of the dragged element. */
1031    protected String m_draggableId;
1032
1033    /** The id of the container from which an element was dragged. */
1034    String m_originalContainerId;
1035
1036    /** Tracks whether an element has been added to the page (rather than just moved around). */
1037    private boolean m_added;
1038
1039    /** The copy group id. */
1040    private String m_copyGroupId;
1041
1042    /** The current place holder index. */
1043    private int m_currentIndex = -1;
1044
1045    /** The current drop target. */
1046    private I_CmsDropTarget m_currentTarget;
1047
1048    /** Map of current drag info beans. */
1049    private Map<I_CmsDropTarget, Element> m_dragInfos;
1050
1051    /** The drag overlay. */
1052    private Element m_dragOverlay;
1053
1054    /** DND controller for images. We set this in onDragStart if we are dragging an image, and then delegate most of the other method calls to it if it is set.*/
1055    private CmsImageDndController m_imageDndController;
1056
1057    /** The ionitial drop target. */
1058    private I_CmsDropTarget m_initialDropTarget;
1059
1060    /** The original position of the draggable. */
1061    private int m_originalIndex;
1062
1063    /** The placement context. */
1064    private CmsPlacementModeContext m_placementContext;
1065
1066    /**
1067     * Constructor.<p>
1068     *
1069     * @param controller the container page controller
1070     */
1071    public CmsContainerpageDNDController(CmsContainerpageController controller) {
1072
1073        m_controller = controller;
1074        m_dragInfos = new HashMap<I_CmsDropTarget, Element>();
1075        Window.addResizeHandler(event -> {
1076            if (m_placementContext != null) {
1077                stopDrag(m_placementContext.getDNDHandler());
1078            }
1079        });
1080    }
1081
1082    /**
1083     * Placement button big.
1084     *
1085     * @return the string
1086     */
1087    public static final String placementButtonBig() {
1088
1089        return PLACEMENT_BUTTON_BIG + "px";
1090    }
1091
1092    /**
1093     * Placement button small.
1094     *
1095     * @return the string
1096     */
1097    public static final String placementButtonSmall() {
1098
1099        return PLACEMENT_BUTTON_SMALL + "px";
1100    }
1101
1102    /**
1103     * Checks if a 1D line segment comes before a position.
1104     *
1105     * @param start the start position of the segment
1106     * @param size the width of the segment
1107     * @param pos the position to check
1108     * @return true, if successful
1109     */
1110    public static boolean segmentBefore(int start, int size, int pos) {
1111
1112        return (start + size) <= pos;
1113    }
1114
1115    /**
1116     * Checks if two 1D line segments with given start positions and sizes intersect.
1117     *
1118     * @param start1 start position of the first segment
1119     * @param size1 size of the first segment
1120     * @param start2 start position of the second segment
1121     * @param size2 size of the second segment
1122     * @return true, if successful
1123     */
1124    public static boolean segmentIntersect(int start1, int size1, int start2, int size2) {
1125
1126        return !segmentBefore(start1, size1, start2) && !segmentBefore(start2, size2, start1);
1127    }
1128
1129    /**
1130     * Checks if the given id is a new id.<p>
1131     *
1132     * @param id the id
1133     *
1134     * @return <code>true</code> if the id is a new id
1135     */
1136    private static boolean isNewId(String id) {
1137
1138        if (id.contains("#")) {
1139            id = id.substring(0, id.indexOf("#"));
1140        }
1141        return !CmsUUID.isValidUUID(id);
1142    }
1143
1144    /**
1145     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onAnimationStart(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1146     */
1147    public void onAnimationStart(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1148
1149        // remove highlighting
1150        for (I_CmsDropTarget dropTarget : m_dragInfos.keySet()) {
1151            if (dropTarget instanceof I_CmsDropContainer) {
1152                ((I_CmsDropContainer)dropTarget).removeHighlighting();
1153            }
1154        }
1155    }
1156
1157    /**
1158     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onBeforeDrop(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1159     */
1160    public boolean onBeforeDrop(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1161
1162        return true;
1163    }
1164
1165    /**
1166     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onDragCancel(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1167     */
1168    public void onDragCancel(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1169
1170        if (m_imageDndController != null) {
1171            m_imageDndController.onDragCancel(draggable, target, handler);
1172            removeDragOverlay();
1173            m_imageDndController = null;
1174            return;
1175        }
1176        stopDrag(handler);
1177    }
1178
1179    /**
1180     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onDragStart(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1181     */
1182    public boolean onDragStart(final I_CmsDraggable draggable, I_CmsDropTarget target, final CmsDNDHandler handler) {
1183
1184        installDragOverlay();
1185        m_currentTarget = null;
1186        m_currentIndex = -1;
1187        m_draggableId = draggable.getId();
1188        m_originalIndex = -1;
1189        m_initialDropTarget = target;
1190        handler.setOrientation(Orientation.ALL);
1191        m_controller.hideEditableListButtons();
1192
1193        if (isImage(draggable)) {
1194            if (m_controller.isGroupcontainerEditing()) {
1195                // don't allow image DND while editing group containers
1196                return false;
1197            }
1198            // We are going to delegate further method calls to this, and set it to null again if the image is dragged or the
1199            // DND operation is cancelled
1200            m_imageDndController = new CmsImageDndController(CmsContainerpageController.get());
1201            return m_imageDndController.onDragStart(draggable, target, handler);
1202
1203        }
1204
1205        if (!m_controller.isGroupcontainerEditing()) {
1206            m_controller.lockContainerpage(new I_CmsSimpleCallback<Boolean>() {
1207
1208                public void execute(Boolean arg) {
1209
1210                    if (!arg.booleanValue()) {
1211                        handler.cancel();
1212                    }
1213                }
1214            });
1215        }
1216
1217        String containerId = null;
1218        if (target instanceof CmsContainerPageContainer) {
1219            containerId = ((CmsContainerPageContainer)target).getContainerId();
1220        } else {
1221            // set marker id
1222            containerId = CmsContainerElement.MENU_CONTAINER_ID;
1223        }
1224        m_originalContainerId = containerId;
1225
1226        if (target != null) {
1227            handler.addTarget(target);
1228            if (target instanceof I_CmsDropContainer) {
1229                prepareTargetContainer((I_CmsDropContainer)target, draggable, handler.getPlaceholder());
1230                Element helper = handler.getDragHelper();
1231                handler.setCursorOffsetX(helper.getOffsetWidth() - 15);
1232                handler.setCursorOffsetY(20);
1233            }
1234        }
1235
1236        m_dragInfos.put(target, handler.getPlaceholder());
1237        m_controller.getHandler().hideMenu();
1238        String clientId = draggable.getId();
1239        if (CmsStringUtil.isEmptyOrWhitespaceOnly(clientId)) {
1240            CmsDebugLog.getInstance().printLine("draggable has no id, canceling drop");
1241            return false;
1242        }
1243        final I_CmsSimpleCallback<CmsContainerElementData> callback = new I_CmsSimpleCallback<CmsContainerElementData>() {
1244
1245            /**
1246             * Execute on success.<p>
1247             *
1248             * @param arg the container element data
1249             */
1250            public void execute(CmsContainerElementData arg) {
1251
1252                prepareHelperElements(arg, handler, draggable);
1253                handler.updatePosition();
1254            }
1255        };
1256        if (isNewId(clientId)) {
1257            // for new content elements dragged from the gallery menu, the given id contains the resource type name
1258            m_controller.getNewElement(clientId, callback);
1259        } else {
1260            m_controller.getElementForDragAndDropFromContainer(clientId, m_originalContainerId, false, callback);
1261        }
1262        if (!(target instanceof CmsContainerPageContainer)) {
1263            handler.setStartPosition(-1, 0);
1264        }
1265        m_controller.sendDragStarted(isNew(draggable));
1266        return true;
1267
1268    }
1269
1270    /**
1271     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onDrop(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1272     */
1273    public void onDrop(final I_CmsDraggable draggable, final I_CmsDropTarget target, CmsDNDHandler handler) {
1274
1275        if (m_imageDndController != null) {
1276            m_imageDndController.onDrop(draggable, target, handler);
1277            removeDragOverlay();
1278            m_imageDndController = null;
1279            return;
1280        }
1281        m_added = (draggable instanceof CmsListItem) && (target instanceof CmsContainerPageContainer);
1282
1283        if (target != m_initialDropTarget) {
1284            if (target instanceof I_CmsDropContainer) {
1285                final I_CmsDropContainer container = (I_CmsDropContainer)target;
1286                final int index = container.getPlaceholderIndex();
1287                final String modelReplaceId = container instanceof CmsContainerPageContainer
1288                ? ((CmsContainerPageContainer)container).getCopyModelReplaceId()
1289                : null;
1290                if (!isNew(draggable) && (draggable instanceof CmsListItem)) {
1291                    // existing element from add menu
1292                    final String copyGroupId = m_copyGroupId;
1293                    AsyncCallback<String> modeCallback = new AsyncCallback<String>() {
1294
1295                        public void onFailure(Throwable caught) {
1296
1297                            if (caught != null) {
1298                                CmsErrorDialog.handleException(caught);
1299                            }
1300                        }
1301
1302                        public void onSuccess(String result) {
1303
1304                            if (Objects.equal(result, CmsEditorConstants.MODE_COPY)) {
1305                                final CmsContainerpageController controller = CmsContainerpageController.get();
1306                                if (copyGroupId == null) {
1307
1308                                    CmsContainerElementData data = controller.getCachedElement(m_draggableId);
1309                                    final Map<String, String> settings = data.getSettings();
1310
1311                                    controller.copyElement(
1312                                        CmsContainerpageController.getServerId(m_draggableId),
1313                                        new I_CmsSimpleCallback<CmsUUID>() {
1314
1315                                            public void execute(CmsUUID resultId) {
1316
1317                                                controller.getElementWithSettings(
1318                                                    "" + resultId,
1319                                                    settings,
1320                                                    new I_CmsSimpleCallback<CmsContainerElementData>() {
1321
1322                                                        public void execute(CmsContainerElementData newData) {
1323
1324                                                            insertDropElement(
1325                                                                newData,
1326                                                                container,
1327                                                                index,
1328                                                                draggable,
1329                                                                modelReplaceId);
1330                                                            container.removePlaceholder();
1331                                                        }
1332                                                    });
1333                                            }
1334                                        });
1335                                } else {
1336                                    controller.getElementForDragAndDropFromContainer(
1337                                        copyGroupId,
1338                                        m_originalContainerId,
1339                                        true,
1340                                        new I_CmsSimpleCallback<CmsContainerElementData>() {
1341
1342                                            public void execute(CmsContainerElementData arg) {
1343
1344                                                insertDropElement(arg, container, index, draggable, modelReplaceId);
1345                                                container.removePlaceholder();
1346                                            }
1347                                        });
1348                                }
1349
1350                            } else if (Objects.equal(result, CmsEditorConstants.MODE_REUSE)) {
1351                                insertDropElement(
1352                                    m_controller.getCachedElement(m_draggableId),
1353                                    container,
1354                                    index,
1355                                    draggable,
1356                                    modelReplaceId);
1357                                container.removePlaceholder();
1358                            }
1359                        }
1360                    };
1361
1362                    CmsUUID structureId = new CmsUUID(CmsContainerpageController.getServerId(m_draggableId));
1363                    CmsContainerElementData cachedElementData = m_controller.getCachedElement(m_draggableId);
1364                    ElementReuseMode reuseMode = isCopyModel(draggable)
1365                    ? ElementReuseMode.copy
1366                    : (((cachedElementData != null) && !cachedElementData.isCopyInModels())
1367                    ? ElementReuseMode.reuse
1368                    : CmsContainerpageController.get().getData().getElementReuseMode());
1369                    if ((!handler.isPlacementMode()) && handler.hasModifierCTRL()) {
1370                        reuseMode = ElementReuseMode.ask;
1371                    }
1372                    if (reuseMode != ElementReuseMode.reuse) {
1373
1374                        if ((cachedElementData != null)
1375                            && (!cachedElementData.hasWritePermission()
1376                                || cachedElementData.isModelGroup()
1377                                || cachedElementData.isCopyDisabled()
1378                                || cachedElementData.isWasModelGroup())) {
1379                            // User is not allowed to create this element in current view, so reuse the element instead
1380                            reuseMode = ElementReuseMode.reuse;
1381                        }
1382                    }
1383                    switch (reuseMode) {
1384                        case ask:
1385                            // when dropping elements from the into the page, we ask the user if the dropped element should
1386                            // be used, or a copy of it. If the user wants a copy, we copy the corresponding resource and replace the element
1387                            // in the page
1388                            CmsDroppedElementModeSelectionDialog.showDialog(structureId, modeCallback);
1389                            break;
1390                        case copy:
1391                            modeCallback.onSuccess(CmsEditorConstants.MODE_COPY);
1392                            break;
1393                        case reuse:
1394                        default:
1395                            modeCallback.onSuccess(CmsEditorConstants.MODE_REUSE);
1396                            break;
1397                    }
1398                } else {
1399                    // new article or moved from different container
1400                    insertDropElement(m_controller.getCachedElement(m_draggableId), container, index, draggable, null);
1401                }
1402            } else if (target instanceof CmsList<?>) {
1403                m_controller.addToFavoriteList(m_draggableId);
1404            }
1405        } else if ((target instanceof I_CmsDropContainer)
1406            && (draggable instanceof CmsContainerPageElementPanel)
1407            && isChangedPosition(target)) {
1408            CmsDomUtil.showOverlay(draggable.getElement(), false);
1409            I_CmsDropContainer container = (I_CmsDropContainer)target;
1410            int count = container.getWidgetCount();
1411            if (!handler.isPlacementMode()) {
1412                handler.getPlaceholder().getStyle().setDisplay(Display.NONE);
1413            }
1414            if (container.getPlaceholderIndex() >= count) {
1415                container.add((CmsContainerPageElementPanel)draggable);
1416            } else {
1417                container.insert((CmsContainerPageElementPanel)draggable, container.getPlaceholderIndex());
1418            }
1419            m_controller.addToRecentList(m_draggableId, null);
1420            m_controller.sendElementMoved((CmsContainerPageElementPanel)draggable);
1421            // changes are only relevant to the container page if not group-container editing
1422            if (!m_controller.isGroupcontainerEditing()) {
1423                m_controller.setPageChanged();
1424            }
1425        } else if (draggable instanceof CmsContainerPageElementPanel) {
1426            CmsDomUtil.showOverlay(draggable.getElement(), false);
1427            // to reset mouse over state remove and attach the option bar
1428            CmsContainerPageElementPanel containerElement = (CmsContainerPageElementPanel)draggable;
1429            CmsElementOptionBar optionBar = containerElement.getElementOptionBar();
1430            optionBar.removeFromParent();
1431            containerElement.setElementOptionBar(optionBar);
1432        }
1433        stopDrag(handler);
1434    }
1435
1436    /**
1437     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onPositionedPlaceholder(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1438     */
1439    public void onPositionedPlaceholder(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1440
1441        if (m_imageDndController != null) {
1442            m_imageDndController.onPositionedPlaceholder(draggable, target, handler);
1443            return;
1444        }
1445
1446        if (hasChangedPosition(target)) {
1447            checkPlaceholderVisibility(target);
1448            updateHighlighting(false, false);
1449        }
1450    }
1451
1452    /**
1453     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onTargetEnter(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1454     */
1455    public boolean onTargetEnter(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1456
1457        if (m_imageDndController != null) {
1458            return m_imageDndController.onTargetEnter(draggable, target, handler);
1459        }
1460
1461        if (target instanceof CmsContainerPageContainer) {
1462            CmsContainerPageContainer container = (CmsContainerPageContainer)target;
1463            if (container.isShowingEmptyContainerElement()) {
1464                CmsContainerPageContainer.newResizeHelper(container).initHeight();
1465            }
1466        }
1467
1468        Element placeholder = m_dragInfos.get(target);
1469        if (placeholder != null) {
1470            handler.getPlaceholder().getStyle().setDisplay(Display.NONE);
1471            handler.setPlaceholder(placeholder);
1472            placeholder.getStyle().clearDisplay();
1473            if ((target != m_initialDropTarget) && (target instanceof I_CmsDropContainer)) {
1474                ((I_CmsDropContainer)target).checkMaxElementsOnEnter();
1475            }
1476        }
1477        return true;
1478    }
1479
1480    /**
1481     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#onTargetLeave(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget, org.opencms.gwt.client.dnd.CmsDNDHandler)
1482     */
1483    public void onTargetLeave(I_CmsDraggable draggable, I_CmsDropTarget target, CmsDNDHandler handler) {
1484
1485        if (m_imageDndController != null) {
1486            m_imageDndController.onTargetLeave(draggable, target, handler);
1487            return;
1488        }
1489        CmsContainerPageContainer.clearResizeHelper();
1490        m_currentTarget = null;
1491        m_currentIndex = -1;
1492        Element placeholder = m_dragInfos.get(m_initialDropTarget);
1493        if (placeholder != null) {
1494            handler.getPlaceholder().getStyle().setDisplay(Display.NONE);
1495            handler.setPlaceholder(placeholder);
1496            placeholder.getStyle().setDisplay(Display.NONE);
1497            if ((target != m_initialDropTarget) && (target instanceof I_CmsDropContainer)) {
1498                ((I_CmsDropContainer)target).checkMaxElementsOnLeave();
1499            }
1500        }
1501        updateHighlighting(false, false);
1502    }
1503
1504    /**
1505     * @see org.opencms.gwt.client.dnd.I_CmsDNDController#postClear(org.opencms.gwt.client.dnd.I_CmsDraggable, org.opencms.gwt.client.dnd.I_CmsDropTarget)
1506     */
1507    @Override
1508    public void postClear(I_CmsDraggable draggable, I_CmsDropTarget target) {
1509
1510        Element dragElem = null;
1511        if (draggable != null) {
1512            dragElem = draggable.getElement();
1513        }
1514
1515        Element targetElem = null;
1516        if (target != null) {
1517            targetElem = target.getElement();
1518        }
1519        m_controller.sendDragFinished(dragElem, targetElem, isNew(draggable));
1520    }
1521
1522    /**
1523     * Starts placement mode for the given draggable.
1524     *
1525     * @param draggable the draggable element
1526     * @param handler the handler
1527     * @return true, if successful
1528     */
1529    public boolean startPlacementMode(I_CmsDraggable draggable, CmsDNDHandler handler) {
1530
1531        if (draggable instanceof CmsListItem) {
1532            // ensure button doesn't remain visible
1533            ((CmsListItem)draggable).getListItemWidget().forceMouseOut();
1534        } else if (draggable instanceof CmsContainerPageElementPanel) {
1535            ((CmsContainerPageElementPanel)draggable).removeHighlighting();
1536        }
1537
1538        if (!m_controller.getData().isPlacementModeEnabled()) {
1539            return false;
1540        }
1541
1542        if (isImage(draggable)) {
1543            return false;
1544        }
1545
1546        if (m_controller.isGroupcontainerEditing()) {
1547            return false;
1548        }
1549
1550        final I_CmsSimpleCallback<CmsContainerElementData> callback = new I_CmsSimpleCallback<CmsContainerElementData>() {
1551
1552            /**
1553             * Execute on success.<p>
1554             *
1555             * @param elem the container element data
1556             */
1557            public void execute(CmsContainerElementData elem) {
1558
1559                Set<String> containerIds = new HashSet<>();
1560                for (String containerId : elem.getContents().keySet()) {
1561                    CmsContainerPageContainer container = m_controller.getContainerTarget(containerId);
1562                    if (draggable instanceof CmsContainerPageElementPanel) {
1563                        if ((container != null) && draggable.getElement().isOrHasChild(container.getElement())) {
1564                            // can't move an element into one of its own nested containers
1565                            continue;
1566                        }
1567                    }
1568                    String content = elem.getContents().get(containerId);
1569                    // check if content is valid HTML
1570                    Element testElement = null;
1571                    if (content != null) {
1572                        try {
1573                            testElement = CmsDomUtil.createElement(content);
1574                        } catch (Exception e) {
1575                            CmsGwtLog.log(
1576                                "Invalid formatter output for element of type "
1577                                    + elem.getResourceType()
1578                                    + ": ["
1579                                    + content
1580                                    + "]");
1581                        }
1582                    }
1583                    if (testElement != null) {
1584                        containerIds.add(containerId);
1585                    }
1586                }
1587
1588                CmsListInfoBean info = null;
1589                if (draggable instanceof CmsListItem) {
1590                    CmsListItem item = (CmsListItem)draggable;
1591                    info = item.getListItemWidget().getInfoBean();
1592                } else {
1593                    info = elem.getListInfo();
1594                }
1595                if (info == null) {
1596                    // should never happen
1597                    info = new CmsListInfoBean("???", "", new ArrayList<>());
1598                }
1599                CmsListItemWidget newItem = new CmsListItemWidget(info);
1600                newItem.setWidth("500px");
1601                if (newItem.getOpenClose() != null) {
1602                    newItem.getOpenClose().removeFromParent();
1603                }
1604                prepareHelperElements(elem, handler, draggable);
1605                setPlacementContext(
1606                    new CmsPlacementModeContext(containerIds, newItem, handler, draggable, (cnt, reference, offset) -> {
1607                        if (reference != null) {
1608                            placeElement(handler, draggable, elem, reference, offset);
1609                        } else {
1610                            placeNewElement(handler, draggable, elem, cnt);
1611                        }
1612                    }));
1613            }
1614        };
1615        // we need to reset this so highlighting works correctly
1616        m_initialDropTarget = null;
1617        String clientId = draggable.getId();
1618        if (isNewId(clientId)) {
1619            // for new content elements dragged from the gallery menu, the given id contains the resource type name
1620            if (draggable instanceof CmsContainerPageElementPanel) {
1621                CmsContainerPageElementPanel containerElement = ((CmsContainerPageElementPanel)draggable);
1622                containerElement.addStyleName(OC_PLACEMENT_SELECTED_ELEMENT);
1623            }
1624            m_controller.getNewElement(clientId, callback);
1625        } else {
1626            String originContainer = CmsContainerElement.MENU_CONTAINER_ID;
1627            if (draggable instanceof CmsContainerPageElementPanel) {
1628                CmsContainerPageElementPanel containerElement = ((CmsContainerPageElementPanel)draggable);
1629                containerElement.addStyleName(OC_PLACEMENT_SELECTED_ELEMENT);
1630                I_CmsDropContainer dropContainer = containerElement.getParentTarget();
1631                if (dropContainer instanceof CmsContainerPageContainer) {
1632                    String realOrigin = ((CmsContainerPageContainer)dropContainer).getContainerId();
1633                    if (realOrigin != null) {
1634                        originContainer = realOrigin;
1635                    }
1636                }
1637            }
1638
1639            m_controller.getElementForDragAndDropFromContainer(clientId, originContainer, false, callback);
1640        }
1641        return true;
1642    }
1643
1644    /**
1645     * Prepares all helper elements for the different drop targets.<p>
1646     *
1647     * @param elementData the element data
1648     * @param handler the drag and drop handler
1649     * @param draggable the draggable
1650     */
1651    protected void prepareHelperElements(
1652        CmsContainerElementData elementData,
1653        CmsDNDHandler handler,
1654        I_CmsDraggable draggable) {
1655
1656        if (elementData == null) {
1657            CmsDebugLog.getInstance().printLine("elementData == null!");
1658            if (!handler.isPlacementMode()) {
1659                handler.cancel();
1660            }
1661            return;
1662        }
1663        if (!handler.isPlacementMode() && !handler.isDragging()) {
1664            return;
1665        }
1666        if (elementData.isGroup()) {
1667            m_copyGroupId = m_draggableId;
1668        } else {
1669            m_copyGroupId = null;
1670        }
1671
1672        if ((elementData.getDndId() != null) && (m_controller.getCachedElement(elementData.getDndId()) != null)) {
1673            m_draggableId = elementData.getDndId();
1674            elementData = m_controller.getCachedElement(m_draggableId);
1675        } else {
1676            m_draggableId = elementData.getClientId();
1677        }
1678        if (!elementData.isGroupContainer() && !elementData.isInheritContainer()) {
1679            for (CmsContainerPageContainer container : m_controller.getContainerTargets().values()) {
1680                String containerId = container.getContainerId();
1681                loadCss(elementData, containerId);
1682            }
1683        }
1684
1685        if (m_controller.isGroupcontainerEditing()) {
1686            CmsGroupContainerElementPanel groupContainer = m_controller.getGroupcontainer();
1687            if ((groupContainer != m_initialDropTarget)
1688                && !(elementData.isGroupContainer() || elementData.isInheritContainer())
1689                && (elementData.getContents().get(groupContainer.getContainerId()) != null)) {
1690                Element placeholder = null;
1691                String containerId = groupContainer.getContainerId();
1692                loadCss(elementData, containerId);
1693                placeholder = createPlaceholderFromHtmlForContainer(elementData, placeholder, containerId);
1694
1695                if (placeholder != null) {
1696                    prepareDragInfo(placeholder, groupContainer, handler);
1697                    groupContainer.highlightContainer(false);
1698                }
1699            }
1700            return;
1701        }
1702        if (!m_controller.isEditingDisabled()) {
1703            for (CmsContainerPageContainer container : m_controller.getContainerTargets().values()) {
1704
1705                if (draggable.getElement().isOrHasChild(container.getElement())) {
1706                    // skip containers that are children of the draggable element
1707                    continue;
1708                }
1709                if ((container != m_initialDropTarget)
1710                    && !container.isDetailView()
1711                    && (m_controller.getData().isModelGroup() || !container.hasModelGroupParent())
1712                    && (elementData.getContents().get(container.getContainerId()) != null)) {
1713
1714                    Element placeholder = null;
1715                    if (elementData.isGroupContainer() || elementData.isInheritContainer()) {
1716                        placeholder = DOM.createDiv();
1717                        String content = "";
1718                        for (String groupId : elementData.getSubItems()) {
1719                            CmsContainerElementData subData = m_controller.getCachedElement(groupId);
1720                            if (subData != null) {
1721                                if (subData.isShowInContext(
1722                                    CmsContainerpageController.get().getData().getTemplateContextInfo().getCurrentContext())) {
1723                                    if ((subData.getContents().get(container.getContainerId()) != null)) {
1724                                        content += subData.getContents().get(container.getContainerId());
1725                                    }
1726                                } else {
1727                                    content += "<div class='"
1728                                        + CmsTemplateContextInfo.DUMMY_ELEMENT_MARKER
1729                                        + "' style='display: none !important;'></div>";
1730                                }
1731                            }
1732                        }
1733                        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(content)) {
1734                            placeholder.setInnerHTML(content);
1735                            // ensure any embedded flash players are set opaque so UI elements may be placed above them
1736                            CmsDomUtil.fixFlashZindex(placeholder);
1737                        } else {
1738                            placeholder.addClassName(
1739                                I_CmsLayoutBundle.INSTANCE.containerpageCss().emptyGroupContainer());
1740                        }
1741                    } else {
1742
1743                        placeholder = createPlaceholderFromHtmlForContainer(
1744                            elementData,
1745                            placeholder,
1746                            container.getContainerId());
1747                    }
1748                    if (placeholder != null) {
1749                        prepareDragInfo(placeholder, container, handler);
1750                    }
1751                }
1752            }
1753        }
1754        initNestedContainers();
1755
1756        // add highlighting after all drag targets have been initialized
1757        updateHighlighting(true, handler.isPlacementMode());
1758
1759    }
1760
1761    /**
1762     * Inserts e new container element into the container once the drag and drop is finished.<p>
1763     *
1764     * @param elementData the element data to create the element from
1765     * @param container the target container
1766     * @param index the element index
1767     * @param draggable the original drag element
1768     * @param modelReplaceId the model replace id
1769     */
1770    void insertDropElement(
1771        CmsContainerElementData elementData,
1772        I_CmsDropContainer container,
1773        int index,
1774        I_CmsDraggable draggable,
1775        String modelReplaceId) {
1776
1777        try {
1778            CmsContainerPageElementPanel containerElement = m_controller.getContainerpageUtil().createElement(
1779                elementData,
1780                container,
1781                isNew(draggable));
1782            if (isNew(draggable)) {
1783                containerElement.setNewType(CmsContainerpageController.getServerId(m_draggableId));
1784            } else {
1785                m_controller.addToRecentList(elementData.getClientId(), null);
1786            }
1787
1788            if (index >= container.getWidgetCount()) {
1789                container.add(containerElement);
1790            } else {
1791                container.insert(containerElement, index);
1792            }
1793            if (draggable instanceof CmsContainerPageElementPanel) {
1794                ((CmsContainerPageElementPanel)draggable).removeFromParent();
1795            }
1796            m_controller.initializeSubContainers(containerElement);
1797
1798            if (modelReplaceId != null) {
1799                m_controller.executeCopyModelReplace(
1800                    modelReplaceId,
1801                    ((CmsContainerPageContainer)container).getFormerModelGroupParent(),
1802                    m_controller.getCachedElement(m_draggableId));
1803            }
1804
1805            if (!m_controller.isGroupcontainerEditing()) {
1806                if (containerElement.hasReloadMarker()) {
1807                    m_controller.setPageChanged(new Runnable() {
1808
1809                        public void run() {
1810
1811                            CmsContainerpageController.get().reloadPage();
1812                        }
1813                    });
1814                } else {
1815                    m_controller.setPageChanged();
1816                }
1817                if (m_added) {
1818                    m_controller.sendElementAdded(containerElement);
1819                } else {
1820                    m_controller.sendElementMoved(containerElement);
1821                }
1822            }
1823            if (m_controller.isGroupcontainerEditing()) {
1824                container.getElement().removeClassName(
1825                    I_CmsLayoutBundle.INSTANCE.containerpageCss().emptyGroupContainer());
1826            }
1827
1828        } catch (Exception e) {
1829            CmsErrorDialog.handleException(e.getMessage(), e);
1830        }
1831    }
1832
1833    /**
1834
1835     * Checks and sets if the place holder should be visible.<p>
1836     *
1837     * @param target the target container
1838     */
1839    private void checkPlaceholderVisibility(I_CmsDropTarget target) {
1840
1841        if (target instanceof I_CmsDropContainer) {
1842            I_CmsDropContainer container = (I_CmsDropContainer)target;
1843            container.setPlaceholderVisibility(
1844                (container != m_initialDropTarget)
1845                    || ((m_currentIndex - m_originalIndex) > 1)
1846                    || ((m_originalIndex - m_currentIndex) > 0));
1847        }
1848    }
1849
1850    /**
1851     * Creates a placeholder for an element and a specific container.
1852     *
1853     * @param elementData the element data
1854     * @param placeholder the previous placeholder
1855     * @param containerId the container id
1856     * @return the element
1857     */
1858    private Element createPlaceholderFromHtmlForContainer(
1859        CmsContainerElementData elementData,
1860        Element placeholder,
1861        String containerId) {
1862
1863        try {
1864            String htmlContent = elementData.getContents().get(containerId);
1865            placeholder = CmsDomUtil.createElement(htmlContent);
1866            // ensure any embedded flash players are set opaque so UI elements may be placed above them
1867            CmsDomUtil.fixFlashZindex(placeholder);
1868            placeholder.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragPlaceholder());
1869        } catch (Exception e) {
1870            CmsDebugLog.getInstance().printLine(e.getMessage());
1871        }
1872        return placeholder;
1873    }
1874
1875    /**
1876     * Checks if the placeholder position has changed.<p>
1877     *
1878     * @param target the current drop target
1879     *
1880     * @return <code>true</code> if the placeholder position has changed
1881     */
1882    private boolean hasChangedPosition(I_CmsDropTarget target) {
1883
1884        if ((m_currentTarget != target) || (m_currentIndex != target.getPlaceholderIndex())) {
1885            m_currentTarget = target;
1886            m_currentIndex = target.getPlaceholderIndex();
1887            return true;
1888        }
1889        return false;
1890    }
1891
1892    /**
1893     * Initializes the nested container infos.<p>
1894     */
1895    private void initNestedContainers() {
1896
1897        for (CmsContainer container : m_controller.getContainers().values()) {
1898            if (container.isSubContainer()) {
1899                CmsContainerPageContainer containerWidget = m_controller.m_targetContainers.get(container.getName());
1900                // check if the sub container is a valid drop targets
1901                if (m_dragInfos.keySet().contains(containerWidget)) {
1902                    CmsContainer parentContainer = m_controller.getContainers().get(container.getParentContainerName());
1903                    // add the container to all it's ancestors as a dnd child
1904                    while (parentContainer != null) {
1905                        if (m_dragInfos.keySet().contains(
1906                            m_controller.m_targetContainers.get(parentContainer.getName()))) {
1907                            m_controller.m_targetContainers.get(parentContainer.getName()).addDndChild(containerWidget);
1908                        }
1909                        if (parentContainer.isSubContainer()) {
1910                            parentContainer = m_controller.getContainers().get(
1911                                parentContainer.getParentContainerName());
1912                        } else {
1913                            parentContainer = null;
1914                        }
1915                    }
1916                }
1917            }
1918
1919        }
1920    }
1921
1922    /**
1923     * Installs the drag overlay to avoid any mouse over issues or similar.<p>
1924     */
1925    private void installDragOverlay() {
1926
1927        if (m_dragOverlay != null) {
1928            m_dragOverlay.removeFromParent();
1929        }
1930        m_dragOverlay = DOM.createDiv();
1931        m_dragOverlay.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragOverlay());
1932        Document.get().getBody().appendChild(m_dragOverlay);
1933    }
1934
1935    /**
1936     * Checks whether the current placeholder position represents a change to the original draggable position within the tree.<p>
1937     *
1938     * @param target the current drop target
1939     *
1940     * @return <code>true</code> if the position changed
1941     */
1942    private boolean isChangedPosition(I_CmsDropTarget target) {
1943
1944        // if the new index is not next to the old one, the position has changed
1945        if ((target != m_initialDropTarget)
1946            || !((target.getPlaceholderIndex() == (m_originalIndex + 1))
1947                || (target.getPlaceholderIndex() == m_originalIndex))) {
1948            return true;
1949        }
1950        return false;
1951    }
1952
1953    /**
1954     * Checks if the draggable item is a copy model.<p>
1955     *
1956     * @param draggable the draggable item
1957     *
1958     * @return true if the item is a copy model
1959     */
1960    private boolean isCopyModel(I_CmsDraggable draggable) {
1961
1962        if (!(draggable instanceof CmsResultListItem)) {
1963            return false;
1964        }
1965        return ((CmsResultListItem)draggable).getResult().isCopyModel();
1966    }
1967
1968    /**
1969     * Checks if the given draggable item is an image.<p>
1970     *
1971     * @param draggable the item to check
1972     *
1973     * @return true if the given item is an image
1974     */
1975    private boolean isImage(I_CmsDraggable draggable) {
1976
1977        return (draggable instanceof CmsResultListItem)
1978            && CmsToolbarAllGalleriesMenu.DND_MARKER.equals(((CmsResultListItem)draggable).getData());
1979
1980    }
1981
1982    /**
1983     * Checks if the given draggable is new.
1984     *
1985     * @param draggable the draggable
1986     * @return true if the given draggable is new
1987     */
1988    private boolean isNew(I_CmsDraggable draggable) {
1989
1990        boolean result = isNewId(draggable.getId());
1991        return result;
1992    }
1993
1994    /**
1995     * Ensures that the CSS is loaded for the given element data and container.
1996     *
1997     * @param elementData the element data
1998     * @param containerId he container id
1999     */
2000    private void loadCss(CmsContainerElementData elementData, String containerId) {
2001
2002        Set<String> cssResources = elementData.getCssResources(containerId);
2003        if ((cssResources != null) && !cssResources.isEmpty()) {
2004            // the element requires certain CSS resources, check if present and include if necessary
2005            for (String cssResourceLink : cssResources) {
2006                CmsDomUtil.ensureStyleSheetIncluded(cssResourceLink);
2007            }
2008        }
2009    }
2010
2011    /**
2012     * Placces an element relative to an existing element in placement mode.
2013     *
2014     * @param handler the handler
2015     * @param draggable the element to be placed
2016     * @param elem the data for the element
2017     * @param reference the element relative to which the placed element should be inserted
2018     * @param offset the index offset for the new element relative to the reference element (0 means 'before the reference element'!)
2019     */
2020    private void placeElement(
2021        CmsDNDHandler handler,
2022        I_CmsDraggable draggable,
2023        CmsContainerElementData elem,
2024        CmsContainerPageElementPanel reference,
2025        int offset) {
2026
2027        CmsContainerPageContainer cnt = (CmsContainerPageContainer)reference.getParentTarget();
2028        int index = cnt.getWidgetIndex(reference) + offset;
2029        if (index < 0) {
2030            index = 0;
2031        }
2032        cnt.setPlaceholderIndex(index);
2033        onDrop(draggable, cnt, handler);
2034        // Placeholder index is not reset by onDrop for container -> container transfer
2035        cnt.removePlaceholder();
2036        handler.clearPlacement();
2037    }
2038
2039    /**
2040     * Places an element into an empty container in placement mode.
2041     *
2042     * @param handler the handler
2043     * @param draggable the draggable for the element
2044     * @param elem the data for the element
2045     * @param cnt the target container
2046     */
2047    private void placeNewElement(
2048        CmsDNDHandler handler,
2049        I_CmsDraggable draggable,
2050        CmsContainerElementData elem,
2051        CmsContainerPageContainer cnt) {
2052
2053        cnt.setPlaceholderIndex(0);
2054        onDrop(draggable, cnt, handler);
2055        // Placeholder index is not reset by onDrop for container -> container transfer
2056        cnt.removePlaceholder();
2057        handler.clearPlacement();
2058    }
2059
2060    /**
2061     * Sets styles of helper elements, appends the to the drop target and puts them into a drag info bean.<p>
2062     *
2063     * @param placeholder the placeholder element
2064     * @param target the drop target
2065     * @param handler the drag and drop handler
2066     */
2067    private void prepareDragInfo(Element placeholder, I_CmsDropContainer target, CmsDNDHandler handler) {
2068
2069        target.getElement().addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging());
2070        String positioning = CmsDomUtil.getCurrentStyle(
2071            target.getElement(),
2072            org.opencms.gwt.client.util.CmsDomUtil.Style.position);
2073        // set target relative, if not absolute or fixed
2074        if (!Position.ABSOLUTE.getCssName().equals(positioning) && !Position.FIXED.getCssName().equals(positioning)) {
2075            target.getElement().getStyle().setPosition(Position.RELATIVE);
2076            // check for empty containers that don't have a minimum top and bottom margin to avoid containers overlapping
2077            if (target.getElement().getFirstChildElement() == null) {
2078                if (CmsDomUtil.getCurrentStyleInt(
2079                    target.getElement(),
2080                    CmsDomUtil.Style.marginTop) < MINIMUM_CONTAINER_MARGIN) {
2081                    target.getElement().getStyle().setMarginTop(MINIMUM_CONTAINER_MARGIN, Unit.PX);
2082                }
2083                if (CmsDomUtil.getCurrentStyleInt(
2084                    target.getElement(),
2085                    CmsDomUtil.Style.marginBottom) < MINIMUM_CONTAINER_MARGIN) {
2086                    target.getElement().getStyle().setMarginBottom(MINIMUM_CONTAINER_MARGIN, Unit.PX);
2087                }
2088            }
2089        }
2090        m_dragInfos.put(target, placeholder);
2091        if (!handler.isPlacementMode()) {
2092            handler.addTarget(target);
2093        }
2094    }
2095
2096    /**
2097     * Prepares the target container.<p>
2098     *
2099     * @param targetContainer the container
2100     * @param draggable the draggable
2101     * @param placeholder the placeholder
2102     */
2103    private void prepareTargetContainer(
2104        I_CmsDropContainer targetContainer,
2105        I_CmsDraggable draggable,
2106        Element placeholder) {
2107
2108        String positioning = CmsDomUtil.getCurrentStyle(
2109            targetContainer.getElement(),
2110            org.opencms.gwt.client.util.CmsDomUtil.Style.position);
2111        // set target relative, if not absolute or fixed
2112        if (!Position.ABSOLUTE.getCssName().equals(positioning) && !Position.FIXED.getCssName().equals(positioning)) {
2113            targetContainer.getElement().getStyle().setPosition(Position.RELATIVE);
2114        }
2115        m_originalIndex = targetContainer.getWidgetIndex((Widget)draggable);
2116        targetContainer.getElement().insertBefore(placeholder, draggable.getElement());
2117        targetContainer.getElement().addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging());
2118        CmsDomUtil.showOverlay(draggable.getElement(), true);
2119        targetContainer.highlightContainer(false);
2120    }
2121
2122    /**
2123     * Removes the drag overlay.<p>
2124     */
2125    private void removeDragOverlay() {
2126
2127        if (m_dragOverlay != null) {
2128            m_dragOverlay.removeFromParent();
2129            m_dragOverlay = null;
2130        }
2131    }
2132
2133    /**
2134     * Sets the placement context and initializes it, but cleans up the previous placement context, if any.
2135     *
2136     * @param newContext the placement context (can be null)
2137     */
2138    private void setPlacementContext(CmsPlacementModeContext newContext) {
2139
2140        CmsPlacementModeContext oldContext = m_placementContext;
2141        m_placementContext = null;
2142        if (oldContext != null) {
2143            oldContext.destroy();
2144        }
2145        m_placementContext = newContext;
2146        if (m_placementContext != null) {
2147            m_placementContext.init();
2148        }
2149    }
2150
2151    /**
2152     * Function which is called when the drag process is stopped, either by cancelling or dropping.<p>
2153     *
2154     * @param handler the drag and drop handler
2155     */
2156    private void stopDrag(final CmsDNDHandler handler) {
2157
2158        removeDragOverlay();
2159        setPlacementContext(null);
2160        CmsContainerPageContainer.clearResizeHelper();
2161        for (I_CmsDropTarget target : m_dragInfos.keySet()) {
2162            target.getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging());
2163            target.getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().clearFix());
2164            Style targetStyle = target.getElement().getStyle();
2165            if (!(target instanceof CmsGroupContainerElementPanel)) {
2166                targetStyle.clearPosition();
2167            }
2168            targetStyle.clearMarginTop();
2169            targetStyle.clearMarginBottom();
2170            if (target instanceof I_CmsDropContainer) {
2171                ((I_CmsDropContainer)target).removeHighlighting();
2172            }
2173        }
2174        if ((!handler.isPlacementMode()) && (handler.getDragHelper() != null)) {
2175            handler.getDragHelper().removeFromParent();
2176        }
2177        m_copyGroupId = null;
2178        m_currentTarget = null;
2179        m_currentIndex = -1;
2180        m_controller.getHandler().deactivateMenuButton();
2181        final List<I_CmsDropTarget> dragTargets = new ArrayList<I_CmsDropTarget>(m_dragInfos.keySet());
2182        m_dragInfos.clear();
2183        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
2184
2185            /**
2186             * @see com.google.gwt.user.client.Command#execute()
2187             */
2188            public void execute() {
2189
2190                if (!handler.isPlacementMode()) {
2191                    handler.clearTargets();
2192                }
2193                m_controller.resetEditButtons();
2194                // in case of group editing, this will refresh the container position and size
2195                for (I_CmsDropTarget target : dragTargets) {
2196                    if (target instanceof I_CmsDropContainer) {
2197                        ((I_CmsDropContainer)target).refreshHighlighting();
2198                        // reset the nested container infos
2199                        ((I_CmsDropContainer)target).clearDnDChildren();
2200                    }
2201                }
2202            }
2203        });
2204        if ((!handler.isPlacementMode()) && (handler.getDraggable() instanceof CmsContainerPageElementPanel)) {
2205            ((CmsContainerPageElementPanel)(handler.getDraggable())).removeHighlighting();
2206        }
2207        if (handler.isPlacementMode()) {
2208            handler.clearPlacement();
2209        }
2210    }
2211
2212    /**
2213     * Updates the drag target highlighting.<p>
2214     *
2215     * @param initial <code>true</code> when initially highlighting the drop containers
2216     * @param placementMode when we're in placement mode
2217     */
2218    private void updateHighlighting(boolean initial, boolean placementMode) {
2219
2220        Map<I_CmsDropContainer, CmsPositionBean> containers = new HashMap<I_CmsDropContainer, CmsPositionBean>();
2221        for (I_CmsDropTarget target : m_dragInfos.keySet()) {
2222            if ((target instanceof I_CmsDropContainer) && (target.getElement().getOffsetParent() != null)) {
2223                if (initial && (target != m_initialDropTarget)) {
2224                    ((I_CmsDropContainer)target).highlightContainer(placementMode);
2225                } else {
2226                    ((I_CmsDropContainer)target).updatePositionInfo();
2227                }
2228                containers.put((I_CmsDropContainer)target, ((I_CmsDropContainer)target).getPositionInfo());
2229            }
2230        }
2231
2232        List<I_CmsDropContainer> containersToMatch = new ArrayList<I_CmsDropContainer>(containers.keySet());
2233        if (!placementMode) {
2234            // in placement mode, only one container is highlighted at a time, so we don't need to run the collision avoidance
2235            for (I_CmsDropContainer contA : containers.keySet()) {
2236                containersToMatch.remove(contA);
2237                for (I_CmsDropContainer contB : containersToMatch) {
2238                    CmsPositionBean posA = containers.get(contA);
2239                    CmsPositionBean posB = containers.get(contB);
2240                    if (CmsPositionBean.checkCollision(posA, posB, HIGHLIGHTING_OFFSET * 3)) {
2241                        if (contA.hasDnDChildren() && contA.getDnDChildren().contains(contB)) {
2242                            if (!posA.isInside(posB, HIGHLIGHTING_OFFSET)) {
2243                                // the nested container is not completely inside the other
2244                                // increase the size of the outer container
2245                                posA.ensureSurrounds(posB, HIGHLIGHTING_OFFSET);
2246                            }
2247                        } else if (contB.hasDnDChildren() && contB.getDnDChildren().contains(contA)) {
2248                            if (!posB.isInside(posA, HIGHLIGHTING_OFFSET)) {
2249                                // the nested container is not completely inside the other
2250                                // increase the size of the outer container
2251                                posB.ensureSurrounds(posA, HIGHLIGHTING_OFFSET);
2252                            }
2253                        } else {
2254                            CmsPositionBean.avoidCollision(posA, posB, HIGHLIGHTING_OFFSET * 3);
2255                        }
2256                    }
2257                }
2258            }
2259        }
2260
2261        for (Entry<I_CmsDropContainer, CmsPositionBean> containerEntry : containers.entrySet()) {
2262            containerEntry.getKey().refreshHighlighting(containerEntry.getValue());
2263        }
2264    }
2265}