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