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}