/** * @version 1.1 * @author Nazar Lazorko * @requires jQuery * @see jQuery.fn.dynamicTip#defaultOptions */ ;(function ($) { "use strict"; /** * @param {Object} options * @return jQuery */ $.fn.dynamicTip = function (options) { /** @private */ this.dynamicTip = (function (plugin, options) { var facade = { show: function () { }, hide: function () { dynamicTip.hideTip(); } }; var dynamicTip = new DynamicTip(facade, plugin, options); return facade; })(this, options); return this; }; var eventNS = 'dynamicTip'; var defaultOptions = { /** * @type {Function} Callback for tip content loading * * @param {Object} tip Tip public methods * @param {HTMLElement} node Active node * @param {Function} callback Callback for tip content updating (function (html) {}) * @return {String|*} Returned string will be immediately shown on tip, otherwise tip will be canceled */ source: undefined, showDelay: 100, hideDelay: 100, alignTo: 'target', // target | cursor alignX: 'right', // right | center | left | inner-left | inner-right ('inner-*' used with alignTo: 'target') alignY: 'top', // bottom | center | top | inner-bottom | inner-top ('inner-*' used with alignTo: 'target') offsetX: 0, offsetY: 0, tipClass: 'dynamic_tip', tipCss: { position: 'absolute', zIndex: 2048 } }; var DynamicTip = function (facade, plugin, options) { this.init.apply(this, arguments); }; DynamicTip.prototype = { /** * * @param {Object} facade * @param {jQuery} plugin * @param {Object} options * @return {DynamicTip} */ init: function (facade, plugin, options) { this.facade = facade; this.options = $.extend(true, {}, defaultOptions, options); this.tip = { /** @type {HTMLElement} Active node */ node: undefined, /** @type {jQuery} Tip container */ container: this.createTipContainer(), pageX: undefined, pageY: undefined, needHide: false }; var that = this; plugin.each(function () { var target = $(this); target.on('mouseleave.' + eventNS, function () { that.onNodeMouseLeave.apply(that, arguments); }); target.on('mouseenter.' + eventNS, function () { that.onNodeMouseEnter.apply(that, arguments); }); }); return this; }, createTipContainer: function () { var that = this; return $(document.createElement('div')) .addClass(this.options.tipClass) .css(this.options.tipCss) .on('mouseleave.' + eventNS, function () { that.onTipMouseLeave.apply(that, arguments); }) .on('mouseenter.' + eventNS, function () { that.onTipMouseEnter.apply(that, arguments); }) .appendTo(document.body) .hide(); }, onTipMouseEnter: function (e) { this.tip.needHide = false; return this; }, onTipMouseLeave: function (e) { var that = this; this.tip.needHide = true; setTimeout(function () { that.tipHideCheck(); }, this.options.hideDelay); return this; }, tipHideCheck: function () { if (this.tip.needHide) { this.tip.needHide = false; this.tip.node = null; this.hideTip(); } return this; }, /** * * @param {String|HTMLElement|jQuery|undefined} content * @return {*} */ showTip: function (content) { if (null == this.tip.node) { return this; } var container = this.tip.container.empty(); if (null == content || false === content) { container.hide(); return this; } if (content && (content instanceof jQuery || content.nodeType == 1)) { container.append(content); } else { container.html(content); } var position = this.calculatePos(); container.css({left: position.l, top: position.t}).show(); return this; }, hideTip: function () { if (this.tip.container.is(':visible')) { this.tip.container.hide().empty(); } return this; }, calculatePos: function () { var result = { l: 0, t: 0, arrow: '' }, outerW = this.tip.container.outerWidth(), outerH = this.tip.container.outerHeight(), $wnd = $(window), $node = $(this.tip.node), view = { l: $wnd.scrollLeft(), t: $wnd.scrollTop(), w: $wnd.width(), h: $wnd.height() }, xL, xC, xR, yT, yC, yB; if ('cursor' === this.options.alignTo) { xL = xC = xR = this.tip.pageX; yT = yC = yB = this.tip.pageY; } else if ('target' === this.options.alignTo) { var offset = $node.offset(), nodePos = { l: offset.left, t: offset.top, w: $node.outerWidth(), h: $node.outerHeight() }; xL = nodePos.l + ('inner-right' !== this.options.alignX ? 0 : nodePos.w); // left edge xC = xL + Math.floor(nodePos.w / 2); // h center xR = xL + ('inner-left' !== this.options.alignX ? nodePos.w : 0); // right edge yT = nodePos.t + ('inner-bottom' !== this.options.alignY ? 0 : nodePos.h); // top edge yC = yT + Math.floor(nodePos.h / 2); // v center yB = yT + ('inner-top' !== this.options.alignY ? nodePos.h : 0); // bottom edge } else { throw new Error('Wrong alignTo value "' + this.options.alignTo + '"'); } // keep in view port and calc arrow position switch (this.options.alignX) { case 'right': case 'inner-left': result.l = xR + this.options.offsetX; if (result.l + outerW > view.l + view.w) result.l = view.l + view.w - outerW; if (this.options.alignX == 'right' || this.options.alignY == 'center') result.arrow = 'left'; break; case 'center': result.l = xC - Math.floor(outerW / 2); if (result.l + outerW > view.l + view.w) result.l = view.l + view.w - outerW; else if (result.l < view.l) result.l = view.l; break; default: // 'left' || 'inner-right' result.l = xL - outerW - this.options.offsetX; if (result.l < view.l) result.l = view.l; if (this.options.alignX == 'left' || this.options.alignY == 'center') result.arrow = 'right'; } switch (this.options.alignY) { case 'bottom': case 'inner-top': result.t = yB + this.options.offsetY; // 'left' and 'right' need priority for 'target' if (!result.arrow || this.options.alignTo == 'cursor') result.arrow = 'top'; if (result.t + outerH > view.t + view.h) { result.t = yT - outerH - this.options.offsetY; if (result.arrow == 'top') result.arrow = 'bottom'; } break; case 'center': result.t = yC - Math.floor(outerH / 2); if (result.t + outerH > view.t + view.h) result.t = view.t + view.h - outerH; else if (result.t < view.t) result.t = view.t; break; default: // 'top' || 'inner-bottom' result.t = yT - outerH - this.options.offsetY; // 'left' and 'right' need priority for 'target' if (!result.arrow || this.options.alignTo == 'cursor') result.arrow = 'bottom'; if (result.t < view.t) { result.t = yB + this.options.offsetY; if (result.arrow == 'bottom') result.arrow = 'top'; } } return result; }, onNodeMouseEnter: function (e) { var that = this, node = e.currentTarget; this.tip.node = node; this.tip.pageX = e.pageX; this.tip.pageY = e.pageY; this.tip.needHide = false; setTimeout(function () { that.checkMouseEnterNode(node); }, this.options.showDelay); return true; }, onNodeMouseLeave: function (e) { var that = this; this.tip.node = e.currentTarget; this.tip.needHide = true; setTimeout(function () { that.tipHideCheck(); }, this.options.hideDelay); return true; }, checkMouseEnterNode: function (node) { var that = this, content; if (this.tip.node === node) { this.hideTip(); if ($.isFunction(this.options.source)) { content = this.options.source.call(null, this.facade, node, function (content) { if (that.tip.node === node) { that.showTip(content); } }); } else { content = this.options.source; } if (null != content && false !== content) { this.showTip(content); } } return this; } }; })(jQuery);