001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.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 * Checks if the 'copy' option for container elements should be enabled. 1510 * 1511 * @param cachedElementData the element data 1512 * @return true if the copy option should be enabled 1513 */ 1514 protected boolean isCopyableElement(CmsContainerElementData cachedElementData) { 1515 1516 if (cachedElementData.isModelGroup() 1517 || cachedElementData.isCopyDisabled() 1518 || cachedElementData.isWasModelGroup()) { 1519 return false; 1520 } 1521 if (cachedElementData.hasNamePatternProperty() && !cachedElementData.hasWritePermission()) { 1522 return false; 1523 } 1524 return CmsContainerpageController.get().isGalleryCreatableType(cachedElementData.getResourceType()); 1525 } 1526 1527 /** 1528 * Prepares all helper elements for the different drop targets.<p> 1529 * 1530 * @param elementData the element data 1531 * @param handler the drag and drop handler 1532 * @param draggable the draggable 1533 */ 1534 protected void prepareHelperElements( 1535 CmsContainerElementData elementData, 1536 CmsDNDHandler handler, 1537 I_CmsDraggable draggable) { 1538 1539 if (elementData == null) { 1540 CmsDebugLog.getInstance().printLine("elementData == null!"); 1541 if (!handler.isPlacementMode()) { 1542 handler.cancel(); 1543 } 1544 return; 1545 } 1546 if (!handler.isPlacementMode() && !handler.isDragging()) { 1547 return; 1548 } 1549 if (elementData.isGroup()) { 1550 m_copyGroupId = m_draggableId; 1551 } else { 1552 m_copyGroupId = null; 1553 } 1554 1555 if ((elementData.getDndId() != null) && (m_controller.getCachedElement(elementData.getDndId()) != null)) { 1556 m_draggableId = elementData.getDndId(); 1557 elementData = m_controller.getCachedElement(m_draggableId); 1558 } else { 1559 m_draggableId = elementData.getClientId(); 1560 } 1561 if (!elementData.isGroupContainer() && !elementData.isInheritContainer()) { 1562 for (CmsContainerPageContainer container : m_controller.getContainerTargets().values()) { 1563 String containerId = container.getContainerId(); 1564 loadCss(elementData, containerId); 1565 } 1566 } 1567 1568 if (m_controller.isGroupcontainerEditing()) { 1569 CmsGroupContainerElementPanel groupContainer = m_controller.getGroupcontainer(); 1570 if ((groupContainer != m_initialDropTarget) 1571 && !(elementData.isGroupContainer() || elementData.isInheritContainer()) 1572 && (elementData.getContents().get(groupContainer.getContainerId()) != null)) { 1573 Element placeholder = null; 1574 String containerId = groupContainer.getContainerId(); 1575 loadCss(elementData, containerId); 1576 placeholder = createPlaceholderFromHtmlForContainer(elementData, placeholder, containerId); 1577 1578 if (placeholder != null) { 1579 prepareDragInfo(placeholder, groupContainer, handler); 1580 groupContainer.highlightContainer(false); 1581 } 1582 } 1583 return; 1584 } 1585 if (!m_controller.isEditingDisabled()) { 1586 for (CmsContainerPageContainer container : m_controller.getContainerTargets().values()) { 1587 1588 if (draggable.getElement().isOrHasChild(container.getElement())) { 1589 // skip containers that are children of the draggable element 1590 continue; 1591 } 1592 if ((container != m_initialDropTarget) 1593 && !container.isDetailView() 1594 && (m_controller.getData().isModelGroup() || !container.hasModelGroupParent()) 1595 && (elementData.getContents().get(container.getContainerId()) != null)) { 1596 1597 Element placeholder = null; 1598 if (elementData.isGroupContainer() || elementData.isInheritContainer()) { 1599 placeholder = DOM.createDiv(); 1600 String content = ""; 1601 for (String groupId : elementData.getSubItems()) { 1602 CmsContainerElementData subData = m_controller.getCachedElement(groupId); 1603 if (subData != null) { 1604 if (subData.isShowInContext( 1605 CmsContainerpageController.get().getData().getTemplateContextInfo().getCurrentContext())) { 1606 if ((subData.getContents().get(container.getContainerId()) != null)) { 1607 content += subData.getContents().get(container.getContainerId()); 1608 } 1609 } else { 1610 content += "<div class='" 1611 + CmsTemplateContextInfo.DUMMY_ELEMENT_MARKER 1612 + "' style='display: none !important;'></div>"; 1613 } 1614 } 1615 } 1616 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(content)) { 1617 placeholder.setInnerHTML(content); 1618 // ensure any embedded flash players are set opaque so UI elements may be placed above them 1619 CmsDomUtil.fixFlashZindex(placeholder); 1620 } else { 1621 placeholder.addClassName( 1622 I_CmsLayoutBundle.INSTANCE.containerpageCss().emptyGroupContainer()); 1623 } 1624 } else { 1625 1626 placeholder = createPlaceholderFromHtmlForContainer( 1627 elementData, 1628 placeholder, 1629 container.getContainerId()); 1630 } 1631 if (placeholder != null) { 1632 prepareDragInfo(placeholder, container, handler); 1633 } 1634 } 1635 } 1636 } 1637 initNestedContainers(); 1638 1639 // add highlighting after all drag targets have been initialized 1640 updateHighlighting(true, handler.isPlacementMode()); 1641 1642 } 1643 1644 /** 1645 * Inserts e new container element into the container once the drag and drop is finished.<p> 1646 * 1647 * @param elementData the element data to create the element from 1648 * @param container the target container 1649 * @param index the element index 1650 * @param draggable the original drag element 1651 * @param modelReplaceId the model replace id 1652 */ 1653 void insertDropElement( 1654 CmsContainerElementData elementData, 1655 I_CmsDropContainer container, 1656 int index, 1657 I_CmsDraggable draggable, 1658 String modelReplaceId) { 1659 1660 try { 1661 CmsContainerPageElementPanel containerElement = m_controller.getContainerpageUtil().createElement( 1662 elementData, 1663 container, 1664 isNew(draggable)); 1665 if (isNew(draggable)) { 1666 containerElement.setNewType(CmsContainerpageController.getServerId(m_draggableId)); 1667 } else { 1668 m_controller.addToRecentList(elementData.getClientId(), null); 1669 } 1670 1671 if (index >= container.getWidgetCount()) { 1672 container.add(containerElement); 1673 } else { 1674 container.insert(containerElement, index); 1675 } 1676 if (draggable instanceof CmsContainerPageElementPanel) { 1677 ((CmsContainerPageElementPanel)draggable).removeFromParent(); 1678 } 1679 m_controller.initializeSubContainers(containerElement); 1680 1681 if (modelReplaceId != null) { 1682 m_controller.executeCopyModelReplace( 1683 modelReplaceId, 1684 ((CmsContainerPageContainer)container).getFormerModelGroupParent(), 1685 m_controller.getCachedElement(m_draggableId)); 1686 } 1687 1688 if (!m_controller.isGroupcontainerEditing()) { 1689 if (containerElement.hasReloadMarker()) { 1690 m_controller.setPageChanged(new Runnable() { 1691 1692 public void run() { 1693 1694 CmsContainerpageController.get().reloadPage(); 1695 } 1696 }); 1697 } else { 1698 m_controller.setPageChanged(); 1699 } 1700 if (m_added) { 1701 m_controller.sendElementAdded(containerElement); 1702 } else { 1703 m_controller.sendElementMoved(containerElement); 1704 } 1705 } 1706 if (m_controller.isGroupcontainerEditing()) { 1707 container.getElement().removeClassName( 1708 I_CmsLayoutBundle.INSTANCE.containerpageCss().emptyGroupContainer()); 1709 } 1710 1711 } catch (Exception e) { 1712 CmsErrorDialog.handleException(e.getMessage(), e); 1713 } 1714 } 1715 1716 /** 1717 1718 * Checks and sets if the place holder should be visible.<p> 1719 * 1720 * @param target the target container 1721 */ 1722 private void checkPlaceholderVisibility(I_CmsDropTarget target) { 1723 1724 if (target instanceof I_CmsDropContainer) { 1725 I_CmsDropContainer container = (I_CmsDropContainer)target; 1726 container.setPlaceholderVisibility( 1727 (container != m_initialDropTarget) 1728 || ((m_currentIndex - m_originalIndex) > 1) 1729 || ((m_originalIndex - m_currentIndex) > 0)); 1730 } 1731 } 1732 1733 /** 1734 * Creates a placeholder for an element and a specific container. 1735 * 1736 * @param elementData the element data 1737 * @param placeholder the previous placeholder 1738 * @param containerId the container id 1739 * @return the element 1740 */ 1741 private Element createPlaceholderFromHtmlForContainer( 1742 CmsContainerElementData elementData, 1743 Element placeholder, 1744 String containerId) { 1745 1746 try { 1747 String htmlContent = elementData.getContents().get(containerId); 1748 placeholder = CmsDomUtil.createElement(htmlContent); 1749 // ensure any embedded flash players are set opaque so UI elements may be placed above them 1750 CmsDomUtil.fixFlashZindex(placeholder); 1751 placeholder.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragPlaceholder()); 1752 } catch (Exception e) { 1753 CmsDebugLog.getInstance().printLine(e.getMessage()); 1754 } 1755 return placeholder; 1756 } 1757 1758 /** 1759 * Checks if the placeholder position has changed.<p> 1760 * 1761 * @param target the current drop target 1762 * 1763 * @return <code>true</code> if the placeholder position has changed 1764 */ 1765 private boolean hasChangedPosition(I_CmsDropTarget target) { 1766 1767 if ((m_currentTarget != target) || (m_currentIndex != target.getPlaceholderIndex())) { 1768 m_currentTarget = target; 1769 m_currentIndex = target.getPlaceholderIndex(); 1770 return true; 1771 } 1772 return false; 1773 } 1774 1775 /** 1776 * Initializes the nested container infos.<p> 1777 */ 1778 private void initNestedContainers() { 1779 1780 for (CmsContainer container : m_controller.getContainers().values()) { 1781 if (container.isSubContainer()) { 1782 CmsContainerPageContainer containerWidget = m_controller.m_targetContainers.get(container.getName()); 1783 // check if the sub container is a valid drop targets 1784 if (m_dragInfos.keySet().contains(containerWidget)) { 1785 CmsContainer parentContainer = m_controller.getContainers().get(container.getParentContainerName()); 1786 // add the container to all it's ancestors as a dnd child 1787 while (parentContainer != null) { 1788 if (m_dragInfos.keySet().contains( 1789 m_controller.m_targetContainers.get(parentContainer.getName()))) { 1790 m_controller.m_targetContainers.get(parentContainer.getName()).addDndChild(containerWidget); 1791 } 1792 if (parentContainer.isSubContainer()) { 1793 parentContainer = m_controller.getContainers().get( 1794 parentContainer.getParentContainerName()); 1795 } else { 1796 parentContainer = null; 1797 } 1798 } 1799 } 1800 } 1801 1802 } 1803 } 1804 1805 /** 1806 * Installs the drag overlay to avoid any mouse over issues or similar.<p> 1807 */ 1808 private void installDragOverlay() { 1809 1810 if (m_dragOverlay != null) { 1811 m_dragOverlay.removeFromParent(); 1812 } 1813 m_dragOverlay = DOM.createDiv(); 1814 m_dragOverlay.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragOverlay()); 1815 Document.get().getBody().appendChild(m_dragOverlay); 1816 } 1817 1818 /** 1819 * Checks whether the current placeholder position represents a change to the original draggable position within the tree.<p> 1820 * 1821 * @param target the current drop target 1822 * 1823 * @return <code>true</code> if the position changed 1824 */ 1825 private boolean isChangedPosition(I_CmsDropTarget target) { 1826 1827 // if the new index is not next to the old one, the position has changed 1828 if ((target != m_initialDropTarget) 1829 || !((target.getPlaceholderIndex() == (m_originalIndex + 1)) 1830 || (target.getPlaceholderIndex() == m_originalIndex))) { 1831 return true; 1832 } 1833 return false; 1834 } 1835 1836 /** 1837 * Checks if the draggable item is a copy model.<p> 1838 * 1839 * @param draggable the draggable item 1840 * 1841 * @return true if the item is a copy model 1842 */ 1843 private boolean isCopyModel(I_CmsDraggable draggable) { 1844 1845 if (!(draggable instanceof CmsResultListItem)) { 1846 return false; 1847 } 1848 return ((CmsResultListItem)draggable).getResult().isCopyModel(); 1849 } 1850 1851 /** 1852 * Checks if the given draggable item is an image.<p> 1853 * 1854 * @param draggable the item to check 1855 * 1856 * @return true if the given item is an image 1857 */ 1858 private boolean isImage(I_CmsDraggable draggable) { 1859 1860 return (draggable instanceof CmsResultListItem) 1861 && CmsToolbarAllGalleriesMenu.DND_MARKER.equals(((CmsResultListItem)draggable).getData()); 1862 1863 } 1864 1865 /** 1866 * Checks if the given draggable is new. 1867 * 1868 * @param draggable the draggable 1869 * @return true if the given draggable is new 1870 */ 1871 private boolean isNew(I_CmsDraggable draggable) { 1872 1873 boolean result = isNewId(draggable.getId()); 1874 return result; 1875 } 1876 1877 /** 1878 * Ensures that the CSS is loaded for the given element data and container. 1879 * 1880 * @param elementData the element data 1881 * @param containerId he container id 1882 */ 1883 private void loadCss(CmsContainerElementData elementData, String containerId) { 1884 1885 Set<String> cssResources = elementData.getCssResources(containerId); 1886 if ((cssResources != null) && !cssResources.isEmpty()) { 1887 // the element requires certain CSS resources, check if present and include if necessary 1888 for (String cssResourceLink : cssResources) { 1889 CmsDomUtil.ensureStyleSheetIncluded(cssResourceLink); 1890 } 1891 } 1892 } 1893 1894 /** 1895 * Drop handling code without the part that handles image DnD. 1896 * 1897 * <p>Also used by placement mode. 1898 * 1899 * @param draggable the draggable element 1900 * @param target the drag target 1901 * @param handler the DND handler 1902 * @param ctrlMode if true, user is holding control or meta 1903 */ 1904 private void onDropInternal( 1905 final I_CmsDraggable draggable, 1906 final I_CmsDropTarget target, 1907 CmsDNDHandler handler, 1908 boolean ctrlMode) { 1909 1910 m_added = (draggable instanceof CmsListItem) && (target instanceof CmsContainerPageContainer); 1911 1912 if (target != m_initialDropTarget) { 1913 if (target instanceof I_CmsDropContainer) { 1914 final I_CmsDropContainer container = (I_CmsDropContainer)target; 1915 final int index = container.getPlaceholderIndex(); 1916 final String modelReplaceId = container instanceof CmsContainerPageContainer 1917 ? ((CmsContainerPageContainer)container).getCopyModelReplaceId() 1918 : null; 1919 if (!isNew(draggable) && (draggable instanceof CmsListItem)) { 1920 // existing element from add menu 1921 final String copyGroupId = m_copyGroupId; 1922 AsyncCallback<String> modeCallback = new AsyncCallback<String>() { 1923 1924 public void onFailure(Throwable caught) { 1925 1926 if (caught != null) { 1927 CmsErrorDialog.handleException(caught); 1928 } 1929 } 1930 1931 public void onSuccess(String result) { 1932 1933 if (Objects.equal(result, CmsEditorConstants.MODE_COPY)) { 1934 final CmsContainerpageController controller = CmsContainerpageController.get(); 1935 if (copyGroupId == null) { 1936 1937 CmsContainerElementData data = controller.getCachedElement(m_draggableId); 1938 final Map<String, String> settings = data.getSettings(); 1939 1940 controller.copyElement( 1941 CmsContainerpageController.getServerId(m_draggableId), 1942 new I_CmsSimpleCallback<CmsUUID>() { 1943 1944 public void execute(CmsUUID resultId) { 1945 1946 controller.getElementWithSettings( 1947 "" + resultId, 1948 settings, 1949 new I_CmsSimpleCallback<CmsContainerElementData>() { 1950 1951 public void execute(CmsContainerElementData newData) { 1952 1953 insertDropElement( 1954 newData, 1955 container, 1956 index, 1957 draggable, 1958 modelReplaceId); 1959 container.removePlaceholder(); 1960 } 1961 }); 1962 } 1963 }); 1964 } else { 1965 controller.getElementForDragAndDropFromContainer( 1966 copyGroupId, 1967 m_originalContainerId, 1968 true, 1969 new I_CmsSimpleCallback<CmsContainerElementData>() { 1970 1971 public void execute(CmsContainerElementData arg) { 1972 1973 insertDropElement(arg, container, index, draggable, modelReplaceId); 1974 container.removePlaceholder(); 1975 } 1976 }); 1977 } 1978 1979 } else if (Objects.equal(result, CmsEditorConstants.MODE_REUSE)) { 1980 insertDropElement( 1981 m_controller.getCachedElement(m_draggableId), 1982 container, 1983 index, 1984 draggable, 1985 modelReplaceId); 1986 container.removePlaceholder(); 1987 } 1988 } 1989 }; 1990 1991 CmsUUID structureId = new CmsUUID(CmsContainerpageController.getServerId(m_draggableId)); 1992 CmsContainerElementData cachedElementData = m_controller.getCachedElement(m_draggableId); 1993 ElementReuseMode reuseMode = isCopyModel(draggable) 1994 ? ElementReuseMode.copy 1995 : (((cachedElementData != null) && !cachedElementData.isCopyInModels()) 1996 ? ElementReuseMode.reuse 1997 : CmsContainerpageController.get().getData().getElementReuseMode()); 1998 if (((!handler.isPlacementMode()) && handler.hasModifierCTRL()) || ctrlMode) { 1999 reuseMode = ElementReuseMode.ask; 2000 } 2001 if (reuseMode != ElementReuseMode.reuse) { 2002 2003 if ((cachedElementData != null) && !isCopyableElement(cachedElementData)) { 2004 // User is not allowed to create this element in current view, so reuse the element instead 2005 reuseMode = ElementReuseMode.reuse; 2006 } 2007 } 2008 switch (reuseMode) { 2009 case ask: 2010 // when dropping elements from the into the page, we ask the user if the dropped element should 2011 // be used, or a copy of it. If the user wants a copy, we copy the corresponding resource and replace the element 2012 // in the page 2013 CmsDroppedElementModeSelectionDialog.showDialog(structureId, modeCallback); 2014 break; 2015 case copy: 2016 modeCallback.onSuccess(CmsEditorConstants.MODE_COPY); 2017 break; 2018 case reuse: 2019 default: 2020 modeCallback.onSuccess(CmsEditorConstants.MODE_REUSE); 2021 break; 2022 } 2023 } else { 2024 // new article or moved from different container 2025 insertDropElement(m_controller.getCachedElement(m_draggableId), container, index, draggable, null); 2026 } 2027 } else if (target instanceof CmsList<?>) { 2028 m_controller.addToFavoriteList(m_draggableId); 2029 } 2030 } else if ((target instanceof I_CmsDropContainer) 2031 && (draggable instanceof CmsContainerPageElementPanel) 2032 && isChangedPosition(target)) { 2033 CmsDomUtil.showOverlay(draggable.getElement(), false); 2034 I_CmsDropContainer container = (I_CmsDropContainer)target; 2035 int count = container.getWidgetCount(); 2036 if (!handler.isPlacementMode()) { 2037 handler.getPlaceholder().getStyle().setDisplay(Display.NONE); 2038 } 2039 if (container.getPlaceholderIndex() >= count) { 2040 container.add((CmsContainerPageElementPanel)draggable); 2041 } else { 2042 container.insert((CmsContainerPageElementPanel)draggable, container.getPlaceholderIndex()); 2043 } 2044 m_controller.addToRecentList(m_draggableId, null); 2045 m_controller.sendElementMoved((CmsContainerPageElementPanel)draggable); 2046 // changes are only relevant to the container page if not group-container editing 2047 if (!m_controller.isGroupcontainerEditing()) { 2048 m_controller.setPageChanged(); 2049 } 2050 } else if (draggable instanceof CmsContainerPageElementPanel) { 2051 CmsDomUtil.showOverlay(draggable.getElement(), false); 2052 // to reset mouse over state remove and attach the option bar 2053 CmsContainerPageElementPanel containerElement = (CmsContainerPageElementPanel)draggable; 2054 CmsElementOptionBar optionBar = containerElement.getElementOptionBar(); 2055 optionBar.removeFromParent(); 2056 containerElement.setElementOptionBar(optionBar); 2057 } 2058 stopDrag(handler); 2059 } 2060 2061 /** 2062 * Placces an element relative to an existing element in placement mode. 2063 * 2064 * @param handler the handler 2065 * @param draggable the element to be placed 2066 * @param elem the data for the element 2067 * @param reference the element relative to which the placed element should be inserted 2068 * @param offset the index offset for the new element relative to the reference element (0 means 'before the reference element'!) 2069 */ 2070 private void placeElement( 2071 CmsDNDHandler handler, 2072 I_CmsDraggable draggable, 2073 CmsContainerElementData elem, 2074 CmsContainerPageElementPanel reference, 2075 int offset, 2076 boolean ctrlMode) { 2077 2078 CmsContainerPageContainer cnt = (CmsContainerPageContainer)reference.getParentTarget(); 2079 int index = cnt.getWidgetIndex(reference) + offset; 2080 if (index < 0) { 2081 index = 0; 2082 } 2083 cnt.setPlaceholderIndex(index); 2084 onDropInternal(draggable, cnt, handler, ctrlMode); 2085 // Placeholder index is not reset by onDrop for container -> container transfer 2086 cnt.removePlaceholder(); 2087 handler.clearPlacement(); 2088 } 2089 2090 /** 2091 * Places an element into an empty container in placement mode. 2092 * 2093 * @param handler the handler 2094 * @param draggable the draggable for the element 2095 * @param elem the data for the element 2096 * @param cnt the target container 2097 */ 2098 private void placeNewElement( 2099 CmsDNDHandler handler, 2100 I_CmsDraggable draggable, 2101 CmsContainerElementData elem, 2102 CmsContainerPageContainer cnt) { 2103 2104 cnt.setPlaceholderIndex(0); 2105 onDropInternal(draggable, cnt, handler, false); 2106 // Placeholder index is not reset by onDrop for container -> container transfer 2107 cnt.removePlaceholder(); 2108 handler.clearPlacement(); 2109 } 2110 2111 /** 2112 * Sets styles of helper elements, appends the to the drop target and puts them into a drag info bean.<p> 2113 * 2114 * @param placeholder the placeholder element 2115 * @param target the drop target 2116 * @param handler the drag and drop handler 2117 */ 2118 private void prepareDragInfo(Element placeholder, I_CmsDropContainer target, CmsDNDHandler handler) { 2119 2120 target.getElement().addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging()); 2121 String positioning = CmsDomUtil.getCurrentStyle( 2122 target.getElement(), 2123 org.opencms.gwt.client.util.CmsDomUtil.Style.position); 2124 // set target relative, if not absolute or fixed 2125 if (!Position.ABSOLUTE.getCssName().equals(positioning) && !Position.FIXED.getCssName().equals(positioning)) { 2126 target.getElement().getStyle().setPosition(Position.RELATIVE); 2127 // check for empty containers that don't have a minimum top and bottom margin to avoid containers overlapping 2128 if (target.getElement().getFirstChildElement() == null) { 2129 if (CmsDomUtil.getCurrentStyleInt( 2130 target.getElement(), 2131 CmsDomUtil.Style.marginTop) < MINIMUM_CONTAINER_MARGIN) { 2132 target.getElement().getStyle().setMarginTop(MINIMUM_CONTAINER_MARGIN, Unit.PX); 2133 } 2134 if (CmsDomUtil.getCurrentStyleInt( 2135 target.getElement(), 2136 CmsDomUtil.Style.marginBottom) < MINIMUM_CONTAINER_MARGIN) { 2137 target.getElement().getStyle().setMarginBottom(MINIMUM_CONTAINER_MARGIN, Unit.PX); 2138 } 2139 } 2140 } 2141 m_dragInfos.put(target, placeholder); 2142 if (!handler.isPlacementMode()) { 2143 handler.addTarget(target); 2144 } 2145 } 2146 2147 /** 2148 * Prepares the target container.<p> 2149 * 2150 * @param targetContainer the container 2151 * @param draggable the draggable 2152 * @param placeholder the placeholder 2153 */ 2154 private void prepareTargetContainer( 2155 I_CmsDropContainer targetContainer, 2156 I_CmsDraggable draggable, 2157 Element placeholder) { 2158 2159 String positioning = CmsDomUtil.getCurrentStyle( 2160 targetContainer.getElement(), 2161 org.opencms.gwt.client.util.CmsDomUtil.Style.position); 2162 // set target relative, if not absolute or fixed 2163 if (!Position.ABSOLUTE.getCssName().equals(positioning) && !Position.FIXED.getCssName().equals(positioning)) { 2164 targetContainer.getElement().getStyle().setPosition(Position.RELATIVE); 2165 } 2166 m_originalIndex = targetContainer.getWidgetIndex((Widget)draggable); 2167 targetContainer.getElement().insertBefore(placeholder, draggable.getElement()); 2168 targetContainer.getElement().addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging()); 2169 CmsDomUtil.showOverlay(draggable.getElement(), true); 2170 targetContainer.highlightContainer(false); 2171 } 2172 2173 /** 2174 * Removes the drag overlay.<p> 2175 */ 2176 private void removeDragOverlay() { 2177 2178 if (m_dragOverlay != null) { 2179 m_dragOverlay.removeFromParent(); 2180 m_dragOverlay = null; 2181 } 2182 } 2183 2184 /** 2185 * Sets the placement context and initializes it, but cleans up the previous placement context, if any. 2186 * 2187 * @param newContext the placement context (can be null) 2188 */ 2189 private void setPlacementContext(CmsPlacementModeContext newContext) { 2190 2191 CmsPlacementModeContext oldContext = m_placementContext; 2192 m_placementContext = null; 2193 if (oldContext != null) { 2194 oldContext.destroy(); 2195 } 2196 m_placementContext = newContext; 2197 if (m_placementContext != null) { 2198 m_placementContext.init(); 2199 } 2200 } 2201 2202 /** 2203 * Function which is called when the drag process is stopped, either by cancelling or dropping.<p> 2204 * 2205 * @param handler the drag and drop handler 2206 */ 2207 private void stopDrag(final CmsDNDHandler handler) { 2208 2209 removeDragOverlay(); 2210 setPlacementContext(null); 2211 CmsContainerPageContainer.clearResizeHelper(); 2212 for (I_CmsDropTarget target : m_dragInfos.keySet()) { 2213 target.getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragging()); 2214 target.getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().clearFix()); 2215 Style targetStyle = target.getElement().getStyle(); 2216 if (!(target instanceof CmsGroupContainerElementPanel)) { 2217 targetStyle.clearPosition(); 2218 } 2219 targetStyle.clearMarginTop(); 2220 targetStyle.clearMarginBottom(); 2221 if (target instanceof I_CmsDropContainer) { 2222 ((I_CmsDropContainer)target).removeHighlighting(); 2223 } 2224 } 2225 if ((!handler.isPlacementMode()) && (handler.getDragHelper() != null)) { 2226 handler.getDragHelper().removeFromParent(); 2227 } 2228 m_copyGroupId = null; 2229 m_currentTarget = null; 2230 m_currentIndex = -1; 2231 m_controller.getHandler().deactivateMenuButton(); 2232 final List<I_CmsDropTarget> dragTargets = new ArrayList<I_CmsDropTarget>(m_dragInfos.keySet()); 2233 m_dragInfos.clear(); 2234 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 2235 2236 /** 2237 * @see com.google.gwt.user.client.Command#execute() 2238 */ 2239 public void execute() { 2240 2241 if (!handler.isPlacementMode()) { 2242 handler.clearTargets(); 2243 } 2244 m_controller.resetEditButtons(); 2245 // in case of group editing, this will refresh the container position and size 2246 for (I_CmsDropTarget target : dragTargets) { 2247 if (target instanceof I_CmsDropContainer) { 2248 ((I_CmsDropContainer)target).refreshHighlighting(); 2249 // reset the nested container infos 2250 ((I_CmsDropContainer)target).clearDnDChildren(); 2251 } 2252 } 2253 } 2254 }); 2255 if ((!handler.isPlacementMode()) && (handler.getDraggable() instanceof CmsContainerPageElementPanel)) { 2256 ((CmsContainerPageElementPanel)(handler.getDraggable())).removeHighlighting(); 2257 } 2258 if (handler.isPlacementMode()) { 2259 handler.clearPlacement(); 2260 } 2261 } 2262 2263 /** 2264 * Updates the drag target highlighting.<p> 2265 * 2266 * @param initial <code>true</code> when initially highlighting the drop containers 2267 * @param placementMode when we're in placement mode 2268 */ 2269 private void updateHighlighting(boolean initial, boolean placementMode) { 2270 2271 Map<I_CmsDropContainer, CmsPositionBean> containers = new HashMap<I_CmsDropContainer, CmsPositionBean>(); 2272 for (I_CmsDropTarget target : m_dragInfos.keySet()) { 2273 if ((target instanceof I_CmsDropContainer) && (target.getElement().getOffsetParent() != null)) { 2274 if (initial && (target != m_initialDropTarget)) { 2275 ((I_CmsDropContainer)target).highlightContainer(placementMode); 2276 } else { 2277 ((I_CmsDropContainer)target).updatePositionInfo(); 2278 } 2279 containers.put((I_CmsDropContainer)target, ((I_CmsDropContainer)target).getPositionInfo()); 2280 } 2281 } 2282 2283 List<I_CmsDropContainer> containersToMatch = new ArrayList<I_CmsDropContainer>(containers.keySet()); 2284 if (!placementMode) { 2285 // in placement mode, only one container is highlighted at a time, so we don't need to run the collision avoidance 2286 for (I_CmsDropContainer contA : containers.keySet()) { 2287 containersToMatch.remove(contA); 2288 for (I_CmsDropContainer contB : containersToMatch) { 2289 CmsPositionBean posA = containers.get(contA); 2290 CmsPositionBean posB = containers.get(contB); 2291 if (CmsPositionBean.checkCollision(posA, posB, HIGHLIGHTING_OFFSET * 3)) { 2292 if (contA.hasDnDChildren() && contA.getDnDChildren().contains(contB)) { 2293 if (!posA.isInside(posB, HIGHLIGHTING_OFFSET)) { 2294 // the nested container is not completely inside the other 2295 // increase the size of the outer container 2296 posA.ensureSurrounds(posB, HIGHLIGHTING_OFFSET); 2297 } 2298 } else if (contB.hasDnDChildren() && contB.getDnDChildren().contains(contA)) { 2299 if (!posB.isInside(posA, HIGHLIGHTING_OFFSET)) { 2300 // the nested container is not completely inside the other 2301 // increase the size of the outer container 2302 posB.ensureSurrounds(posA, HIGHLIGHTING_OFFSET); 2303 } 2304 } else { 2305 CmsPositionBean.avoidCollision(posA, posB, HIGHLIGHTING_OFFSET * 3); 2306 } 2307 } 2308 } 2309 } 2310 } 2311 2312 for (Entry<I_CmsDropContainer, CmsPositionBean> containerEntry : containers.entrySet()) { 2313 containerEntry.getKey().refreshHighlighting(containerEntry.getValue()); 2314 } 2315 } 2316}