Callout Class

if(!javaxt) var javaxt={};
if(!javaxt.dhtml) javaxt.dhtml={};


//******************************************************************************
//**  Callout Class
//******************************************************************************
/**
 *   Used to create simple tooltip/popup boxes with an arrow.
 *
 ******************************************************************************/

javaxt.dhtml.Callout = function(parent, config) {
    this.className = "javaxt.dhtml.Callout";

    var me = this;
    var div, innerDiv, callout, notch, notchBorder;
    var opening = false;

    var defaultConfig = {

        position: "absolute",


      /** Style for individual elements within the component. Note that you can
       *  provide CSS class names instead of individual style definitions.
       */
        style: {

            panel: {
                border: "1px solid #c5d9e8",
                backgroundColor: "#eef4f9",
                borderRadius: "6px",
                boxShadow: "0 12px 14px 0 rgba(0, 0, 0, 0.2), 0 13px 20px 0 rgba(0, 0, 0, 0.2)"
            },

            arrow: {

              //Only backgroundColor, borderColor, width, height, and padding
              //are considered. All other properties are ignored.
                borderColor: "#c5d9e8",
                backgroundColor: "#eef4f9",
                width: "10px",
                height: "10px",
                padding: "10px"
            }

        }
    };


  //**************************************************************************
  //** Constructor
  //**************************************************************************
    var init = function(){

      //Clone the config so we don't modify the original config object
        var clone = {};
        merge(clone, config);


      //Merge clone with default config
        merge(clone, defaultConfig);
        config = clone;




      //Create outer div
        div = createElement("div", parent);
        div.setAttribute("desc", me.className);
        if (config.position==="absolute"){
            div.style.display = "none";
            div.style.position = "absolute";
            div.style.top = div.style.left = 0;
        }
        me.el = div;


      //Create callout box
        callout = createElement("div", div, config.style.panel);
        callout.style.position = "relative";
        callout.style.margin = 0;
        callout.style.padding = 0;
        callout.style.borderWidth = "1px"; //notch assumes the border is 1px. See showAt() method...



      //Create content div
        innerDiv = createElement("div", callout, {
            width: "100%",
            height: "100%"
        });



      //Create temporary div to get arrow style
        var temp = createElement("div", config.style.arrow);
        temp.style.position = "absolute";
        temp.style.visibility = 'hidden';
        temp.style.display = 'block';
        var body = document.getElementsByTagName("body")[0];
        body.appendChild(temp);
        var style = temp.currentStyle || window.getComputedStyle(temp);
        var getStyle = function(prop){

            var _getStyle = function(prop){
                if (style.getPropertyValue){
                    var val = style.getPropertyValue(prop);
                    if (val && val.length>0) return val;
                    prop = prop.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase();
                    return style.getPropertyValue(prop);
                }
                else{
                    return style[prop];
                }
            };

            if (prop instanceof Array){
                var arr = prop;
                for (var i=0; i<arr.length; i++){
                    var val = _getStyle(arr[i]);
                    if (val && val.length>0){
                        return val;
                    }
                }
            }
            else{
                return _getStyle(prop);
            }

        };

        config.arrow = {
            backgroundColor: getStyle("backgroundColor"),
            borderColor: getStyle(["borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"]),
            paddingTop: parseInt(getStyle("paddingTop")),
            paddingBottom: parseInt(getStyle("paddingBottom")),
            paddingLeft: parseInt(getStyle("paddingLeft")),
            paddingRight: parseInt(getStyle("paddingRight"))
        };
        temp.style.border = 0;
        temp.style.padding = 0;
        temp.style.margin = 0;
        config.arrow.width = temp.offsetWidth;
        config.arrow.height = temp.offsetHeight;
        body.removeChild(temp);
        temp = null;



      //Create notch (triangle)
        notch = createElement("b");
        notch.setAttribute("desc","notch");
        notch.style.position = "absolute";
        notch.style.top=0;
        notch.style.left=0;
        notch.style.margin=0;
        notch.style.padding=0;
        notch.style.width=0;
        notch.style.height=0;
        notch.style.fontSize=0;
        notch.style.lineHeight=0;



      //Create border for the notch
        notchBorder = createElement("b", div);
        notchBorder.setAttribute("desc","notchBorder");
        notchBorder.style.position="absolute";
        notchBorder.style.top=0;
        notchBorder.style.left=0;
        notchBorder.style.margin=0;
        notchBorder.style.padding=0;
        notchBorder.style.width=0;
        notchBorder.style.height=0;
        notchBorder.style.fontSize=0;
        notchBorder.style.lineHeight=0;


        // ie6 transparent fix
        //_border-right-color: pink;
        //_border-left-color: pink;
        //_filter: chroma(color=pink);



        div.appendChild(notch);



      //Add event listeners
        if (config.position==="absolute"){

            var onresize = function(){
                me.hide();
            };

            var onclick = function(e){
                var x = e.clientX;
                var y = e.clientY;
                hideIfOutside(x, y);
            };

            var ontouchstart = function(e){
                var x = e.changedTouches[0].pageX;
                var y = e.changedTouches[0].pageY;
                hideIfOutside(x, y);
            };


            if (document.addEventListener) { // For all major browsers, except IE 8 and earlier
                document.addEventListener("click", onclick);
                document.addEventListener("touchstart", ontouchstart);
                window.addEventListener("resize", onresize);
            }
            else if (document.attachEvent) { // For IE 8 and earlier versions
                document.attachEvent("onclick", onclick);
                document.attachEvent("ontouchstart", ontouchstart);
                window.attachEvent("onresize", onresize);
            }
        }
    };


  //**************************************************************************
  //** hideIfOutside
  //**************************************************************************
    var hideIfOutside = function(x, y){

        if (opening) return;

        if (div.style.display === 'block'){

            var x1 = parseInt(div.style.left);
            var x2 = x1+div.offsetWidth;
            var y1 = parseInt(div.style.top);
            var y2 = y1+div.offsetHeight;

            if (x<x1 || x>x2){
                me.hide();
            }
            else{
                if (y<y1 || y>y2){
                    me.hide();
                }
            }
        }
    };


  //**************************************************************************
  //** getInnerDiv
  //**************************************************************************
  /** Returns the content div inside the callout that can be populated with
   *  text, html, menu buttons, etc.
   */
    this.getInnerDiv = function(){
        return innerDiv;
    };


  //**************************************************************************
  //** getSize
  //**************************************************************************
  /** Returns the width and height of the callout.
   */
    this.getSize = function(){
        var size;

        if (div.style.display === 'none'){
            div.style.visibility = 'hidden';
            div.style.display = 'block';
            size = {
                width: div.offsetWidth,
                height: div.offsetHeight
            };
            div.style.visibility = '';
            div.style.display = 'none';
        }
        else{
            size = {
                width: div.offsetWidth,
                height: div.offsetHeight
            };
        }
        return size;
    };


  //**************************************************************************
  //** show
  //**************************************************************************
  /** Used to render the callout.
   */
    this.show = function(){
        opening = true;

        div.style.zIndex = getNextHighestZindex();
        div.style.display = 'block';
        me.onShow();

        setTimeout(function() {
            opening = false;
        }, 500);
    };


  //**************************************************************************
  //** showAt
  //**************************************************************************
  /** Used to render the callout at a specific coordinate. The tip of the
   *  arrow associated with the callout will appear at the given coordinate.
   *
   *  @param position Where to place the callout box relative to the given
   *  coordinate. Options include left, right, above, and below.
   *
   *  @param align Options include left, right, center if the "position" is
   *  above or below. Otherwise, options are top, bottom, or middle.
   */
    this.showAt = function(x, y, position, align){
        opening = true;


      //Hack to get div width/height BEFORE making the div visible
        div.style.visibility = 'hidden';
        div.style.display = 'block';


        var backgroundColor = config.arrow.backgroundColor;
        var borderColor = config.arrow.borderColor;


        var notchSize = Math.max(config.arrow.width, config.arrow.height);
        var notchOffset = 0;
        var notchCenter = notchSize;
        var notchHeight = notchSize;



        var halign = function(){
            if (align==="left"){
                notchOffset = config.arrow.paddingLeft;
                div.style.left = (x-(notchOffset+notchCenter)) + "px";
                notch.style.left=notchBorder.style.left=notchOffset + "px";
            }
            else if (align==="right"){
                notchOffset = config.arrow.paddingRight;
                div.style.left = ((x-div.offsetWidth) + (notchOffset+notchCenter)) + "px";
                notch.style.left=notchBorder.style.left= (div.offsetWidth-(notchOffset+(notchCenter*2))) + "px";
            }
            else if (align==="center" || align==="middle"){
                var center = div.offsetWidth/2;
                div.style.left = (x-center) + "px";
                notch.style.left=notchBorder.style.left= (center-notchCenter) + "px";
            }
            else{
                return;
            }
        };


        var valign = function(){
            callout.style.top = "0px";

            if (align==="top"){
                notchOffset = config.arrow.paddingTop;
                div.style.top = (y-(notchOffset+notchCenter)) + "px";
                notch.style.top=notchBorder.style.top=notchOffset + "px";
            }
            else if (align==="bottom"){
                notchOffset = config.arrow.paddingBottom;
                div.style.top = ((y-div.offsetWidth) + (notchOffset+notchCenter)) + "px";
                notch.style.top = notchBorder.style.top = (div.offsetHeight-(notchOffset+(notchCenter*2))) + "px";
            }
            else if (align==="middle" || align==="center"){
                var center = div.offsetHeight/2;
                div.style.top = (y-center) + "px";
                notch.style.top = notchBorder.style.top = (center-notchCenter) + "px";
            }
            else{
                return;
            }
        };


      //Update notch style align elements. Notch style is based on a CSS triangle
      //described here: https://css-tricks.com/snippets/css/css-triangle/
        if (position==="above"){

          //Update notch style so the arrow is pointing down
            notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid " + backgroundColor;
            notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent";
            notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent";
            notch.style.borderBottom=notchBorder.style.borderBottom=0;
            notchBorder.style.borderTopColor=borderColor; //<--Make sure this appears after all other border definitions!


          //Set vertical position of the notch, div, and callout
            div.style.left = x + "px";
            div.style.top = ((y-div.offsetHeight)-notchHeight) + "px";
            callout.style.top = "0px";
            notch.style.top = (div.offsetHeight-1) + "px"; //-1 for the border width
            notchBorder.style.top = div.offsetHeight + "px";


          //Set horizontal alignment of the notch and div
            halign();
        }
        else if (position==="below"){

          //Update notch style so the arrow is pointing up
            notch.style.borderTop=notchBorder.style.borderTop=0;
            notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent";
            notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent";
            notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid " + backgroundColor;
            notchBorder.style.borderBottomColor=borderColor; //<--Make sure this appears after all other border definitions!


          //Set vertical position of the notch, div, and callout
            div.style.left = x + "px";
            div.style.top = y + "px";
            callout.style.top = notchHeight + "px";
            notch.style.top = "1px"; //+1 for border width
            notchBorder.style.top = "0px";


          //Set horizontal position of the notch and div
            halign();
        }
        else if (position==="left"){

          //Update notch style so the arrow is pointing right
            notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent";
            notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid " + backgroundColor;
            notch.style.borderRight=notchBorder.style.borderRight=0;
            notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent";
            notchBorder.style.borderLeftColor=borderColor; //<--Make sure this appears after all other border definitions!


          //Set horizontal position
            div.style.left = (x-(div.offsetWidth+notchHeight)) + "px";
            callout.style.left = "0px";
            notch.style.left = (div.offsetWidth-1) + "px";
            notchBorder.style.left = div.offsetWidth + "px";


          //Set vertical position of the notch and div
            valign();
        }
        else if (position==="right"){


          //Update notch style so the arrow is pointing left
            notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent";
            notch.style.borderLeft=notchBorder.style.borderLeft=0;
            notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid " + backgroundColor;
            notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent";
            notchBorder.style.borderRightColor=borderColor; //<--Make sure this appears after all other border definitions!


          //Set horizontal position
            div.style.left = x + "px";
            callout.style.left = notchHeight + "px";
            notch.style.left = "1px";
            notchBorder.style.left = "0px";


          //Set vertical position of the notch and div
            valign();
        }
        else{
            return;
        }


        div.style.visibility = "";

        me.show();
    };


  //**************************************************************************
  //** hide
  //**************************************************************************
  /** Used to hide the callout.
   */
    this.hide = function(){
        div.style.display = 'none';
        opening = false;
        me.onHide();
    };


  //**************************************************************************
  //** isVisible
  //**************************************************************************
  /** Returns true of the callout is visible.
   */
    this.isVisible = function(){
        return (div.style.display !== 'none');
    };


  //**************************************************************************
  //** onShow
  //**************************************************************************
  /** Called whenever the callout is made visible.
   */
    this.onShow = function(){};


  //**************************************************************************
  //** onHide
  //**************************************************************************
  /** Called whenever the callout is hidden.
   */
    this.onHide = function(){};



  //**************************************************************************
  //** Utils
  //**************************************************************************
    var merge = javaxt.dhtml.utils.merge;
    var getNextHighestZindex = javaxt.dhtml.utils.getNextHighestZindex;
    var createElement = javaxt.dhtml.utils.createElement;


    init();
};