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