com.cgxlib.core.component.tooltip.SingleTooltip.java Source code

Java tutorial

Introduction

Here is the source code for com.cgxlib.core.component.tooltip.SingleTooltip.java

Source

package com.cgxlib.core.component.tooltip;

/*
 * #%L
 * CGXlib
 * %%
 * Copyright (C) 2016 CGXlib (http://www.cgxlib.com)
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.cgxlib.core.*;
import com.cgxlib.xq.client.Function;
import com.cgxlib.xq.client.XQ;
import com.cgxlib.xq.client.js.JsUtils;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.regexp.shared.SplitResult;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;

import java.util.ArrayList;
import java.util.List;

/**
 *
 */
public class SingleTooltip extends TooltipBase {

    public static final Class<SingleTooltip> CGX = CGXlib.registerPlugin(SingleTooltip.class,
            new CGXPlugin<SingleTooltip, TooltipView.ViewHandler>() {
                @Override
                public List<HTML5DataAPILoader> autoLoaders() {
                    HTML5DataAPILoader selector = GWT.create(SingleTooltipHTML5Loader.class);

                    List<HTML5DataAPILoader> selectors = new ArrayList<HTML5DataAPILoader>();
                    selectors.add(selector);

                    return selectors;
                }

                @Override
                public SingleTooltip init(CGXComponentBase xq,
                        ViewHandlerFactory<? extends TooltipView.ViewHandler> factory) {
                    return new SingleTooltip(xq, factory.make());
                }

                @Override
                public SingleTooltip init(XQ xq) {
                    return new SingleTooltip(xq);
                }
            });

    protected XQ $arrow;
    protected boolean enabled;
    protected String type;
    protected TooltipOptions options;
    protected XQ $viewport;
    protected InState inState;
    protected XQ $tip;
    protected HoverState hoverState;
    protected TooltipView.ViewHandler viewHandler;
    protected Timer timeout;

    protected SingleTooltip(XQ xq) {
        this(xq, new TooltipBSViewHandler());
    }

    protected SingleTooltip(XQ xq, TooltipView.ViewHandler viewHandler) {
        super(xq, viewHandler);
    }

    protected static XQ enter(final SingleTooltip self, boolean isEvent) {
        if (isEvent) {
            if ("focusin".equals(self.type)) {
                self.inState.focus = true;
            } else {
                self.inState.hover = true;
            }
        }

        if (self.tip().hasClass("in") || self.hoverState == HoverState.IN) {
            self.hoverState = HoverState.IN;
            return self;
        }

        if (self.timeout != null) {
            self.timeout.cancel();
        }
        self.hoverState = HoverState.IN;

        boolean willShow = self.options.delay() < 1 || self.options.showDelay() < 1;
        if (willShow) {
            return self.doShow();
        }

        self.timeout = new Timer() {
            @Override
            public void run() {
                if (self.hoverState == HoverState.IN) {
                    self.doShow();
                }
            }
        };
        self.timeout.schedule(self.options.showDelay());

        return self;
    }

    protected static SingleTooltip leave(final SingleTooltip self, boolean isEvent) {
        if (isEvent) {
            if ("focusin".equals(self.type)) {
                self.inState.focus = false;
            } else {
                self.inState.hover = false;
            }
        }

        if (self.inState.isAnyStateTrue()) {
            return self;
        }
        if (self.timeout != null) {
            self.timeout.cancel();
        }

        self.hoverState = HoverState.OUT;

        if (self.options.delay() < 1 || self.options.hideDelay() < 1) {
            self.tooltipHide(null);
        } else {
            self.timeout = new Timer() {
                @Override
                public void run() {
                    if (self.hoverState == HoverState.OUT) {
                        self.tooltipHide(null);
                    }
                }
            };
            self.timeout.schedule(self.options.hideDelay());
        }

        return self;
    }

    @Override
    public SingleTooltip tooltip() {
        return tooltip(null);
    }

    @Override
    public SingleTooltip tooltip(TooltipOptions options) {
        initTooltip(type(), getOptions(options));

        return this;
    }

    public SingleTooltip tooltipHide() {
        leave(this, false);
        return this;
    }

    public SingleTooltip tooltipShow() {
        enter(this, false);
        return this;
    }

    public SingleTooltip tooltipToggle() {
        tooltipToggle(null, this);

        return this;
    }

    public String tooltipTitle() {
        String title;
        TooltipOptions o = this.options;

        title = viewHandler.getTitle();
        if ((title == null || title.length() < 1) && o != null) {
            title = o.title();
        }

        return title;
    }

    protected SingleTooltip tooltipToggle(Event e, SingleTooltip tooltip) {
        SingleTooltip self = tooltip;
        if (e != null) {
            XQ $target = $(e.getCurrentEventTarget());
            self = $target.data("cgx." + this.type);
            if (self == null) {
                self = $target.as(CGX);
                $target.data("cgx." + this.type, self);
            }
        }

        if (e != null) {
            self.inState.click = !self.inState.click;
            if (self.inState.isAnyStateTrue()) {
                self.enter(self, false);
            } else {
                self.leave(self, false);
            }
        } else if (viewHandler.isTipIn(self.tip())) {
            self.leave(self, false);
        } else {
            self.enter(self, false);
        }

        return this;
    }

    @Override
    protected void apply(XQ xq, TooltipView.ViewHandler viewHandler) {
        viewHandler.init(this);
        this.viewHandler = viewHandler;
    }

    protected String type() {
        return "tooltip";
    }

    protected void initTooltip(String type, TooltipOptions pOptions) {
        this.enabled = true;
        this.type = type;
        final SingleTooltip $this = this;

        options = pOptions;
        this.$viewport = options.viewPortSelector() != null ? $(options.viewPortSelector()) : null;
        this.inState = new InState();

        if (Document.get().getDocumentElement().equals($this.get(0)) && this.options.selector() == null) {
            throw new Error("`selector` option must be specified when initializing " + this.type
                    + " on the window.document object!");
        }

        SplitResult triggers = RegExp.compile(" ").split(options.trigger());

        for (int i = triggers.length() - 1; i >= 0; i--) {
            String trigger = triggers.get(i);

            if ("click".equals(trigger)) {
                $this.on("click." + this.type, this.options.selector(), new Function() {
                    @Override
                    public void f() {
                        $this.tooltipToggle(null, $this);
                    }
                });
            } else if (trigger != "manual") {
                String eventIn = trigger == "hover" ? "mouseenter" : "focusin";
                String eventOut = trigger == "hover" ? "mouseleave" : "focusout";

                $this.on(eventIn + '.' + this.type, options.selector(), new Function() {
                    @Override
                    public void f() {
                        $this.enter($this, true);
                    }
                });
                $this.on(eventOut + '.' + this.type, options.selector(), new Function() {
                    @Override
                    public void f() {
                        $this.leave($this, true);
                    }
                });
            }
        }

        if (options.selector() != null) {
            options.trigger("manual");
            options.selector(null);
        } else {
            viewHandler.fixTitle();
        }
    }

    public SingleTooltip tooltipEnable() {
        enabled = true;

        return this;
    }

    public SingleTooltip tooltipDisable() {
        enabled = false;

        return this;
    }

    public SingleTooltip tooltipToggleEnabled() {
        this.enabled = !this.enabled;

        return this;
    }

    protected SingleTooltip doShow() {
        NativeEvent e = CGXHelper.createNativeEvent("show", "cgx." + type, null);

        if (this.hasContent() && this.enabled) {
            this.trigger(e);

            boolean inDom = viewHandler.inDom();

            if (JsUtils.isDefaultPrevented(e) || !inDom) {
                return this;
            }

            final SingleTooltip that = this;

            XQ $tip = tip();

            String tipId = getUID(type);

            setContent();

            viewHandler.assignId($tip, tipId);

            if (this.options.animation()) {
                viewHandler.animateTip($tip);
            }

            Placement placement = this.options.placement();
            String placementStr = placement != null ? placement.name() : null;

            String autoToken = "/\\s?auto?\\s?/i";
            RegExp regExp = RegExp.compile(autoToken);
            boolean autoPlace = placementStr != null && regExp.test(placementStr);
            if (autoPlace) {
                placementStr = regExp.replace(autoToken, "");
                if (placementStr == null || placementStr.length() < 1) {
                    placement = Placement.top;
                }
            }

            viewHandler.setupTip($tip, placement);
            $tip.data("cgx." + this.type, this);

            if (this.options.container() != null) {
                $tip.appendTo(this.options.container());
            } else {
                $tip.insertAfter(this);
            }
            this.trigger("inserted.cgx." + this.type);

            Position pos = viewHandler.getPosition(this);

            int actualWidth = viewHandler.tipWidth($tip);
            int actualHeight = viewHandler.tipHeight($tip);

            if (autoPlace) {

                Placement orgPlacement = placement;
                Position viewportDim = viewHandler.getPosition($viewport);

                if (placement == Placement.bottom && pos.bottom + actualHeight > viewportDim.bottom) {
                    placement = Placement.top;
                } else if (placement == Placement.top && pos.top - actualHeight < viewportDim.top) {
                    placement = Placement.bottom;
                } else if (placement == Placement.right && pos.right + actualWidth > viewportDim.width) {
                    placement = Placement.left;
                } else if (placement == Placement.left && pos.left - actualWidth < viewportDim.left) {
                    placement = Placement.right;
                }

                viewHandler.switchPlacement($tip, orgPlacement, placement);
            }

            Offset calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight);

            applyPlacement(calculatedOffset, placement);

            Function complete = new Function() {
                @Override
                public void f() {
                    HoverState prevHoverState = that.hoverState;
                    that.trigger("shown.cgx." + that.type);
                    that.hoverState = null;

                    if (prevHoverState == HoverState.OUT) {
                        that.leave(that, false);
                    }
                }
            };

            final boolean useTransitions = transitions() && viewHandler.useTransitions($tip);
            if (useTransitions) {
                $tip.as(CGXlib.CGX).onceWhenTransitionEnds(complete).emulateTransitionEnd(transitionDuration());
            } else {
                complete.f();
            }
        }

        return this;
    }

    protected void setContent() {
        viewHandler.setContent($tip, tooltipTitle(), options.html());
    }

    protected void applyPlacement(Offset offset, Placement placement) {
        XQ $tip = this.tip();
        int width = $tip.get(0).getOffsetWidth();
        int height = $tip.get(0).getOffsetHeight();

        int marginTop = viewHandler.tipMarginTop($tip);
        int marginLeft = viewHandler.tipMarginLeft($tip);

        offset.top += marginTop;
        offset.left += marginLeft;

        viewHandler.tipIn($tip);

        int actualWidth = $tip.get(0).getOffsetWidth();
        int actualHeight = $tip.get(0).getOffsetHeight();

        if (placement == Placement.top && actualHeight != height) {
            offset.top = offset.top + height - actualHeight;
        }

        Offset delta = getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight);

        if (delta.left > 0) {
            offset.left += delta.left;
        } else {
            offset.top += delta.top;
        }

        boolean isVertical = placement == Placement.top || placement == Placement.bottom;
        int arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight;

        $tip.offset(offset);
        Element tip = $tip.get(0);
        int offsetAmount = isVertical ? tip.getOffsetWidth() : tip.getOffsetHeight();
        replaceArrow(arrowDelta, offsetAmount, isVertical);

    }

    protected void replaceArrow(int delta, int dimension, boolean isVertical) {
        viewHandler.replaceArrow(arrow(), delta, dimension, isVertical);
    }

    protected XQ arrow() {
        if ($arrow == null) {
            $arrow = viewHandler.arrow(tip());
        }
        return $arrow;
    }

    protected Offset getViewportAdjustedDelta(Placement placement, Offset pos, int actualWidth, int actualHeight) {
        Offset delta = new Offset(0, 0);
        if (this.$viewport == null) {
            return delta;
        }

        int viewportPadding = options.viewPortPadding();
        Position viewportDimensions = viewHandler.getPosition($viewport);

        if (placement == Placement.right || placement == Placement.left) {
            int topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll;
            int bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight;
            if (topEdgeOffset < viewportDimensions.top) { // top overflow
                delta.top = viewportDimensions.top - topEdgeOffset;
            } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
                delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset;
            }
        } else {
            int leftEdgeOffset = pos.left - viewportPadding;
            int rightEdgeOffset = pos.left + viewportPadding + actualWidth;
            if (leftEdgeOffset < viewportDimensions.left) { // left overflow
                delta.left = viewportDimensions.left - leftEdgeOffset;
            } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
                delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset;
            }
        }

        return delta;
    }

    protected Offset getCalculatedOffset(Placement placement, Position pos, int actualWidth, int actualHeight) {
        Offset offset = null;

        if (placement == Placement.bottom) {
            offset = new Offset(pos.left + pos.width / 2 - actualWidth / 2, pos.top + pos.height);
        } else if (placement == Placement.top) {
            offset = new Offset(pos.left + pos.width / 2 - actualWidth / 2, pos.top - actualHeight);
        } else if (placement == Placement.left) {
            offset = new Offset(pos.left - actualWidth, pos.top + pos.height / 2 - actualHeight / 2);
        } else {
            //right
            offset = new Offset(pos.left + pos.width, pos.top + pos.height / 2 - actualHeight / 2);
        }

        return offset;
    }

    public SingleTooltip tooltipDestroy() {
        final SingleTooltip that = this;
        if (this.timeout != null) {
            this.timeout.cancel();
        }
        Function callback = new Function() {
            @Override
            public void f() {
                that.off('.' + that.type).removeData("cgx." + that.type);
                if (that.$tip != null) {
                    that.$tip.detach();
                }
                that.$tip = null;
                that.$arrow = null;
                that.$viewport = null;
            }
        };
        tooltipHide(callback);

        return this;
    }

    protected SingleTooltip tooltipHide(final Function callback) {
        if ($tip == null) {
            return this;
        }

        final SingleTooltip that = this;
        NativeEvent e = CGXHelper.createNativeEvent("hide", "cgx." + type, null);

        Function complete = new Function() {
            @Override
            public void f() {
                viewHandler.hideTip($tip, that.hoverState);
                that.trigger("hidden.cgx." + that.type);

                if (callback != null) {
                    callback.f();
                }
            }
        };

        this.trigger(e);

        if (JsUtils.isDefaultPrevented(e)) {
            return this;
        }

        viewHandler.preHideTip($tip);

        final boolean useTransitions = transitions() && viewHandler.useTransitions($tip);
        if (useTransitions) {
            $tip.as(CGXlib.CGX).onceWhenTransitionEnds(complete).emulateTransitionEnd(transitionDuration());
        } else {
            complete.f();
        }

        this.hoverState = null;

        return this;
    }

    protected String getUID(String prefix) {
        String UID = viewHandler.getUID(prefix);
        return UID;
    }

    protected boolean hasContent() {
        String title = tooltipTitle();
        return title != null && title.length() > 0;
    }

    protected XQ tip() {
        if ($tip == null) {
            $tip = viewHandler.createTip(options.template());
            if ($tip.length() != 1) {
                throw new Error(this.type + " `template` option must consist of exactly 1 top-level element!");
            }
        }
        return $tip;
    }

    protected TooltipOptions getOptions(TooltipOptions pOptions) {
        TooltipOptions options = createOptions();
        if (pOptions != null) {
            options.animation(pOptions.animation());
            options.placement(pOptions.placement());
            options.selector(pOptions.selector());
            options.template(pOptions.template());
            options.trigger(pOptions.trigger());
            options.title(pOptions.title());
            options.delay(pOptions.delay());
            options.html(pOptions.html());
            options.container(pOptions.container());
            options.viewPortSelector(pOptions.viewPortSelector());
            options.viewPortPadding(pOptions.viewPortPadding());
            options.delay(pOptions.delay());
            options.content(pOptions.content());
        }

        options.loadData(this);

        return options;
    }

    protected TooltipOptions createOptions() {
        return new TooltipOptions();
    }

    public enum HoverState {
        IN, OUT;
    }

    public static class Position {
        public int scroll;
        public int top;
        public int left;
        public int right;
        public int width;
        public int height;
        public int bottom;

        @Override
        public String toString() {
            JavaScriptObject obj = JavaScriptObject.createObject();
            JsUtils.prop(obj, "scroll", scroll);
            JsUtils.prop(obj, "top", top);
            JsUtils.prop(obj, "left", left);
            JsUtils.prop(obj, "right", right);
            JsUtils.prop(obj, "width", width);
            JsUtils.prop(obj, "height", height);
            JsUtils.prop(obj, "bottom", bottom);

            return JsUtils.JSON2String(obj);
        }
    }

    protected class InState {
        boolean click = false;
        boolean hover = false;
        boolean focus = false;

        boolean isAnyStateTrue() {
            return click || hover || focus;
        }
    }

}