Source: BoundPanel.js

/**
 * A bound panel is a panel that is shown at a certain place
 * @constructor
 * @param {Object} options The options
 * @param {Node} options.element The root element
 * @param {string} options.name The component name
 */
hui.ui.BoundPanel = function(options) {
  this.options = options;
  this.element = hui.get(options.element);
  this.name = options.name;
  this.visible = false;
  this.content = hui.get.firstByClass(this.element, 'hui_boundpanel_content');
  this.arrow = hui.get.firstByClass(this.element, 'hui_boundpanel_arrow');
  this.arrowWide = 37;
  this.arrowNarrow = 18;
  if (options.variant == 'light') {
    this.arrowWide = 23;
    this.arrowNarrow = 12;
  }
  hui.ui.extend(this);
};

/**
 * Creates a new bound panel
 *
 * @param {Object} options The options
 * @param {String} options.name The component name
 * @param {String} options.variant A visual variation
 * @param {Number} options.left Pixels from left
 * @param {Number} options.top Pixels from top
 * @param {Number} options.width Width in pixels
 * @param {Number} options.padding Padding in pixels
 */
hui.ui.BoundPanel.create = function(options) {
  options = hui.override({
    name: null,
    top: 0,
    left: 0,
    width: null,
    padding: null,
    modal: false,
    hideOnClick: false
  }, options);


  var html =
    '<div class="hui_boundpanel_arrow"></div>' +
  '<div class="hui_boundpanel_content" style="';
  if (options.width) {
    html += 'width:' + options.width + 'px;';
  }
  if (options.padding) {
    html += 'padding:' + options.padding + 'px;';
  }
  html += '"></div>';

  options.element = hui.build(
    'div', {
      'class': options.variant ? 'hui_boundpanel hui_boundpanel-' + options.variant : 'hui_boundpanel',
      style: 'display:none;zIndex:' + hui.ui.nextPanelIndex() + ';top:' + options.top + 'px;left:' + options.left + 'px',
      html: html,
      parent: document.body
    }
  );
  return new hui.ui.BoundPanel(options);
};


hui.ui.BoundPanel.prototype = {
  /**
   * Show or hide the panel
   */
  toggle: function() {
    if (!this.visible) {
      this.show();
    } else {
      this.hide();
    }
  },
  /** Shows the panel */
  show: function(options) {
    options = options || {};
    var target = options.target || this.options.target;

    if (target) {
      if (target.nodeName) {
        this.position(target);
      } else {
        this.position(hui.ui.get(target));
      }
    }
    if (this.visible) {
      this.element.style.zIndex = hui.ui.nextPanelIndex();
      return;
    }
    if (hui.browser.opacity) {
      hui.style.setOpacity(this.element, 0);
    }
    var vert;
    if (this.relativePosition == 'left') {
      vert = false;
      this.element.style.marginLeft = '20px';
    } else if (this.relativePosition == 'right') {
      vert = false;
      this.element.style.marginLeft = '-20px';
    } else if (this.relativePosition == 'top') {
      vert = true;
      this.element.style.marginTop = '20px';
    } else if (this.relativePosition == 'bottom') {
      vert = true;
      this.element.style.marginTop = '-20px';
    }
    this.element.style.visibility = 'visible';
    this.element.style.display = 'block';
    var index = hui.ui.nextPanelIndex();
    this.element.style.zIndex = index;
    hui.ui.callVisible(this);
    if (hui.browser.opacity) {
      hui.animate(this.element, 'opacity', 1, 300, {
        ease: hui.ease.fastSlow
      });
    }
    hui.animate(this.element, vert ? 'margin-top' : 'margin-left', '0px', 300, {
      ease: hui.ease.fastSlow
    });
    this.visible = true;
    if (this.options.modal) {
      hui.ui.showCurtain({
        widget: this,
        zIndex: index - 1,
        transparent: this.options.modal == 'transparent',
        color: 'auto'
      });
    }
    if (this.options.hideOnClick) {
      this.hideListener = hui.listen(document.body, 'click', function(e) {
        if (!hui.ui.isWithin(e, this.element)) {
          this.hide();
        }
      }.bind(this));
    }
  },
  /** @private */
  $curtainWasClicked: function() {
    hui.ui.hideCurtain(this);
    this.hide();
  },
  /** Hides the panel */
  hide: function() {
    if (!this.visible) {
      return;
    }
    if (!hui.browser.opacity) {
      this.element.style.display = 'none';
      hui.ui.callVisible(this);
    } else {
      hui.animate(this.element, 'opacity', 0, 100, {
        ease: hui.ease.slowFast,
        $complete: function() {
          this.element.style.display = 'none';
          hui.ui.callVisible(this);
        }.bind(this)
      });
    }
    if (this.options.modal) {
      hui.ui.hideCurtain(this);
    }
    this.visible = false;
    hui.unListen(document.body, 'click', this.hideListener);
  },
  /**
   * If the panel is currently visible
   */
  isVisible: function() {
    return this.visible;
  },
  /**
   * Adds a widget or element to the panel
   * @param {Node | Widget} child The object to add
   */
  add: function(child) {
    if (child.getElement) {
      this.content.appendChild(child.getElement());
    } else {
      this.content.appendChild(child);
    }
  },
  clear: function() {
    hui.ui.destroyDescendants(this.content);
    this.content.innerHTML = '';
  },
  /**
   * Adds som vertical space to the panel
   * @param {pixels} height The height of the space in pixels
   */
  addSpace: function(height) {
    this.add(hui.build('div', {
      style: 'font-size:0px;height:' + height + 'px'
    }));
  },
  _getDimensions: function() {
    var width, height;
    if (this.element.style.display == 'none') {
      this.element.style.visibility = 'hidden';
      this.element.style.display = 'block';
      width = this.element.clientWidth;
      height = this.element.clientHeight;
      this.element.style.display = 'none';
      this.element.style.visibility = '';
    } else {
      width = this.element.clientWidth;
      height = this.element.clientHeight;
    }
    return {
      width: width,
      height: height
    };
  },
  $$childSizeChanged: function() {
    this._rePosition();
  },
  $$layout: function() {
    this._rePosition();
  },
  _rePosition: function() {
    if (this._latest) {
      this.position(this._latest);
    }
  },
  /** Position the panel at a node
   * @param {Node} node The node the panel should be positioned at
   */
  position: function(options) {
    this._latest = options;
    var node,
      position,
      nodeOffset,
      nodeScrollOffset;
    if (options.getElement) {
      node = options.getElement();
    } else if (options.element) {
      node = options.element;
      position = options.position;
    } else if (options.rect) {
      position = options.position;
      node = {
        offsetWidth: options.rect.width,
        offsetHeight: options.rect.height
      };
      nodeOffset = {
        left: options.rect.left,
        top: options.rect.top
      };
      nodeScrollOffset = {
        left: 0,
        top: 0
      };
    } else {
      node = hui.get(options);
    }

    if (!nodeOffset) {
      nodeOffset = {
        left: hui.position.getLeft(node),
        top: hui.position.getTop(node)
      };
    }
    if (!nodeScrollOffset) {
      nodeScrollOffset = hui.position.getScrollOffset(node);
    }

    var windowScrollOffset = {
      left: hui.window.getScrollLeft(),
      top: hui.window.getScrollTop()
    };
    var nodeLeft = nodeOffset.left - windowScrollOffset.left + hui.window.getScrollLeft();
    var nodeWidth = node.clientWidth || node.offsetWidth;
    var nodeHeight = node.clientHeight || node.offsetHeight;

    var panelDimensions = this._getDimensions();
    var viewportWidth = document.body.clientWidth;
    var viewportHeight = hui.window.getViewHeight();

    var arrowLeft, arrowTop, left, top;
    var positionOnScreen = {
      top: nodeOffset.top - windowScrollOffset.top - (nodeScrollOffset.top - windowScrollOffset.top)
    };
    var vertical = positionOnScreen.top / viewportHeight;

    if (position == 'vertical') {
      vertical = vertical > 0.5 ? 0.9 : 0.1;
    }
    var min, max;
    if (vertical <= 0.1) {
      this.relativePosition = 'top';
      this.arrow.className = 'hui_boundpanel_arrow hui_boundpanel_arrow_top';
      if (this.options.variant == 'light') {
        arrowTop = this.arrowNarrow * -1;
      } else {
        arrowTop = this.arrowNarrow * -1;
      }
      left = Math.min(viewportWidth - panelDimensions.width - 2, Math.max(3, nodeLeft + (nodeWidth / 2) - ((panelDimensions.width) / 2)));
      arrowLeft = (nodeLeft + nodeWidth / 2) - left - this.arrowNarrow;
      top = nodeOffset.top + nodeHeight + 8 - (nodeScrollOffset.top - windowScrollOffset.top);
    } else if (vertical >= 0.9) {
      this.relativePosition = 'bottom';
      this.arrow.className = 'hui_boundpanel_arrow hui_boundpanel_arrow_bottom';
      if (this.options.variant == 'light') {
        arrowTop = panelDimensions.height - 1;
      } else {
        arrowTop = panelDimensions.height;
      }
      left = Math.min(viewportWidth - panelDimensions.width - 3, Math.max(3, nodeLeft + (nodeWidth / 2) - ((panelDimensions.width) / 2)));
      arrowLeft = (nodeLeft + nodeWidth / 2) - left - this.arrowNarrow;
      top = nodeOffset.top - panelDimensions.height - 5 - (nodeScrollOffset.top - windowScrollOffset.top);
    } else if ((nodeLeft + nodeWidth / 2) / viewportWidth < 0.5) {
      this.relativePosition = 'left';
      left = nodeLeft + nodeWidth + 10;
      this.arrow.className = 'hui_boundpanel_arrow hui_boundpanel_arrow_left';
      top = nodeOffset.top + (nodeHeight - panelDimensions.height) / 2;
      //top = Math.min(top,viewportHeight-panelDimensions.height+(windowScrollOffset.top+nodeScrollOffset.top));
      top -= (nodeScrollOffset.top - windowScrollOffset.top);
      min = windowScrollOffset.top + 3;
      max = windowScrollOffset.top + (viewportHeight - panelDimensions.height) - 3;
      top = Math.min(Math.max(top, min), max);
      arrowTop = nodeOffset.top - top;
      arrowTop -= (nodeScrollOffset.top - windowScrollOffset.top);
      arrowTop -= this.arrowWide / 2;
      arrowTop += nodeHeight / 2;
      if (this.options.variant == 'light') {
        arrowLeft = -12;
        arrowTop += 2;
      } else {
        arrowLeft = -18;
      }
    } else {
      this.relativePosition = 'right';
      left = nodeLeft - panelDimensions.width - 10;
      this.arrow.className = 'hui_boundpanel_arrow hui_boundpanel_arrow_right';
      top = nodeOffset.top + (nodeHeight - panelDimensions.height) / 2;
      //top = Math.min(top,viewportHeight-panelDimensions.height+(windowScrollOffset.top+nodeScrollOffset.top));
      top -= (nodeScrollOffset.top - windowScrollOffset.top);
      min = windowScrollOffset.top + 3;
      max = windowScrollOffset.top + (viewportHeight - panelDimensions.height) - 3;
      top = Math.min(Math.max(top, min), max);
      arrowTop = nodeOffset.top - top;
      arrowTop -= (nodeScrollOffset.top - windowScrollOffset.top);
      arrowTop -= this.arrowWide / 2;
      arrowTop += nodeHeight / 2;
      if (this.options.variant == 'light') {
        arrowLeft = panelDimensions.width;
        arrowTop += 2;
      } else {
        arrowLeft = panelDimensions.width;
      }
    }
    this.arrow.style.marginTop = arrowTop + 'px';
    this.arrow.style.marginLeft = arrowLeft + 'px';
    if (this.visible) {
      hui.animate(this.element, 'top', top + 'px', 500, {
        ease: hui.ease.fastSlow
      });
      hui.animate(this.element, 'left', left + 'px', 500, {
        ease: hui.ease.fastSlow
      });
    } else {
      this.element.style.top = top + 'px';
      this.element.style.left = left + 'px';
    }
  },
  detach: function() {
    hui.ui.hideCurtain(this);
  }
};