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.ui; 029 030import org.opencms.acacia.client.CmsEditorBase; 031import org.opencms.acacia.client.I_CmsInlineFormParent; 032import org.opencms.ade.containerpage.client.CmsContainerpageController; 033import org.opencms.ade.containerpage.client.ui.css.I_CmsLayoutBundle; 034import org.opencms.ade.containerpage.shared.CmsElementLockInfo; 035import org.opencms.ade.containerpage.shared.CmsInheritanceInfo; 036import org.opencms.ade.contenteditor.client.CmsContentEditor; 037import org.opencms.gwt.client.I_CmsElementToolbarContext; 038import org.opencms.gwt.client.dnd.I_CmsDraggable; 039import org.opencms.gwt.client.dnd.I_CmsDropTarget; 040import org.opencms.gwt.client.ui.CmsHighlightingBorder; 041import org.opencms.gwt.client.ui.CmsListItemWidget; 042import org.opencms.gwt.client.ui.I_CmsButton; 043import org.opencms.gwt.client.util.CmsDebugLog; 044import org.opencms.gwt.client.util.CmsDomUtil; 045import org.opencms.gwt.client.util.CmsDomUtil.Tag; 046import org.opencms.gwt.client.util.CmsPositionBean; 047import org.opencms.gwt.shared.CmsGwtConstants; 048import org.opencms.gwt.shared.CmsListInfoBean; 049import org.opencms.util.CmsStringUtil; 050import org.opencms.util.CmsUUID; 051 052import java.util.Arrays; 053import java.util.HashMap; 054import java.util.Iterator; 055import java.util.List; 056import java.util.Map; 057import java.util.Map.Entry; 058 059import com.google.common.base.Optional; 060import com.google.common.collect.Lists; 061import com.google.gwt.core.client.Scheduler; 062import com.google.gwt.core.client.Scheduler.ScheduledCommand; 063import com.google.gwt.dom.client.Element; 064import com.google.gwt.dom.client.NodeList; 065import com.google.gwt.dom.client.Style; 066import com.google.gwt.dom.client.Style.Display; 067import com.google.gwt.dom.client.Style.Position; 068import com.google.gwt.dom.client.Style.Unit; 069import com.google.gwt.event.dom.client.ClickEvent; 070import com.google.gwt.event.dom.client.ClickHandler; 071import com.google.gwt.event.dom.client.HasClickHandlers; 072import com.google.gwt.event.shared.HandlerRegistration; 073import com.google.gwt.regexp.shared.MatchResult; 074import com.google.gwt.regexp.shared.RegExp; 075import com.google.gwt.user.client.DOM; 076import com.google.gwt.user.client.Event; 077import com.google.gwt.user.client.Event.NativePreviewEvent; 078import com.google.gwt.user.client.Event.NativePreviewHandler; 079import com.google.gwt.user.client.Timer; 080import com.google.gwt.user.client.ui.AbsolutePanel; 081import com.google.gwt.user.client.ui.IsWidget; 082import com.google.gwt.user.client.ui.RootPanel; 083 084import elemental2.core.JsArray; 085import elemental2.core.JsObject; 086import elemental2.dom.MutationObserver; 087import elemental2.dom.MutationObserver.MutationObserverCallbackFn; 088import elemental2.dom.MutationObserverInit; 089import elemental2.dom.MutationRecord; 090import jsinterop.base.Js; 091 092/** 093 * Content element within a container-page.<p> 094 * 095 * @since 8.0.0 096 */ 097public class CmsContainerPageElementPanel extends AbsolutePanel 098implements I_CmsDraggable, HasClickHandlers, I_CmsInlineFormParent { 099 100 /** 101 * Parses CSS classess of the form 'oc-point-TY_LX', where X and Y are strings 102 * of decimal digits possibly preceded by a minus sign.<p> 103 * 104 * The numeric values of Y and X will be available after a successful parse using the 105 * methods getOffsetTop() and getOffsetLeft(). 106 * 107 * This is used to offer the formatter developer some control over the edit point 108 * positioning. 109 */ 110 public static class PointPositioningParser { 111 112 /** Regular expression used to match the special CSS classes. */ 113 private static final RegExp REGEX = RegExp.compile("^oc-point-T(-?[0-9]+)_L(-?[0-9]+)$"); 114 115 /** The left offset. */ 116 private int m_left; 117 118 /** The top offset. */ 119 private int m_top; 120 121 /** 122 * Gets the left offset after a CSS class has successfully been parsed.<p> 123 * 124 * @return the left offset 125 */ 126 int getOffsetLeft() { 127 128 return m_left; 129 } 130 131 /** 132 * Gets the top offset after a CSS class has successfully been parsed.<p> 133 * 134 * @return the top offset 135 */ 136 int getOffsetTop() { 137 138 return m_top; 139 } 140 141 /** 142 * Tries to parse a point positioning instruction from an element's class attribute 143 * and returns true when successful.<p> 144 * 145 * @param cssClass the value of a class attribute 146 * 147 * @return true if a positioning instruction was found 148 */ 149 boolean tryParse(String cssClass) { 150 151 m_left = 0; 152 m_top = 0; 153 if (cssClass == null) { 154 cssClass = ""; 155 } 156 for (String token : cssClass.trim().split(" +")) { 157 if (tryParseSingleClass(token)) { 158 return true; 159 } 160 } 161 return false; 162 } 163 164 /** 165 * Parses a single token from a class attribute.<p> 166 * 167 * @param token the token 168 * @return true if the token was a point positioning instruction 169 */ 170 private boolean tryParseSingleClass(String token) { 171 172 MatchResult result = REGEX.exec(token); 173 if (result != null) { 174 m_top = Integer.parseInt(result.getGroup(1)); 175 m_left = Integer.parseInt(result.getGroup(2)); 176 return true; 177 } else { 178 if (token.startsWith("oc-point")) { 179 CmsDebugLog.consoleLog("Malformed oc-point class: " + token); 180 } 181 return false; 182 } 183 } 184 185 } 186 187 /** Property used to mark an element as belonging to this widget. */ 188 public static final String PROP_ELEMENT_OBJECT_ID = "element_object_id"; 189 190 /** The is model group property key. */ 191 public static final String PROP_IS_MODEL_GROUP = "is_model_group"; 192 193 /** The former copy model property. */ 194 public static final String PROP_WAS_MODEL_GROUP = "was_model_group"; 195 196 /** Highlighting border for this element. */ 197 protected CmsHighlightingBorder m_highlighting; 198 199 /** A flag which indicates whether the height has already been checked. */ 200 private boolean m_checkedHeight; 201 202 /** Flag indicating the the editables are currently being checked. */ 203 private boolean m_checkingEditables; 204 205 /** The elements client id. */ 206 private String m_clientId; 207 208 /** The 'create new' flag. */ 209 private boolean m_createNew; 210 211 /** 212 * Flag which indicates whether the new editor is disabled for this element.<p> 213 */ 214 private boolean m_disableNewEditor; 215 216 /** The direct edit bar instances. */ 217 private Map<Element, CmsListCollectorEditor> m_editables; 218 219 /** The editor click handler registration. */ 220 private HandlerRegistration m_editorClickHandlerRegistration; 221 222 /** The option bar, holding optional function buttons. */ 223 private CmsElementOptionBar m_elementOptionBar; 224 225 /** The element element view. */ 226 private CmsUUID m_elementView; 227 228 /** The overlay for expired elements. */ 229 private Element m_expiredOverlay; 230 231 /** Indicates an edit handler is configured for the element resource type. */ 232 private boolean m_hasEditHandler; 233 234 /** Indicates whether this element has settings to edit. */ 235 private boolean m_hasSettings; 236 237 /** The resource type icon CSS classes. */ 238 private String m_iconClasses; 239 240 /** The inheritance info for this element. */ 241 private CmsInheritanceInfo m_inheritanceInfo; 242 243 /** The lock information. */ 244 private CmsElementLockInfo m_lockInfo; 245 246 /** The model group id. */ 247 private CmsUUID m_modelGroupId; 248 249 /** The is new element type. */ 250 private String m_newType; 251 252 /** The registered mutation observer. */ 253 private MutationObserver m_mutationObserver; 254 255 /** The no edit reason, if empty editing is allowed. */ 256 private String m_noEditReason; 257 258 /** A random id, which is also stored as a property on the HTML element for this widget. */ 259 private String m_objectId; 260 261 /** The parent drop target. */ 262 private I_CmsDropContainer m_parent; 263 264 /** Parser for point positioning isntructions. */ 265 private PointPositioningParser m_positioningInstructionParser = new PointPositioningParser(); 266 267 /** The drag and drop parent div. */ 268 private Element m_provisionalParent; 269 270 /** Flag indicating if the element resource is currently released and not expired. */ 271 private boolean m_releasedAndNotExpired; 272 273 /** The resource type. */ 274 private String m_resourceType; 275 276 /** True if this element is marked as 'reused'. */ 277 private boolean m_reused; 278 279 /** The element resource site-path. */ 280 private String m_sitePath; 281 /** The sub title. */ 282 private String m_subTitle; 283 284 /** The resource title. */ 285 private String m_title; 286 287 /** 288 * Indicates if the current user has view permissions on the element resource. 289 * Without view permissions, the element can neither be edited, nor moved. 290 **/ 291 private boolean m_viewPermission; 292 293 /** The former copy model status. */ 294 private boolean m_wasModelGroup; 295 296 /** 297 * Indicates if the current user has write permissions on the element resource. 298 * Without write permissions, the element can not be edited. 299 **/ 300 private boolean m_writePermission; 301 302 /** 303 * Constructor.<p> 304 * 305 * @param element the DOM element 306 * @param parent the drag parent 307 * @param clientId the client id 308 * @param sitePath the element site-path 309 * @param noEditReason the no edit reason, if empty, editing is allowed 310 * @param lockInfo the lock information 311 * @param title the resource title 312 * @param subTitle the sub title 313 * @param resourceType the resource type 314 * @param hasSettings should be true if the element has settings which can be edited 315 * @param hasViewPermission indicates if the current user has view permissions on the element resource 316 * @param hasWritePermission indicates if the current user has write permissions on the element resource 317 * @param releasedAndNotExpired <code>true</code> if the element resource is currently released and not expired 318 * @param disableNewEditor flag to disable the new editor for this element 319 * @param hasEditHandler indicates an edit handler is configured for the element resource type 320 * @param modelGroupId the model group id 321 * @param wasModelGroup in case of a former copy model group 322 * @param elementView the element view of the element 323 * @param iconClasses the resource type icon CSS classes 324 * @param isReused true if this element is marked as reused 325 */ 326 public CmsContainerPageElementPanel( 327 Element element, 328 I_CmsDropContainer parent, 329 String clientId, 330 String sitePath, 331 String noEditReason, 332 CmsElementLockInfo lockInfo, 333 String title, 334 String subTitle, 335 String resourceType, 336 boolean hasSettings, 337 boolean hasViewPermission, 338 boolean hasWritePermission, 339 boolean releasedAndNotExpired, 340 boolean disableNewEditor, 341 boolean hasEditHandler, 342 CmsUUID modelGroupId, 343 boolean wasModelGroup, 344 CmsUUID elementView, 345 String iconClasses, 346 boolean isReused) { 347 348 super(element); 349 m_clientId = clientId; 350 m_objectId = "" + Math.random(); 351 m_sitePath = sitePath; 352 m_title = title; 353 m_subTitle = subTitle; 354 m_resourceType = resourceType; 355 m_noEditReason = noEditReason; 356 m_lockInfo = lockInfo; 357 m_hasSettings = hasSettings; 358 m_parent = parent; 359 m_disableNewEditor = disableNewEditor; 360 m_modelGroupId = modelGroupId; 361 m_wasModelGroup = wasModelGroup; 362 m_hasEditHandler = hasEditHandler; 363 setViewPermission(hasViewPermission); 364 setWritePermission(hasWritePermission); 365 setReleasedAndNotExpired(releasedAndNotExpired); 366 m_elementView = elementView; 367 getElement().setPropertyBoolean(PROP_IS_MODEL_GROUP, modelGroupId != null); 368 getElement().setPropertyBoolean(PROP_WAS_MODEL_GROUP, wasModelGroup); 369 getElement().setPropertyString(PROP_ELEMENT_OBJECT_ID, m_objectId); 370 m_iconClasses = iconClasses; 371 m_reused = isReused; 372 } 373 374 /** 375 * Checks if the element is an overlay for a container page element.<p> 376 * 377 * @param element the element to check 378 * @return true if the element is an overlay 379 */ 380 public static boolean isOverlay(Element element) { 381 382 for (String overlayClass : Arrays.asList( 383 I_CmsLayoutBundle.INSTANCE.containerpageCss().expiredOverlay(), 384 I_CmsElementToolbarContext.ELEMENT_OPTION_BAR_CSS_CLASS)) { 385 if (element.hasClassName(overlayClass)) { 386 return true; 387 } 388 } 389 return false; 390 391 } 392 393 /** 394 * @see com.google.gwt.event.dom.client.HasClickHandlers#addClickHandler(com.google.gwt.event.dom.client.ClickHandler) 395 */ 396 public HandlerRegistration addClickHandler(ClickHandler handler) { 397 398 return addDomHandler(handler, ClickEvent.getType()); 399 } 400 401 /** 402 * @see org.opencms.acacia.client.I_CmsInlineFormParent#adoptWidget(com.google.gwt.user.client.ui.IsWidget) 403 */ 404 public void adoptWidget(IsWidget widget) { 405 406 getChildren().add(widget.asWidget()); 407 adopt(widget.asWidget()); 408 } 409 410 /** 411 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getCursorOffsetDelta() 412 */ 413 public Optional<int[]> getCursorOffsetDelta() { 414 415 return Optional.absent(); 416 } 417 418 /** 419 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getDragHelper(org.opencms.gwt.client.dnd.I_CmsDropTarget) 420 */ 421 public Element getDragHelper(I_CmsDropTarget target) { 422 423 CmsListInfoBean info = new CmsListInfoBean(m_title, m_subTitle, null); 424 info.setResourceType(m_resourceType); 425 info.setBigIconClasses(m_iconClasses); 426 CmsListItemWidget helperWidget = new CmsListItemWidget(info); 427 helperWidget.setWidth("600px"); 428 helperWidget.truncate("ggg", 550); 429 Element helper = helperWidget.getElement(); 430 Element button = DOM.createDiv(); 431 button.addClassName("opencms-icon"); 432 button.addClassName(I_CmsButton.MOVE_SMALL); 433 button.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragHandle()); 434 helper.appendChild(button); 435 helper.addClassName(org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.generalCss().shadow()); 436 Element parentElement = getElement().getParentElement(); 437 int elementTop = getElement().getAbsoluteTop(); 438 int parentTop = parentElement.getAbsoluteTop(); 439 m_provisionalParent = DOM.createElement(parentElement.getTagName()); 440 RootPanel.getBodyElement().appendChild(m_provisionalParent); 441 m_provisionalParent.addClassName( 442 org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.generalCss().clearStyles()); 443 m_provisionalParent.addClassName( 444 org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.generalCss().opencms()); 445 m_provisionalParent.getStyle().setWidth(parentElement.getOffsetWidth(), Unit.PX); 446 m_provisionalParent.appendChild(helper); 447 Style style = helper.getStyle(); 448 style.setWidth(helper.getOffsetWidth(), Unit.PX); 449 // the dragging class will set position absolute 450 helper.addClassName(org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.listItemWidgetCss().dragging()); 451 style.setTop(elementTop - parentTop, Unit.PX); 452 m_provisionalParent.getStyle().setPosition(Position.ABSOLUTE); 453 m_provisionalParent.getStyle().setTop(parentTop, Unit.PX); 454 m_provisionalParent.getStyle().setLeft(parentElement.getAbsoluteLeft(), Unit.PX); 455 m_provisionalParent.getStyle().setZIndex(I_CmsLayoutBundle.INSTANCE.constants().css().zIndexDND()); 456 457 return helper; 458 } 459 460 /** 461 * Returns the option bar of this element.<p> 462 * 463 * @return the option bar widget 464 */ 465 public CmsElementOptionBar getElementOptionBar() { 466 467 return m_elementOptionBar; 468 } 469 470 /** 471 * Returns the elements element view.<p> 472 * 473 * @return the element view 474 */ 475 public CmsUUID getElementView() { 476 477 return m_elementView; 478 } 479 480 /** 481 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getId() 482 */ 483 public String getId() { 484 485 return m_clientId; 486 } 487 488 /** 489 * Returns the inheritance info for this element.<p> 490 * 491 * @return the inheritance info for this element 492 */ 493 public CmsInheritanceInfo getInheritanceInfo() { 494 495 return m_inheritanceInfo; 496 } 497 498 /** 499 * Gets the lock information. 500 * 501 * @return the lock information 502 */ 503 public CmsElementLockInfo getLockInfo() { 504 505 return m_lockInfo != null ? m_lockInfo : new CmsElementLockInfo(null, false); 506 } 507 508 /** 509 * Returns the model group id.<p> 510 * 511 * @return the model group id 512 */ 513 public CmsUUID getModelGroupId() { 514 515 return m_modelGroupId; 516 } 517 518 /** 519 * Returns the new element type. 520 * 521 * @return the new element type 522 */ 523 public String getNewType() { 524 525 return m_newType; 526 } 527 528 /** 529 * Returns the no edit reason.<p> 530 * 531 * @return the no edit reason 532 */ 533 public String getNoEditReason() { 534 535 return m_noEditReason; 536 } 537 538 /** 539 * Gets the random id identifying this widget. 540 * 541 * <p>The id is also stored in the element_object_id property of the DOM element for this widget. 542 * 543 * @return the random id identifying this widget 544 */ 545 public String getObjectId() { 546 547 return m_objectId; 548 } 549 550 /** 551 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getParentTarget() 552 */ 553 public I_CmsDropContainer getParentTarget() { 554 555 return m_parent; 556 } 557 558 /** 559 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getPlaceholder(org.opencms.gwt.client.dnd.I_CmsDropTarget) 560 */ 561 public Element getPlaceholder(I_CmsDropTarget target) { 562 563 Element placeholder = CmsDomUtil.clone(getElement()); 564 placeholder.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragPlaceholder()); 565 return placeholder; 566 } 567 568 /** 569 * Returns if the element resource is currently released and not expired.<p> 570 * 571 * @return <code>true</code> if the element resource is currently released and not expired 572 */ 573 public boolean getReleasedAndNotExpired() { 574 575 return m_releasedAndNotExpired; 576 } 577 578 /** 579 * Returns the element resource type name.<p> 580 * 581 * @return the resource type name 582 */ 583 public String getResourceType() { 584 585 return m_resourceType; 586 } 587 588 /** 589 * Returns the site-path.<p> 590 * 591 * @return the site-path 592 */ 593 public String getSitePath() { 594 595 return m_sitePath; 596 } 597 598 /** 599 * Returns the structure id of the element.<p> 600 * 601 * @return the structure id of the element 602 */ 603 public CmsUUID getStructureId() { 604 605 if (m_clientId == null) { 606 return null; 607 } 608 return new CmsUUID(CmsContainerpageController.getServerId(m_clientId)); 609 } 610 611 /** 612 * Indicates an edit handler is configured for the element resource type.<p> 613 * 614 * @return indicates an edit handler is configured for the element resource type 615 */ 616 public boolean hasEditHandler() { 617 618 return m_hasEditHandler; 619 } 620 621 /** 622 * Returns whether this element has a model group parent.<p> 623 * 624 * @return <code>true</code> if this element has a model group parent 625 */ 626 public boolean hasModelGroupParent() { 627 628 boolean result = false; 629 Element parent = getElement().getParentElement(); 630 while (parent != null) { 631 if (parent.getPropertyBoolean(PROP_IS_MODEL_GROUP)) { 632 result = true; 633 break; 634 } 635 parent = parent.getParentElement(); 636 } 637 return result; 638 } 639 640 /** 641 * In case the inner HTML contains the reload marker.<p> 642 * 643 * @return <code>true</code> in case the inner HTML contains the reload marker 644 */ 645 public boolean hasReloadMarker() { 646 647 return getElement().getInnerHTML().contains(CmsGwtConstants.FORMATTER_RELOAD_MARKER); 648 } 649 650 /** 651 * Returns true if the element has settings to edit.<p> 652 * 653 * @return true if the element has settings to edit 654 */ 655 public boolean hasSettings() { 656 657 return m_hasSettings; 658 } 659 660 /** 661 * Returns if the current user has view permissions for the element resource.<p> 662 * 663 * @return <code>true</code> if the current user has view permissions for the element resource 664 */ 665 public boolean hasViewPermission() { 666 667 return m_viewPermission; 668 } 669 670 /** 671 * Returns if the user has write permission.<p> 672 * 673 * @return <code>true</code> if the user has write permission 674 */ 675 public boolean hasWritePermission() { 676 677 return m_writePermission; 678 } 679 680 /** 681 * Hides list collector direct edit buttons, if present.<p> 682 */ 683 public void hideEditableListButtons() { 684 685 if (m_editables != null) { 686 for (CmsListCollectorEditor editor : m_editables.values()) { 687 editor.getElement().getStyle().setDisplay(Display.NONE); 688 } 689 } 690 } 691 692 /** 693 * Puts a highlighting border around the element.<p> 694 */ 695 public void highlightElement() { 696 697 CmsPositionBean position = CmsPositionBean.getBoundingClientRect(getElement()); 698 if (m_highlighting == null) { 699 m_highlighting = new CmsHighlightingBorder( 700 position, 701 isNew() || (CmsContainerpageController.get().getData().isModelPage() && isCreateNew()) 702 ? CmsHighlightingBorder.BorderColor.blue 703 : CmsHighlightingBorder.BorderColor.red); 704 RootPanel.get().add(m_highlighting); 705 } else { 706 m_highlighting.setPosition(position); 707 } 708 } 709 710 /** 711 * Initializes the editor click handler.<p> 712 * 713 * @param controller the container page controller instance 714 */ 715 public void initInlineEditor(final CmsContainerpageController controller) { 716 717 if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_noEditReason) 718 && !m_disableNewEditor 719 && CmsContentEditor.setEditable(getElement(), CmsContainerpageController.getServerId(m_clientId), true)) { 720 if (m_editorClickHandlerRegistration != null) { 721 m_editorClickHandlerRegistration.removeHandler(); 722 } 723 m_editorClickHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() { 724 725 public void onPreviewNativeEvent(NativePreviewEvent event) { 726 727 if (event.getTypeInt() == Event.ONCLICK) { 728 // if another content is already being edited, don't start another editor 729 if (controller.isContentEditing()) { 730 return; 731 } 732 Element eventTarget = event.getNativeEvent().getEventTarget().cast(); 733 // check if the event target is a child 734 if (getElement().isOrHasChild(eventTarget)) { 735 Element target = event.getNativeEvent().getEventTarget().cast(); 736 // check if the target closest ancestor drag element is this element 737 Element parentContainerElement = CmsDomUtil.getAncestor( 738 target, 739 I_CmsLayoutBundle.INSTANCE.dragdropCss().dragElement()); 740 if (parentContainerElement == getElement()) { 741 while ((target != null) 742 && !target.getTagName().equalsIgnoreCase("a") 743 && (target != getElement())) { 744 if (CmsContentEditor.isEditable(target)) { 745 final Element finalTarget = target; 746 event.cancel(); 747 CmsContainerpageController.get().checkReuse( 748 CmsContainerPageElementPanel.this, 749 () -> { 750 CmsEditorBase.markForInlineFocus(finalTarget); 751 controller.getHandler().openEditorForElement( 752 CmsContainerPageElementPanel.this, 753 true, 754 isNew()); 755 removeEditorHandler(); 756 }); 757 break; 758 } else { 759 target = target.getParentElement(); 760 } 761 } 762 } 763 } 764 } 765 } 766 }); 767 } 768 } 769 770 /** 771 * Checks if this element has 'createNew' status, i.e. will be copied when using this page as a model for a new container page.<p> 772 * 773 * @return true if this element has createNew status 774 */ 775 public boolean isCreateNew() { 776 777 return m_createNew; 778 } 779 780 /** 781 * Returns whether the element is used as a model group.<p> 782 * 783 * @return <code>true</code> if the element is used as a model group 784 */ 785 public boolean isModelGroup() { 786 787 return m_modelGroupId != null; 788 } 789 790 /** 791 * Returns if this is e newly created element.<p> 792 * 793 * @return <code>true</code> if the element is new 794 */ 795 public boolean isNew() { 796 797 return m_newType != null; 798 } 799 800 /** 801 * Returns true if the new content editor is disabled for this element.<p> 802 * 803 * @return true if the new editor is disabled for this element 804 */ 805 public boolean isNewEditorDisabled() { 806 807 return m_disableNewEditor; 808 } 809 810 /** 811 * Checks if this element is marked as reused. 812 * 813 * @return true if the element is marked as reused 814 */ 815 public boolean isReused() { 816 817 return m_reused; 818 } 819 820 /** 821 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDragCancel() 822 */ 823 public void onDragCancel() { 824 825 clearDrag(); 826 resetOptionbar(); 827 } 828 829 /** 830 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDrop(org.opencms.gwt.client.dnd.I_CmsDropTarget) 831 */ 832 public void onDrop(I_CmsDropTarget target) { 833 834 clearDrag(); 835 } 836 837 /** 838 * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onStartDrag(org.opencms.gwt.client.dnd.I_CmsDropTarget) 839 */ 840 public void onStartDrag(I_CmsDropTarget target) { 841 842 CmsDomUtil.addDisablingOverlay(getElement()); 843 getElement().getStyle().setOpacity(0.7); 844 removeHighlighting(); 845 } 846 847 /** 848 * @see com.google.gwt.user.client.ui.Widget#removeFromParent() 849 */ 850 @Override 851 public void removeFromParent() { 852 853 if (m_highlighting != null) { 854 m_highlighting.removeFromParent(); 855 m_highlighting = null; 856 } 857 super.removeFromParent(); 858 } 859 860 /** 861 * Removes the highlighting border.<p> 862 */ 863 public void removeHighlighting() { 864 865 if (m_highlighting != null) { 866 m_highlighting.removeFromParent(); 867 m_highlighting = null; 868 } 869 } 870 871 /** 872 * Removes the inline editor.<p> 873 */ 874 public void removeInlineEditor() { 875 876 CmsContentEditor.setEditable(getElement(), CmsContainerpageController.getServerId(m_clientId), false); 877 removeEditorHandler(); 878 } 879 880 /** 881 * @see org.opencms.acacia.client.I_CmsInlineFormParent#replaceHtml(java.lang.String) 882 */ 883 public void replaceHtml(String html) { 884 885 // detach all children first 886 while (getChildren().size() > 0) { 887 getChildren().get(getChildren().size() - 1).removeFromParent(); 888 } 889 Element tempDiv = DOM.createDiv(); 890 tempDiv.setInnerHTML(html); 891 getElement().setInnerHTML(tempDiv.getFirstChildElement().getInnerHTML()); 892 } 893 894 /** 895 * Sets the 'create new' status of the element.<p> 896 * 897 * @param createNew the new value for the 'create new' status 898 */ 899 public void setCreateNew(boolean createNew) { 900 901 m_createNew = createNew; 902 } 903 904 /** 905 * Sets the element option bar.<p> 906 * 907 * @param elementOptionBar the element option bar to set 908 */ 909 public void setElementOptionBar(CmsElementOptionBar elementOptionBar) { 910 911 if ((m_elementOptionBar != null) && (getWidgetIndex(m_elementOptionBar) >= 0)) { 912 m_elementOptionBar.removeFromParent(); 913 } 914 m_elementOptionBar = elementOptionBar; 915 if (m_elementOptionBar != null) { 916 getElement().addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragElement()); 917 insert(m_elementOptionBar, 0); 918 updateOptionBarPosition(); 919 } else { 920 getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragElement()); 921 } 922 } 923 924 /** 925 * Sets the element id.<p> 926 * 927 * @param id the id 928 */ 929 public void setId(String id) { 930 931 m_clientId = id; 932 } 933 934 /** 935 * Sets the inheritance info for this element.<p> 936 * 937 * @param inheritanceInfo the inheritance info for this element to set 938 */ 939 public void setInheritanceInfo(CmsInheritanceInfo inheritanceInfo) { 940 941 m_inheritanceInfo = inheritanceInfo; 942 } 943 944 /** 945 * Sets the new-type of the element.<p> 946 * 947 * @param newType the new-type 948 */ 949 public void setNewType(String newType) { 950 951 m_newType = newType; 952 } 953 954 /** 955 * Sets the no edit reason.<p> 956 * 957 * @param noEditReason the no edit reason to set 958 */ 959 public void setNoEditReason(String noEditReason) { 960 961 m_noEditReason = noEditReason; 962 } 963 964 /** 965 * Sets if the element resource is currently released and not expired.<p> 966 * 967 * @param releasedAndNotExpired <code>true</code> if the element resource is currently released and not expired 968 */ 969 public void setReleasedAndNotExpired(boolean releasedAndNotExpired) { 970 971 m_releasedAndNotExpired = releasedAndNotExpired; 972 if (m_releasedAndNotExpired) { 973 removeStyleName(I_CmsLayoutBundle.INSTANCE.containerpageCss().expired()); 974 if (m_expiredOverlay != null) { 975 m_expiredOverlay.removeFromParent(); 976 m_expiredOverlay = null; 977 } 978 979 } else { 980 addStyleName(I_CmsLayoutBundle.INSTANCE.containerpageCss().expired()); 981 m_expiredOverlay = DOM.createDiv(); 982 m_expiredOverlay.setTitle("Expired resource"); 983 m_expiredOverlay.addClassName(I_CmsLayoutBundle.INSTANCE.containerpageCss().expiredOverlay()); 984 getElement().appendChild(m_expiredOverlay); 985 } 986 } 987 988 /** 989 * Sets the site path.<p> 990 * 991 * @param sitePath the site path to set 992 */ 993 public void setSitePath(String sitePath) { 994 995 m_sitePath = sitePath; 996 } 997 998 /** 999 * Sets if the current user has view permissions for the element resource.<p> 1000 * 1001 * @param viewPermission the view permission to set 1002 */ 1003 public void setViewPermission(boolean viewPermission) { 1004 1005 m_viewPermission = viewPermission; 1006 } 1007 1008 /** 1009 * Sets the user write permission.<p> 1010 * 1011 * @param writePermission the user write permission to set 1012 */ 1013 public void setWritePermission(boolean writePermission) { 1014 1015 m_writePermission = writePermission; 1016 } 1017 1018 /** 1019 * Shows list collector direct edit buttons (old direct edit style), if present.<p> 1020 */ 1021 public void showEditableListButtons() { 1022 1023 m_checkingEditables = true; 1024 if (m_editables == null) { 1025 m_editables = new HashMap<Element, CmsListCollectorEditor>(); 1026 List<Element> editables = getEditableElements(); 1027 if ((editables != null) && (editables.size() > 0)) { 1028 for (Element editable : editables) { 1029 addListCollectorEditorButtons(editable); 1030 } 1031 } 1032 } else { 1033 1034 Iterator<Entry<Element, CmsListCollectorEditor>> it = m_editables.entrySet().iterator(); 1035 while (it.hasNext()) { 1036 Entry<Element, CmsListCollectorEditor> entry = it.next(); 1037 CmsListCollectorEditor editor = entry.getValue(); 1038 if (!editor.isValid()) { 1039 editor.removeFromParent(); 1040 it.remove(); 1041 } else { 1042 if (CmsDomUtil.hasDimension(editor.getElement().getParentElement())) { 1043 editor.setParentHasDimensions(true); 1044 editor.setPosition( 1045 CmsDomUtil.getEditablePosition(entry.getValue().getMarkerTag()), 1046 getElement()); 1047 } else { 1048 editor.setParentHasDimensions(false); 1049 } 1050 } 1051 } 1052 List<Element> editables = getEditableElements(); 1053 if (editables.size() > m_editables.size()) { 1054 for (Element editable : editables) { 1055 if (!m_editables.containsKey(editable)) { 1056 addListCollectorEditorButtons(editable); 1057 } 1058 } 1059 } 1060 } 1061 1062 boolean editableContainer = true; 1063 if (m_parent instanceof CmsContainerPageContainer) { 1064 editableContainer = CmsContainerpageController.get().isContainerEditable(m_parent); 1065 } 1066 for (CmsListCollectorEditor editor : m_editables.values()) { 1067 editor.updateVisibility(editableContainer); 1068 } 1069 1070 m_checkingEditables = false; 1071 initMutationObserver(); 1072 } 1073 1074 /** 1075 * Updates the option bar position.<p> 1076 */ 1077 public void updateOptionBarPosition() { 1078 1079 // only if attached to the DOM 1080 if ((m_elementOptionBar != null) && RootPanel.getBodyElement().isOrHasChild(getElement())) { 1081 int absoluteTop = getElement().getAbsoluteTop(); 1082 int absoluteRight = getElement().getAbsoluteRight(); 1083 CmsPositionBean dimensions = CmsPositionBean.getBoundingClientRect(getElement()); 1084 1085 int top = 0; 1086 int right = 0; 1087 int offsetLeft = 0; 1088 int offsetTop = 0; 1089 1090 final Style style = m_elementOptionBar.getElement().getStyle(); 1091 1092 if (m_positioningInstructionParser.tryParse(getElement().getClassName())) { 1093 offsetLeft = m_positioningInstructionParser.getOffsetLeft(); 1094 offsetTop = m_positioningInstructionParser.getOffsetTop(); 1095 } 1096 1097 if (Math.abs(absoluteTop - dimensions.getTop()) > 20) { 1098 absoluteTop = (dimensions.getTop() - absoluteTop) + 2; 1099 top = absoluteTop; 1100 } 1101 if (Math.abs(absoluteRight - dimensions.getLeft() - dimensions.getWidth()) > 20) { 1102 absoluteRight = (absoluteRight - dimensions.getLeft() - dimensions.getWidth()) + 2; 1103 right = absoluteRight; 1104 } 1105 1106 top += offsetTop; 1107 right -= offsetLeft; 1108 1109 if (top != 0) { 1110 style.setTop(top, Unit.PX); 1111 } else { 1112 style.clearTop(); 1113 } 1114 1115 if (right != 0) { 1116 style.setRight(right, Unit.PX); 1117 } else { 1118 style.clearRight(); 1119 } 1120 1121 if (isOptionbarIFrameCollision(absoluteTop, m_elementOptionBar.getCalculatedWidth())) { 1122 style.setPosition(Position.RELATIVE); 1123 int marginLeft = getElement().getClientWidth() - m_elementOptionBar.getCalculatedWidth(); 1124 if (marginLeft > 0) { 1125 style.setMarginLeft(marginLeft, Unit.PX); 1126 } 1127 } else { 1128 style.clearPosition(); 1129 style.clearMarginLeft(); 1130 } 1131 } 1132 } 1133 1134 /** 1135 * Checks for changes in the list collector direct edit content.<p> 1136 */ 1137 protected void checkForEditableChanges() { 1138 1139 if (!m_checkingEditables) { 1140 m_checkingEditables = true; 1141 Timer timer = new Timer() { 1142 1143 @Override 1144 public void run() { 1145 1146 showEditableListButtons(); 1147 } 1148 }; 1149 timer.schedule(500); 1150 } 1151 } 1152 1153 /** 1154 * Gets the editable list elements.<p> 1155 * 1156 * @return the editable list elements 1157 */ 1158 protected List<Element> getEditableElements() { 1159 1160 List<Element> elems = CmsDomUtil.getElementsByClass(CmsGwtConstants.CLASS_EDITABLE, Tag.ALL, getElement()); 1161 List<Element> result = Lists.newArrayList(); 1162 for (Element currentElem : elems) { 1163 // don't return elements which are contained in nested containers 1164 if (m_parent.getContainerId().equals(getParentContainerId(currentElem))) { 1165 result.add(currentElem); 1166 } 1167 } 1168 return result; 1169 } 1170 1171 /** 1172 * Returns if the list collector direct edit content has changed.<p> 1173 * 1174 * @return <code>true</code> if the list collector direct edit content has changed 1175 */ 1176 protected boolean hasChangedEditables() { 1177 1178 if (m_editables == null) { 1179 return true; 1180 } 1181 for (CmsListCollectorEditor editor : m_editables.values()) { 1182 if (!editor.isValid()) { 1183 return true; 1184 } 1185 } 1186 return getEditableElements().size() > m_editables.size(); 1187 } 1188 1189 /** 1190 * @see com.google.gwt.user.client.ui.Widget#onDetach() 1191 */ 1192 @Override 1193 protected void onDetach() { 1194 1195 super.onDetach(); 1196 removeEditorHandler(); 1197 } 1198 1199 /** 1200 * @see com.google.gwt.user.client.ui.Widget#onLoad() 1201 */ 1202 @Override 1203 protected void onLoad() { 1204 1205 if ((getParentTarget() instanceof CmsContainerPageContainer) 1206 && ((CmsContainerPageContainer)getParentTarget()).isEditable() 1207 && !hasCheckedHeight()) { 1208 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 1209 1210 public void execute() { 1211 1212 CmsContainerPageElementPanel thisElement = CmsContainerPageElementPanel.this; 1213 if (!hasCheckedHeight() && CmsSmallElementsHandler.isSmall(thisElement)) { 1214 CmsContainerpageController.get().getSmallElementsHandler().prepareSmallElement(thisElement); 1215 } 1216 setCheckedHeight(true); 1217 } 1218 }); 1219 } 1220 resetOptionbar(); 1221 } 1222 1223 /** 1224 * Removes the inline editor handler.<p> 1225 */ 1226 protected void removeEditorHandler() { 1227 1228 if (m_editorClickHandlerRegistration != null) { 1229 m_editorClickHandlerRegistration.removeHandler(); 1230 m_editorClickHandlerRegistration = null; 1231 } 1232 } 1233 1234 /** 1235 * Returns if the minimum element height has been checked.<p> 1236 * 1237 * @return <code>true</code> if the minimum element height has been checked 1238 */ 1239 boolean hasCheckedHeight() { 1240 1241 return m_checkedHeight; 1242 } 1243 1244 /** 1245 * Sets the checked height flag.<p> 1246 * 1247 * @param checked the checked height flag 1248 */ 1249 void setCheckedHeight(boolean checked) { 1250 1251 m_checkedHeight = checked; 1252 } 1253 1254 /** 1255 * Adds the collector edit buttons.<p> 1256 * 1257 * @param editable the marker element for an editable list element 1258 */ 1259 private void addListCollectorEditorButtons(Element editable) { 1260 1261 CmsListCollectorEditor editor = new CmsListCollectorEditor(editable, m_clientId); 1262 add(editor, editable.getParentElement()); 1263 if (CmsDomUtil.hasDimension(editable.getParentElement())) { 1264 editor.setParentHasDimensions(true); 1265 editor.setPosition(CmsDomUtil.getEditablePosition(editable), getElement()); 1266 } else { 1267 editor.setParentHasDimensions(false); 1268 } 1269 m_editables.put(editable, editor); 1270 } 1271 1272 /** 1273 * Removes all styling done during drag and drop.<p> 1274 */ 1275 private void clearDrag() { 1276 1277 CmsDomUtil.removeDisablingOverlay(getElement()); 1278 m_elementOptionBar.getElement().removeClassName( 1279 org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.stateCss().cmsHovering()); 1280 getElement().getStyle().clearOpacity(); 1281 getElement().getStyle().clearDisplay(); 1282 updateOptionBarPosition(); 1283 if (m_provisionalParent != null) { 1284 m_provisionalParent.removeFromParent(); 1285 m_provisionalParent = null; 1286 } 1287 } 1288 1289 /** 1290 * Gets the container id of the most deeply nested container containing the given element, or null if no such container can be found.<p> 1291 * 1292 * @param elem the element 1293 * @return the container id of the deepest container containing the element 1294 */ 1295 private String getParentContainerId(Element elem) { 1296 1297 String attr = CmsContainerPageContainer.PROP_CONTAINER_MARKER; 1298 Element lastElem; 1299 do { 1300 String propValue = elem.getPropertyString(attr); 1301 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(propValue)) { 1302 return propValue; 1303 } 1304 lastElem = elem; 1305 elem = elem.getParentElement(); 1306 } while ((elem != null) && (elem != lastElem)); 1307 return null; 1308 } 1309 1310 /** 1311 * Initializes the mutation observer used for updating the edit buttons after DOM changes (e.g. pagination). 1312 */ 1313 private void initMutationObserver() { 1314 1315 if (m_mutationObserver == null) { 1316 MutationObserverCallbackFn callback = (JsArray<MutationRecord> records, MutationObserver obs) -> { 1317 checkForEditableChanges(); 1318 return null; 1319 }; 1320 m_mutationObserver = new MutationObserver(callback); 1321 MutationObserverInit options = Js.cast(new JsObject()); 1322 options.setSubtree(true); 1323 options.setChildList(true); 1324 m_mutationObserver.observe(Js.cast(getElement()), options); 1325 } 1326 } 1327 1328 /** 1329 * Returns if the option bar position collides with any iframe child elements.<p> 1330 * 1331 * @param optionTop the option bar absolute top 1332 * @param optionWidth the option bar width 1333 * 1334 * @return <code>true</code> if there are iframe child elements located no less than 25px below the upper edge of the element 1335 */ 1336 private boolean isOptionbarIFrameCollision(int optionTop, int optionWidth) { 1337 1338 if (RootPanel.getBodyElement().isOrHasChild(getElement())) { 1339 NodeList<Element> frames = getElement().getElementsByTagName(CmsDomUtil.Tag.iframe.name()); 1340 for (int i = 0; i < frames.getLength(); i++) { 1341 int frameTop = frames.getItem(i).getAbsoluteTop(); 1342 int frameHeight = frames.getItem(i).getOffsetHeight(); 1343 int frameRight = frames.getItem(i).getAbsoluteRight(); 1344 if (((frameTop - optionTop) < 25) 1345 && (((frameTop + frameHeight) - optionTop) > 0) 1346 && ((frameRight - getElement().getAbsoluteRight()) < optionWidth)) { 1347 return true; 1348 } 1349 1350 } 1351 } 1352 return false; 1353 } 1354 1355 /** 1356 * This method removes the option-bar widget from DOM and re-attaches it at it's original position.<p> 1357 * Use to avoid mouse-over and mouse-down malfunction.<p> 1358 */ 1359 private void resetOptionbar() { 1360 1361 if (m_elementOptionBar != null) { 1362 if (getWidgetIndex(m_elementOptionBar) >= 0) { 1363 m_elementOptionBar.removeFromParent(); 1364 } 1365 updateOptionBarPosition(); 1366 insert(m_elementOptionBar, 0); 1367 } 1368 } 1369}