001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (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.acacia.client.ui; 029 030import org.opencms.ade.containerpage.client.ui.css.I_CmsLayoutBundle; 031import org.opencms.gwt.client.util.CmsClientStringUtil; 032import org.opencms.gwt.client.util.CmsDomUtil; 033import org.opencms.gwt.client.util.CmsPositionBean; 034 035import java.util.ArrayList; 036import java.util.HashMap; 037import java.util.List; 038import java.util.Map; 039 040import com.google.gwt.core.client.GWT; 041import com.google.gwt.dom.client.Element; 042import com.google.gwt.dom.client.Style; 043import com.google.gwt.dom.client.Style.Display; 044import com.google.gwt.dom.client.Style.Unit; 045import com.google.gwt.event.dom.client.ClickEvent; 046import com.google.gwt.event.dom.client.ClickHandler; 047import com.google.gwt.event.dom.client.HasClickHandlers; 048import com.google.gwt.event.shared.HandlerRegistration; 049import com.google.gwt.uibinder.client.UiBinder; 050import com.google.gwt.uibinder.client.UiField; 051import com.google.gwt.user.client.Window; 052import com.google.gwt.user.client.ui.Composite; 053import com.google.gwt.user.client.ui.FlowPanel; 054import com.google.gwt.user.client.ui.HTMLPanel; 055import com.google.gwt.user.client.ui.RootPanel; 056import com.google.gwt.user.client.ui.Widget; 057 058import jsinterop.base.Js; 059 060/** 061 * In-line edit overlay covering rest of the page.<p> 062 */ 063public class CmsInlineEditOverlay extends Composite implements HasClickHandlers { 064 065 /** The ui binder. */ 066 interface I_CmsInlineEditOverlayUiBinder extends UiBinder<HTMLPanel, CmsInlineEditOverlay> { 067 // nothing to do 068 } 069 070 /** The width rquired by the button bar. */ 071 private static final int BUTTON_BAR_WIDTH = 28; 072 073 /** List of present overlays. */ 074 private static List<CmsInlineEditOverlay> m_overlays = new ArrayList<CmsInlineEditOverlay>(); 075 076 /** The ui binder instance. */ 077 private static I_CmsInlineEditOverlayUiBinder uiBinder = GWT.create(I_CmsInlineEditOverlayUiBinder.class); 078 079 /** Bottom border. */ 080 @UiField 081 protected Element m_borderBottom; 082 083 /** Left border. */ 084 @UiField 085 protected Element m_borderLeft; 086 087 /** Right border. */ 088 @UiField 089 protected Element m_borderRight; 090 091 /** Top border. */ 092 @UiField 093 protected Element m_borderTop; 094 095 /** The button bar element. */ 096 @UiField 097 protected Element m_buttonBar; 098 099 /** Edit overlay. */ 100 @UiField 101 protected Element m_overlayBottom; 102 103 /** Edit overlay. */ 104 @UiField 105 protected Element m_overlayLeft; 106 107 /** Edit overlay. */ 108 @UiField 109 protected Element m_overlayRight; 110 111 /** Edit overlay. */ 112 @UiField 113 protected Element m_overlayTop; 114 115 /** The edit button panel. */ 116 @UiField 117 FlowPanel m_buttonPanel; 118 119 /** Style of border. */ 120 private Style m_borderBottomStyle; 121 122 /** Style of border. */ 123 private Style m_borderLeftStyle; 124 125 /** Style of border. */ 126 private Style m_borderRightStyle; 127 128 /** Style of border. */ 129 private Style m_borderTopStyle; 130 131 /** Map of attached edit buttons and their absolute top positions. */ 132 private Map<CmsInlineEntityWidget, Integer> m_buttons; 133 134 /** The current overlay position. */ 135 private CmsPositionBean m_currentPosition; 136 137 /** The element to surround with the overlay. */ 138 private Element m_element; 139 140 /** Flag indicating this overlay has a button bar. */ 141 private boolean m_hasButtonBar; 142 143 /** The main panel. */ 144 private HTMLPanel m_main; 145 146 /** The overlay offset. */ 147 private int m_offset = 3; 148 149 /** Style of overlay. */ 150 private Style m_overlayBottomStyle; 151 152 /** Style of overlay. */ 153 private Style m_overlayLeftStyle; 154 155 /** Style of overlay. */ 156 private Style m_overlayRightStyle; 157 158 /** Style of overlay. */ 159 private Style m_overlayTopStyle; 160 161 /** Elements marked as non inline editable, for which an overlay is displayed. */ 162 private List<elemental2.dom.Element> m_disabledElements = new ArrayList<>(); 163 164 /** 165 * Constructor.<p> 166 * 167 * @param element the element to surround with the overlay 168 */ 169 public CmsInlineEditOverlay(Element element) { 170 171 m_main = uiBinder.createAndBindUi(this); 172 initWidget(m_main); 173 m_element = element; 174 m_overlayLeftStyle = m_overlayLeft.getStyle(); 175 m_overlayBottomStyle = m_overlayBottom.getStyle(); 176 m_overlayRightStyle = m_overlayRight.getStyle(); 177 m_overlayTopStyle = m_overlayTop.getStyle(); 178 m_borderBottomStyle = m_borderBottom.getStyle(); 179 m_borderLeftStyle = m_borderLeft.getStyle(); 180 m_borderRightStyle = m_borderRight.getStyle(); 181 m_borderTopStyle = m_borderTop.getStyle(); 182 m_buttonBar.getStyle().setDisplay(Display.NONE); 183 m_buttonPanel.addDomHandler(new ClickHandler() { 184 185 public void onClick(ClickEvent event) { 186 187 // prevent the click event to propagated from the button panel to the main widget 188 event.stopPropagation(); 189 } 190 }, ClickEvent.getType()); 191 m_buttons = new HashMap<CmsInlineEntityWidget, Integer>(); 192 } 193 194 /** 195 * Adds an overlay surrounding the given DOM element.<p> 196 * 197 * @param element the element 198 * 199 * @return the overlay widget 200 */ 201 public static CmsInlineEditOverlay addOverlayForElement(Element element) { 202 203 CmsInlineEditOverlay overlay = new CmsInlineEditOverlay(element); 204 if (!m_overlays.isEmpty()) { 205 m_overlays.get(m_overlays.size() - 1).setVisible(false); 206 } 207 m_overlays.add(overlay); 208 RootPanel.get().add(overlay); 209 overlay.updatePosition(); 210 overlay.checkZIndex(); 211 return overlay; 212 } 213 214 /** 215 * Returns the root overlay if available.<p> 216 * 217 * @return the root overlay 218 */ 219 public static CmsInlineEditOverlay getRootOverlay() { 220 221 return m_overlays.isEmpty() ? null : m_overlays.get(0); 222 } 223 224 /** 225 * Removes all present overlays.<p> 226 */ 227 public static void removeAll() { 228 229 for (CmsInlineEditOverlay overlay : m_overlays) { 230 overlay.removeFromParent(); 231 } 232 m_overlays.clear(); 233 } 234 235 /** 236 * Removes the last overlay to display the previous or none.<p> 237 */ 238 public static void removeLastOverlay() { 239 240 if (!m_overlays.isEmpty()) { 241 CmsInlineEditOverlay last = m_overlays.remove(m_overlays.size() - 1); 242 last.removeFromParent(); 243 } 244 if (!m_overlays.isEmpty()) { 245 m_overlays.get(m_overlays.size() - 1).setVisible(true); 246 } 247 } 248 249 /** 250 * Updates the current overlay's position.<p> 251 */ 252 public static void updateCurrentOverlayPosition() { 253 254 if (!m_overlays.isEmpty()) { 255 m_overlays.get(m_overlays.size() - 1).updatePosition(); 256 } 257 } 258 259 /** 260 * Adds a button widget to the button panel.<p> 261 * 262 * @param widget the button widget 263 * @param absoluteTop the absolute top position 264 */ 265 public void addButton(CmsInlineEntityWidget widget, int absoluteTop) { 266 267 setButtonBarVisible(true); 268 m_buttonPanel.add(widget); 269 setButtonPosition(widget, absoluteTop); 270 } 271 272 /** 273 * @see com.google.gwt.event.dom.client.HasClickHandlers#addClickHandler(com.google.gwt.event.dom.client.ClickHandler) 274 */ 275 public HandlerRegistration addClickHandler(ClickHandler handler) { 276 277 return addDomHandler(handler, ClickEvent.getType()); 278 } 279 280 /** 281 * Increases the overlay z-index if necessary.<p> 282 */ 283 public void checkZIndex() { 284 285 int zIndex = 100000; 286 Element parent = m_element.getParentElement(); 287 while (parent != null) { 288 int parentIndex = CmsDomUtil.getCurrentStyleInt(parent, CmsDomUtil.Style.zIndex); 289 if (parentIndex > zIndex) { 290 zIndex = parentIndex; 291 } 292 parent = parent.getParentElement(); 293 } 294 if (zIndex > 100000) { 295 getElement().getStyle().setZIndex(zIndex); 296 } 297 } 298 299 /** 300 * Clears and hides the button panel.<p> 301 */ 302 public void clearButtonPanel() { 303 304 m_buttonPanel.clear(); 305 m_buttons.clear(); 306 setButtonBarVisible(false); 307 } 308 309 /** 310 * Initializes the overlay for 'disabled' (not inline editable) elements. 311 */ 312 public void initDisabled() { 313 314 clearDisabled(); 315 elemental2.dom.Element elem = Js.cast(m_element); 316 List<elemental2.dom.Element> inactive = elem.querySelectorAll( 317 ".oc-container, .oc-not-inline-editable").asList(); 318 for (elemental2.dom.Element candidate : inactive) { 319 boolean isRoot = true; 320 for (elemental2.dom.Element other : inactive) { 321 if ((other != candidate) && other.contains(candidate)) { 322 isRoot = false; 323 break; 324 } 325 } 326 if (isRoot) { 327 m_disabledElements.add(candidate); 328 candidate.classList.add(I_CmsLayoutBundle.INSTANCE.containerpageCss().inlineEditDisabled()); 329 } 330 } 331 } 332 333 /** 334 * Updates the position of the given button widget.<p> 335 * 336 * @param widget the button widget 337 * @param absoluteTop the top absolute top position 338 */ 339 public void setButtonPosition(CmsInlineEntityWidget widget, int absoluteTop) { 340 341 if (m_buttonPanel.getWidgetIndex(widget) > -1) { 342 int buttonBarTop = CmsClientStringUtil.parseInt(m_buttonBar.getStyle().getTop()); 343 if (absoluteTop < buttonBarTop) { 344 absoluteTop = buttonBarTop; 345 } 346 int positionTop = getAvailablePosition(widget, absoluteTop) - buttonBarTop; 347 widget.getElement().getStyle().setTop(positionTop, Unit.PX); 348 if (CmsClientStringUtil.parseInt(m_buttonBar.getStyle().getHeight()) < (positionTop + 20)) { 349 increaseOverlayHeight(positionTop + 20); 350 } 351 } 352 } 353 354 /** 355 * Sets the overlay offset.<p> 356 * 357 * @param offset the offset 358 */ 359 public void setOffset(int offset) { 360 361 m_offset = offset; 362 } 363 364 /** 365 * @see com.google.gwt.user.client.ui.UIObject#setVisible(boolean) 366 */ 367 @Override 368 public void setVisible(boolean visible) { 369 370 super.setVisible(visible); 371 if (!visible && m_hasButtonBar) { 372 for (Widget widget : m_buttonPanel) { 373 if (widget instanceof CmsInlineEntityWidget) { 374 ((CmsInlineEntityWidget)widget).setContentHighlightingVisible(false); 375 } 376 } 377 } 378 } 379 380 /** 381 * Updates the overlay position.<p> 382 */ 383 public void updatePosition() { 384 385 setPosition(CmsPositionBean.getBoundingClientRect(m_element)); 386 for (Widget widget : m_buttonPanel) { 387 if (widget instanceof CmsInlineEntityWidget) { 388 ((CmsInlineEntityWidget)widget).positionWidget(); 389 } 390 } 391 } 392 393 /** 394 * @see com.google.gwt.user.client.ui.Composite#onDetach() 395 */ 396 @Override 397 protected void onDetach() { 398 399 super.onDetach(); 400 clearDisabled(); 401 402 } 403 404 /** 405 * Clears the overlay for non inline editable elements. 406 */ 407 private void clearDisabled() { 408 409 m_disabledElements.forEach( 410 elem2 -> elem2.classList.remove(I_CmsLayoutBundle.INSTANCE.containerpageCss().inlineEditDisabled())); 411 m_disabledElements.clear(); 412 } 413 414 /** 415 * Returns the available absolute top position for the given button.<p> 416 * 417 * @param widget the button widget 418 * @param absoluteTop the proposed position 419 * 420 * @return the available position 421 */ 422 private int getAvailablePosition(CmsInlineEntityWidget widget, int absoluteTop) { 423 424 m_buttons.remove(widget); 425 boolean positionBlocked = true; 426 while (positionBlocked) { 427 positionBlocked = false; 428 for (int pos : m_buttons.values()) { 429 if (((pos - 24) < absoluteTop) && (absoluteTop < (pos + 24))) { 430 positionBlocked = true; 431 absoluteTop = pos + 25; 432 break; 433 } 434 } 435 } 436 m_buttons.put(widget, Integer.valueOf(absoluteTop)); 437 return absoluteTop; 438 } 439 440 /** 441 * Increases the overlay height to make space for edit buttons.<p> 442 * 443 * @param height the height to set 444 */ 445 private void increaseOverlayHeight(int height) { 446 447 if (m_currentPosition != null) { 448 m_currentPosition.setHeight(height); 449 setPosition(m_currentPosition); 450 } 451 } 452 453 /** 454 * Sets button bar visibility.<p> 455 * 456 * @param visible <code>true</code> to set the button bar visible 457 */ 458 private void setButtonBarVisible(boolean visible) { 459 460 if (m_hasButtonBar != visible) { 461 m_hasButtonBar = visible; 462 if (m_hasButtonBar) { 463 464 m_buttonBar.getStyle().clearDisplay(); 465 int width = CmsClientStringUtil.parseInt(m_borderTopStyle.getWidth()) + BUTTON_BAR_WIDTH; 466 m_borderTopStyle.setWidth(width, Unit.PX); 467 m_borderBottomStyle.setWidth(width, Unit.PX); 468 m_borderRightStyle.setLeft( 469 CmsClientStringUtil.parseInt(m_borderRightStyle.getLeft()) + BUTTON_BAR_WIDTH, 470 Unit.PX); 471 } else { 472 m_buttonBar.getStyle().setDisplay(Display.NONE); 473 int width = CmsClientStringUtil.parseInt(m_borderTopStyle.getWidth()) - BUTTON_BAR_WIDTH; 474 m_borderTopStyle.setWidth(width, Unit.PX); 475 m_borderBottomStyle.setWidth(width, Unit.PX); 476 m_borderRightStyle.setLeft( 477 CmsClientStringUtil.parseInt(m_borderRightStyle.getLeft()) - BUTTON_BAR_WIDTH, 478 Unit.PX); 479 } 480 } 481 } 482 483 /** 484 * Sets position and size of the overlay area.<p> 485 * 486 * @param position the position of highlighted area 487 */ 488 private void setPosition(CmsPositionBean position) { 489 490 m_currentPosition = position; 491 setSelectPosition(position.getLeft(), position.getTop(), position.getHeight(), position.getWidth()); 492 } 493 494 /** 495 * Sets position and size of the overlay area.<p> 496 * 497 * @param posX the new X position 498 * @param posY the new Y position 499 * @param height the new height 500 * @param width the new width 501 */ 502 private void setSelectPosition(int posX, int posY, int height, int width) { 503 504 int useWidth = Window.getClientWidth(); 505 int bodyWidth = RootPanel.getBodyElement().getClientWidth() + RootPanel.getBodyElement().getOffsetLeft(); 506 if (bodyWidth > useWidth) { 507 useWidth = bodyWidth; 508 } 509 int useHeight = Window.getClientHeight(); 510 int bodyHeight = RootPanel.getBodyElement().getClientHeight() + RootPanel.getBodyElement().getOffsetTop(); 511 if (bodyHeight > useHeight) { 512 useHeight = bodyHeight; 513 } 514 515 m_overlayLeftStyle.setWidth(posX - m_offset, Unit.PX); 516 m_overlayLeftStyle.setHeight(useHeight, Unit.PX); 517 518 m_borderLeftStyle.setHeight(height + (4 * m_offset), Unit.PX); 519 m_borderLeftStyle.setTop(posY - (2 * m_offset), Unit.PX); 520 m_borderLeftStyle.setLeft(posX - (2 * m_offset), Unit.PX); 521 522 m_overlayTopStyle.setLeft(posX - m_offset, Unit.PX); 523 m_overlayTopStyle.setWidth(width + (2 * m_offset), Unit.PX); 524 m_overlayTopStyle.setHeight(posY - m_offset, Unit.PX); 525 526 m_borderTopStyle.setLeft(posX - m_offset, Unit.PX); 527 m_borderTopStyle.setTop(posY - (2 * m_offset), Unit.PX); 528 if (m_hasButtonBar) { 529 m_borderTopStyle.setWidth(width + (2 * m_offset) + BUTTON_BAR_WIDTH, Unit.PX); 530 } else { 531 m_borderTopStyle.setWidth(width + (2 * m_offset), Unit.PX); 532 } 533 534 m_overlayBottomStyle.setLeft(posX - m_offset, Unit.PX); 535 m_overlayBottomStyle.setWidth(width + m_offset + m_offset, Unit.PX); 536 m_overlayBottomStyle.setHeight(useHeight - posY - height - m_offset, Unit.PX); 537 m_overlayBottomStyle.setTop(posY + height + m_offset, Unit.PX); 538 539 m_borderBottomStyle.setLeft(posX - m_offset, Unit.PX); 540 m_borderBottomStyle.setTop((posY + height) + m_offset, Unit.PX); 541 if (m_hasButtonBar) { 542 m_borderBottomStyle.setWidth(width + (2 * m_offset) + BUTTON_BAR_WIDTH, Unit.PX); 543 } else { 544 m_borderBottomStyle.setWidth(width + (2 * m_offset), Unit.PX); 545 } 546 547 m_overlayRightStyle.setLeft(posX + width + m_offset, Unit.PX); 548 m_overlayRightStyle.setWidth(useWidth - posX - width - m_offset, Unit.PX); 549 m_overlayRightStyle.setHeight(useHeight, Unit.PX); 550 551 m_borderRightStyle.setHeight(height + (4 * m_offset), Unit.PX); 552 m_borderRightStyle.setTop(posY - (2 * m_offset), Unit.PX); 553 if (m_hasButtonBar) { 554 m_borderRightStyle.setLeft(posX + width + m_offset + BUTTON_BAR_WIDTH, Unit.PX); 555 } else { 556 m_borderRightStyle.setLeft(posX + width + m_offset, Unit.PX); 557 } 558 559 m_buttonBar.getStyle().setTop(posY - m_offset, Unit.PX); 560 m_buttonBar.getStyle().setHeight(height + (2 * m_offset), Unit.PX); 561 m_buttonBar.getStyle().setLeft(posX + width + m_offset + 1, Unit.PX); 562 } 563}