content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (C) Alkacon Software (https://www.alkacon.com)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   * Lesser General Public License for more details.
   *
   * For further information about Alkacon Software, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://www.opencms.org
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package org.opencms.acacia.client.ui;
  
  import org.opencms.acacia.client.CmsAttributeHandler;
  import org.opencms.acacia.client.CmsButtonBarHandler;
  import org.opencms.acacia.client.CmsChoiceMenuEntryBean;
  import org.opencms.acacia.client.CmsEditorBase;
  import org.opencms.acacia.client.I_CmsInlineFormParent;
  import org.opencms.acacia.client.I_CmsInlineHtmlUpdateHandler;
  import org.opencms.acacia.client.I_CmsWidgetService;
  import org.opencms.acacia.client.css.I_CmsLayoutBundle;
  import org.opencms.acacia.client.entity.CmsEntityBackend;
  import org.opencms.acacia.shared.CmsEntity;
  import org.opencms.acacia.shared.CmsType;
  import org.opencms.gwt.client.ui.CmsHighlightingBorder;
  import org.opencms.gwt.client.ui.CmsPopup;
  import org.opencms.gwt.client.ui.CmsPushButton;
  import org.opencms.gwt.client.ui.I_CmsButton;
  import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
  import org.opencms.gwt.client.util.CmsPositionBean;
  
  import java.util.List;
  
  import com.google.gwt.core.shared.GWT;
  import com.google.gwt.dom.client.Element;
  import com.google.gwt.dom.client.Style.Display;
  import com.google.gwt.dom.client.Style.Unit;
  import com.google.gwt.event.dom.client.ClickEvent;
  import com.google.gwt.event.dom.client.ClickHandler;
  import com.google.gwt.event.dom.client.MouseOutEvent;
  import com.google.gwt.event.dom.client.MouseOverEvent;
  import com.google.gwt.event.logical.shared.CloseEvent;
  import com.google.gwt.event.logical.shared.CloseHandler;
  import com.google.gwt.event.logical.shared.ResizeEvent;
  import com.google.gwt.event.logical.shared.ResizeHandler;
  import com.google.gwt.event.logical.shared.ValueChangeEvent;
  import com.google.gwt.event.logical.shared.ValueChangeHandler;
  import com.google.gwt.event.shared.HandlerRegistration;
  import com.google.gwt.uibinder.client.UiBinder;
  import com.google.gwt.uibinder.client.UiField;
  import com.google.gwt.uibinder.client.UiHandler;
  import com.google.gwt.user.client.Command;
  import com.google.gwt.user.client.Timer;
  import com.google.gwt.user.client.Window;
  import com.google.gwt.user.client.rpc.AsyncCallback;
  import com.google.gwt.user.client.ui.Composite;
  import com.google.gwt.user.client.ui.FlowPanel;
  import com.google.gwt.user.client.ui.PopupPanel;
  import com.google.gwt.user.client.ui.RootPanel;
  
  /**
   * Widget allowing form based editing for parts of a content to enhance the in-line editing.<p>
   */
  public final class CmsInlineEntityWidget extends Composite {
  
      /**
       * Flow panel with handling descendant resizes to reposition pop-up.<p>
       */
      protected class FormPanel extends FlowPanel implements org.opencms.gwt.client.I_CmsDescendantResizeHandler {
  
          /**
           * Constructor.<p>
           */
          protected FormPanel() {
  
          }
  
          /**
           * @see org.opencms.gwt.client.I_CmsDescendantResizeHandler#onResizeDescendant()
           */
          public void onResizeDescendant() {
  
              positionPopup();
          }
      }
  
      /**
       * Timer to update the HTML.<p>
       */
      protected class UpdateTimer extends Timer {
  
          /** Indicates if the timer is scheduled. */
          private boolean m_scheduled;
  
          /**
           * @see com.google.gwt.user.client.Timer#cancel()
           */
          @Override
          public void cancel() {
  
              m_scheduled = false;
              super.cancel();
          }
  
          /**
           * Returns if the timer is already scheduled.<p>
           *
           * @return <code>true</code> if the timer is scheduled
           */
          public boolean isScheduled() {
  
              return m_scheduled;
          }
  
          /**
           * @see com.google.gwt.user.client.Timer#run()
           */
          @Override
          public void run() {
  
              m_scheduled = false;
              runHtmlUpdate();
          }
  
          /**
           * @see com.google.gwt.user.client.Timer#schedule(int)
           */
          @Override
          public void schedule(int delayMillis) {
  
              m_scheduled = true;
              super.schedule(delayMillis);
          }
      }
  
      /**
       * The UI binder interface.<p>
       */
      interface I_InlineEntityWidgetUiBinder extends UiBinder<FlowPanel, CmsInlineEntityWidget> {
          // nothing to do
      }
  
      /** The height required by edit buttons. */
      private static final int REQUIRED_BUTTON_HEIGHT = 20;
  
      /** The UI binder instance. */
      private static I_InlineEntityWidgetUiBinder uiBinder = GWT.create(I_InlineEntityWidgetUiBinder.class);
  
      /** The add button. */
      @UiField
      protected CmsAttributeChoiceWidget m_addButton;
  
      /** The attribute choice button. */
      @UiField
      protected CmsAttributeChoiceWidget m_attributeChoice;
  
      /** The down button. */
      @UiField
      protected CmsPushButton m_downButton;
  
      /** The injected button. */
      @UiField
      protected CmsPushButton m_editButton;
  
      /** The remove button. */
      @UiField
      protected CmsPushButton m_removeButton;
  
      /** The up button. */
      @UiField
      protected CmsPushButton m_upButton;
  
      /** The highlighting border widget. */
      CmsHighlightingBorder m_highlighting;
  
      /** The pop-up panel. */
      CmsPopup m_popup;
  
      /** The handler of the attribute to edit. */
      private CmsAttributeHandler m_attributeHandler;
  
      /** The attribute value index. */
      private int m_attributeIndex;
  
      /** The change handler registration. */
      private HandlerRegistration m_entityChangeHandlerRegistration;
  
      /** The parent widget. */
      private I_CmsInlineFormParent m_formParent;
  
      /** Indicates if the content has been changed while the edit pop-up was shown. */
      private boolean m_hasChanges;
  
      /** Handles HTML updates if required. */
      private I_CmsInlineHtmlUpdateHandler m_htmlUpdateHandler;
  
      /** A timer to update the overlay position. */
      private Timer m_overlayTimer;
  
      /** The parent of the entity to edit. */
      private CmsEntity m_parentEntity;
  
      /** Flag indicating the popup has been closed. */
      private boolean m_popupClosed;
  
      /** The reference DOM element, will be highlighted during editing. */
      private Element m_referenceElement;
  
      /** Flag indicating it is required to open the edit popup aftera HTML update. */
      private boolean m_requireShowPopup;
  
      /** Flag indicating an HTML update is running. */
      private boolean m_runningUpdate;
  
      /** The dialog title. */
      private String m_title;
  
      /** Schedules the HTML update. */
      private UpdateTimer m_updateTimer;
  
      /** The widget service. */
      private I_CmsWidgetService m_widgetService;
  
      /**
       * Constructor.<p>
       *
       * @param referenceElement the reference DOM element, will be highlighted during editing
       * @param formParent the parent widget
       * @param parentEntity the parent of the entity to edit
       * @param attributeHandler the attribute handler
       * @param attributeIndex the attribute value index
       * @param htmlUpdateHandler handles HTML updates if required
       * @param widgetService the widget service
       */
      private CmsInlineEntityWidget(
          Element referenceElement,
          I_CmsInlineFormParent formParent,
          CmsEntity parentEntity,
          CmsAttributeHandler attributeHandler,
          int attributeIndex,
          I_CmsInlineHtmlUpdateHandler htmlUpdateHandler,
          I_CmsWidgetService widgetService) {
  
          initWidget(uiBinder.createAndBindUi(this));
          m_parentEntity = parentEntity;
          m_attributeHandler = attributeHandler;
          m_attributeIndex = attributeIndex;
          m_referenceElement = referenceElement;
          m_formParent = formParent;
          m_htmlUpdateHandler = htmlUpdateHandler;
          m_widgetService = widgetService;
          m_title = "";
          m_updateTimer = new UpdateTimer();
          m_popupClosed = true;
          initButtons();
          addDomHandler(CmsButtonBarHandler.INSTANCE, MouseOverEvent.getType());
          addDomHandler(CmsButtonBarHandler.INSTANCE, MouseOutEvent.getType());
      }
  
      /**
       * Creates the inline edit widget and injects it next to the context element.<p>
       *
       * @param element the context element
       * @param formParent the parent widget
       * @param parentEntity the parent entity
       * @param attributeHandler the attribute handler
       * @param attributeIndex the attribute value index
       * @param htmlUpdateHandler handles HTML updates if required
       * @param widgetService the widget service
       *
       * @return the widget instance
       */
      public static CmsInlineEntityWidget createWidgetForEntity(
          Element element,
          I_CmsInlineFormParent formParent,
          CmsEntity parentEntity,
          CmsAttributeHandler attributeHandler,
          int attributeIndex,
          I_CmsInlineHtmlUpdateHandler htmlUpdateHandler,
          I_CmsWidgetService widgetService) {
  
          CmsInlineEntityWidget widget = new CmsInlineEntityWidget(
              element,
              formParent,
              parentEntity,
              attributeHandler,
              attributeIndex,
              htmlUpdateHandler,
              widgetService);
          CmsInlineEditOverlay.getRootOverlay().addButton(widget, getButtonTop(element));
          attributeHandler.updateButtonVisibilty(widget);
          return widget;
      }
  
      /**
       * Returns the button top position for the given reference element.<p>
       *
       * @param element the reference element
       *
       * @return the button position
       */
      private static int getButtonTop(Element element) {
  
          int top = element.getAbsoluteTop();
          int height = element.getClientHeight();
          if ((height < REQUIRED_BUTTON_HEIGHT) && (top > REQUIRED_BUTTON_HEIGHT)) {
              top = top - REQUIRED_BUTTON_HEIGHT;
          }
          return top;
      }
  
      /**
       * Returns the attribute value index.<p>
       *
       * @return the attribute value index
       */
      public int getAttributeIndex() {
  
          return m_attributeIndex;
      }
  
      /**
       * Sets the visibility of the reference element highlighting border.<p>
       *
       * @param visible <code>true</code> to show the highlighting
       */
      public void setContentHighlightingVisible(boolean visible) {
  
          if (visible) {
              if (m_highlighting == null) {
                  m_highlighting = new CmsHighlightingBorder(
                      CmsPositionBean.getBoundingClientRect(m_referenceElement),
                      CmsHighlightingBorder.BorderColor.red);
                  RootPanel.get().add(m_highlighting);
              } else {
                  m_highlighting.setPosition(CmsPositionBean.getBoundingClientRect(m_referenceElement));
              }
          } else {
              if (m_highlighting != null) {
                  m_highlighting.removeFromParent();
                  m_highlighting = null;
              }
          }
      }
  
      /**
       * Updates the visibility of the add, remove, up and down buttons.<p>
       *
       * @param hasEditButton <code>true</code> if the edit button should be visible
       * @param hasAddButton <code>true</code> if the add button should be visible
       * @param hasRemoveButton <code>true</code> if the remove button should be visible
       * @param hasSortButtons <code>true</code> if the sort buttons should be visible
       */
      public void updateButtonVisibility(
          boolean hasEditButton,
          boolean hasAddButton,
          boolean hasRemoveButton,
          boolean hasSortButtons) {
  
          if (hasEditButton) {
              m_editButton.getElement().getStyle().clearDisplay();
          } else {
              m_editButton.getElement().getStyle().setDisplay(Display.NONE);
          }
          //        if (hasAddButton && m_isChoice) {
          //            m_attributeChoice.getElement().getStyle().clearDisplay();
          //        } else {
          m_attributeChoice.getElement().getStyle().setDisplay(Display.NONE);
          //        }
          if (hasAddButton) {
              m_addButton.getElement().getStyle().clearDisplay();
          } else {
              m_addButton.getElement().getStyle().setDisplay(Display.NONE);
          }
  
          if (hasRemoveButton) {
              m_removeButton.getElement().getStyle().clearDisplay();
          } else {
              m_removeButton.getElement().getStyle().setDisplay(Display.NONE);
          }
          if (hasSortButtons && (m_attributeIndex != 0)) {
              m_upButton.getElement().getStyle().clearDisplay();
          } else {
              m_upButton.getElement().getStyle().setDisplay(Display.NONE);
          }
          if (hasSortButtons && (getElement().getNextSibling() != null)) {
              m_downButton.getElement().getStyle().clearDisplay();
          } else {
              m_downButton.getElement().getStyle().setDisplay(Display.NONE);
          }
  
          if (hasEditButton && (hasAddButton || hasRemoveButton || hasSortButtons)) {
              // set multi button mode
              addStyleName(I_CmsLayoutBundle.INSTANCE.form().multiButtonBar());
          } else {
              removeStyleName(I_CmsLayoutBundle.INSTANCE.form().multiButtonBar());
          }
      }
  
      /**
       * Positions the widget button above the reference element.<p>
       */
      protected void positionWidget() {
  
          CmsInlineEditOverlay.getRootOverlay().setButtonPosition(this, getButtonTop(m_referenceElement));
      }
  
      /** Adds a new attribute value. */
      void addNewAttributeValue() {
  
          m_attributeHandler.addNewAttributeValueToEntity(m_attributeIndex);
          m_requireShowPopup = true;
          m_attributeIndex += 1;
          runHtmlUpdate();
      }
  
      /**
       * Repositions the edit overlay after the HTML has been updated.<p>
       */
      void afterHtmlUpdate() {
  
          if (m_overlayTimer != null) {
              m_overlayTimer.cancel();
              m_overlayTimer = null;
          }
          m_runningUpdate = false;
          List<Element> elements = CmsEntityBackend.getInstance().getAttributeElements(
              m_parentEntity,
              m_attributeHandler.getAttributeName(),
              m_formParent.getElement());
          if (m_popupClosed) {
              // the form popup has already been closed, reinitialize the editing widgets for updated HTML
              CmsInlineEditOverlay.updateCurrentOverlayPosition();
              if (m_requireShowPopup) {
                  if (elements.size() > m_attributeIndex) {
                      m_referenceElement = elements.get(m_attributeIndex);
                  }
                  showEditPopup(null);
                  m_hasChanges = true;
              } else {
                  CmsInlineEditOverlay.getRootOverlay().clearButtonPanel();
                  m_htmlUpdateHandler.reinitWidgets(m_formParent);
              }
  
          } else {
              if (m_referenceElement != null) {
                  CmsInlineEditOverlay.removeLastOverlay();
              }
              if (elements.size() > m_attributeIndex) {
                  m_referenceElement = elements.get(m_attributeIndex);
                  CmsInlineEditOverlay.addOverlayForElement(m_referenceElement);
              } else {
                  m_referenceElement = m_formParent.getElement();
                  CmsInlineEditOverlay.addOverlayForElement(m_referenceElement);
              }
          }
          // schedule to update the ovelay position
          m_overlayTimer = new Timer() {
  
              /** Timer run counter. */
              private int m_timerRuns;
  
              /**
               * @see com.google.gwt.user.client.Timer#run()
               */
              @Override
              public void run() {
  
                  CmsInlineEditOverlay.updateCurrentOverlayPosition();
                  if (m_timerRuns > 3) {
                      cancel();
                  }
                  m_timerRuns++;
              }
          };
          m_overlayTimer.scheduleRepeating(100);
      }
  
      /**
       * Sets the changed flag.<p>
       */
      void onEntityChange() {
  
          if (m_updateTimer.isScheduled()) {
              m_updateTimer.cancel();
          }
          m_updateTimer.schedule(150);
          m_hasChanges = true;
      }
  
      /**
       * Cleanup after the edit pop-up was opened.<p>
       */
      void onPopupClose() {
  
          if (m_referenceElement != null) {
              CmsInlineEditOverlay.removeLastOverlay();
          }
          CmsInlineEditOverlay.updateCurrentOverlayPosition();
          if (m_entityChangeHandlerRegistration != null) {
              m_entityChangeHandlerRegistration.removeHandler();
          }
          CmsAttributeHandler.setResizeHandler(null);
          if (!m_runningUpdate) {
              if (m_hasChanges) {
                  CmsInlineEditOverlay.getRootOverlay().clearButtonPanel();
                  m_htmlUpdateHandler.reinitWidgets(m_formParent);
              }
          }
          m_popup = null;
      }
  
      /** Handles the remove attribute click.<p>
       *
       * @param event the click event
       */
      @UiHandler("m_removeButton")
      void onRemoveClick(ClickEvent event) {
  
          m_attributeHandler.removeAttributeValueFromEntity(m_attributeIndex);
          setContentHighlightingVisible(false);
          runHtmlUpdate();
      }
  
      /**
       * Positions the given pop-up relative to the reference element.<p>
       */
      void positionPopup() {
  
          if (m_popup != null) {
              if (m_referenceElement != null) {
                  CmsPositionBean referencePosition = CmsPositionBean.getBoundingClientRect(m_referenceElement);
                  int currentTop = m_popup.getAbsoluteTop();
                  int windowHeight = Window.getClientHeight();
                  int scrollTop = Window.getScrollTop();
                  int contentHeight = m_popup.getOffsetHeight();
                  int top = referencePosition.getTop();
                  if (((windowHeight + scrollTop) < (top + referencePosition.getHeight() + contentHeight + 20))
                      && ((contentHeight + 40) < top)) {
                      top = top - contentHeight - 5;
                      if ((currentTop < top) && ((top - currentTop) < 200)) {
                          // keep the current position
                          top = currentTop;
                      }
                  } else {
                      top = top + referencePosition.getHeight() + 5;
                      if ((currentTop > top) && ((currentTop - top) < 200)) {
                          // keep the current position
                          top = currentTop;
                      }
                  }
                  m_popup.center();
                  m_popup.setPopupPosition(m_popup.getPopupLeft(), top);
                  if (((contentHeight + top) - scrollTop) > windowHeight) {
                      Window.scrollTo(Window.getScrollLeft(), ((contentHeight + top) - windowHeight) + 20);
                  }
              } else {
                  m_popup.center();
              }
          }
      }
  
      /**
       * Updates the HTML according to the entity data.<p>
       */
      void runHtmlUpdate() {
  
          if (m_runningUpdate) {
              m_updateTimer.schedule(50);
          } else {
              m_runningUpdate = true;
              m_htmlUpdateHandler.updateHtml(m_formParent, new Command() {
  
                  public void execute() {
  
                      afterHtmlUpdate();
                  }
              });
          }
      }
  
      /**
       * Opens the form popup.<p>
       *
       * @param clickEvent the click event
       */
      @UiHandler("m_editButton")
      void showEditPopup(ClickEvent clickEvent) {
  
          m_editButton.clearHoverState();
          m_popup = new CmsPopup(m_title, -1);
          m_popup.setModal(true);
          m_popup.setAutoHideEnabled(true);
          m_popup.removePadding();
          m_popup.addCloseHandler(new CloseHandler<PopupPanel>() {
  
              public void onClose(CloseEvent<PopupPanel> event) {
  
                  onPopupClose();
              }
          });
          m_hasChanges = false;
          m_requireShowPopup = false;
          m_entityChangeHandlerRegistration = m_parentEntity.addValueChangeHandler(new ValueChangeHandler<CmsEntity>() {
  
              public void onValueChange(ValueChangeEvent<CmsEntity> event) {
  
                  onEntityChange();
              }
          });
          CmsType type = CmsEntityBackend.getInstance().getType(m_parentEntity.getTypeName());
          FlowPanel formPanel = new FormPanel();
          formPanel.setStyleName(I_CmsLayoutBundle.INSTANCE.form().formParent());
          formPanel.getElement().getStyle().setMargin(0, Unit.PX);
          formPanel.getElement().getStyle().setBorderWidth(0, Unit.PX);
          formPanel.getElement().getStyle().setPropertyPx("minHeight", 30);
          m_popup.add(formPanel);
          m_popup.addDialogClose(null);
          I_CmsLayoutBundle.INSTANCE.dialogCss().ensureInjected();
          m_popup.show();
          CmsAttributeHandler.setScrollElement(formPanel.getElement());
          CmsAttributeHandler.setResizeHandler(new ResizeHandler() {
  
              public void onResize(ResizeEvent event) {
  
                  positionPopup();
              }
          });
          m_widgetService.getRendererForType(
              type).renderAttributeValue(m_parentEntity, m_attributeHandler, m_attributeIndex, formPanel);
          CmsInlineEditOverlay.addOverlayForElement(m_referenceElement);
          positionPopup();
          m_popup.getElement().getStyle().setZIndex(I_CmsLayoutBundle.INSTANCE.constants().css().zIndexPopup());
          m_popupClosed = false;
      }
  
      /**
       * Initializes the button styling.<p>
       */
      private void initButtons() {
  
          m_addButton.addChoice(
              m_attributeHandler.getWidgetService(),
              new CmsChoiceMenuEntryBean(m_attributeHandler.getAttributeName()),
              new AsyncCallback<CmsChoiceMenuEntryBean>() {
  
                  public void onFailure(Throwable caught) {
  
                      // will not be called
  
                  }
  
                  public void onSuccess(CmsChoiceMenuEntryBean selectedEntry) {
  
                      // nothing to do
                  }
              });
          m_addButton.addDomHandler(new ClickHandler() {
  
              public void onClick(ClickEvent event) {
  
                  m_addButton.hide();
                  addNewAttributeValue();
                  event.preventDefault();
                  event.stopPropagation();
  
              }
          }, ClickEvent.getType());
  
          m_editButton.setImageClass(I_CmsButton.PEN_SMALL);
          m_editButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
  
          m_removeButton.setImageClass(I_CmsButton.CUT_SMALL);
          m_removeButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
  
          m_upButton.setImageClass(I_CmsButton.EDIT_UP_SMALL);
          m_upButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
  
          m_downButton.setImageClass(I_CmsButton.EDIT_DOWN_SMALL);
          m_downButton.setButtonStyle(ButtonStyle.FONT_ICON, null);
  
          if (CmsEditorBase.hasDictionary()) {
              String label = m_widgetService.getAttributeLabel(m_attributeHandler.getAttributeName());
              m_addButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_ADD_1, label));
              m_removeButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_DELETE_1, label));
              m_upButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_UP_0));
              m_downButton.setTitle(CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_MOVE_DOWN_0));
              m_title = CmsEditorBase.getMessageForKey(CmsEditorBase.GUI_VIEW_EDIT_1, label);
              m_editButton.setTitle(m_title);
          }
      }
  }
  