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