UNPKG

qtip2

Version:

Introducing... qTip2. The second generation of the advanced qTip plugin for the ever popular jQuery framework.

637 lines (524 loc) 19.1 kB
var TIP, createVML, SCALE, PIXEL_RATIO, BACKING_STORE_RATIO, // Common CSS strings MARGIN = 'margin', BORDER = 'border', COLOR = 'color', BG_COLOR = 'background-color', TRANSPARENT = 'transparent', IMPORTANT = ' !important', // Check if the browser supports <canvas/> elements HASCANVAS = !!document.createElement('canvas').getContext, // Invalid colour values used in parseColours() INVALID = /rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i; // Camel-case method, taken from jQuery source // http://code.jquery.com/jquery-1.8.0.js function camel(s) { return s.charAt(0).toUpperCase() + s.slice(1); } /* * Modified from Modernizr's testPropsAll() * http://modernizr.com/downloads/modernizr-latest.js */ var cssProps = {}, cssPrefixes = ['Webkit', 'O', 'Moz', 'ms']; function vendorCss(elem, prop) { var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), props = (prop + ' ' + cssPrefixes.join(ucProp + ' ') + ucProp).split(' '), cur, val, i = 0; // If the property has already been mapped... if(cssProps[prop]) { return elem.css(cssProps[prop]); } while(cur = props[i++]) { if((val = elem.css(cur)) !== undefined) { cssProps[prop] = cur; return val; } } } // Parse a given elements CSS property into an int function intCss(elem, prop) { return Math.ceil(parseFloat(vendorCss(elem, prop))); } // VML creation (for IE only) if(!HASCANVAS) { createVML = function(tag, props, style) { return '<qtipvml:'+tag+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(props||'')+ ' style="behavior: url(#default#VML); '+(style||'')+ '" />'; }; } // Canvas only definitions else { PIXEL_RATIO = window.devicePixelRatio || 1; BACKING_STORE_RATIO = (function() { var context = document.createElement('canvas').getContext('2d'); return context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || 1; })(); SCALE = PIXEL_RATIO / BACKING_STORE_RATIO; } function Tip(qtip, options) { this._ns = 'tip'; this.options = options; this.offset = options.offset; this.size = [ options.width, options.height ]; // Initialize this.qtip = qtip; this.init(qtip); } $.extend(Tip.prototype, { init: function(qtip) { var context, tip; // Create tip element and prepend to the tooltip tip = this.element = qtip.elements.tip = $('<div />', { 'class': NAMESPACE+'-tip' }).prependTo(qtip.tooltip); // Create tip drawing element(s) if(HASCANVAS) { // save() as soon as we create the canvas element so FF2 doesn't bork on our first restore()! context = $('<canvas />').appendTo(this.element)[0].getContext('2d'); // Setup constant parameters context.lineJoin = 'miter'; context.miterLimit = 100000; context.save(); } else { context = createVML('shape', 'coordorigin="0,0"', 'position:absolute;'); this.element.html(context + context); // Prevent mousing down on the tip since it causes problems with .live() handling in IE due to VML qtip._bind( $('*', tip).add(tip), ['click', 'mousedown'], function(event) { event.stopPropagation(); }, this._ns); } // Bind update events qtip._bind(qtip.tooltip, 'tooltipmove', this.reposition, this._ns, this); // Create it this.create(); }, _swapDimensions: function() { this.size[0] = this.options.height; this.size[1] = this.options.width; }, _resetDimensions: function() { this.size[0] = this.options.width; this.size[1] = this.options.height; }, _useTitle: function(corner) { var titlebar = this.qtip.elements.titlebar; return titlebar && ( corner.y === TOP || corner.y === CENTER && this.element.position().top + this.size[1] / 2 + this.options.offset < titlebar.outerHeight(TRUE) ); }, _parseCorner: function(corner) { var my = this.qtip.options.position.my; // Detect corner and mimic properties if(corner === FALSE || my === FALSE) { corner = FALSE; } else if(corner === TRUE) { corner = new CORNER( my.string() ); } else if(!corner.string) { corner = new CORNER(corner); corner.fixed = TRUE; } return corner; }, _parseWidth: function(corner, side, use) { var elements = this.qtip.elements, prop = BORDER + camel(side) + 'Width'; return (use ? intCss(use, prop) : intCss(elements.content, prop) || intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) || intCss(elements.tooltip, prop) ) || 0; }, _parseRadius: function(corner) { var elements = this.qtip.elements, prop = BORDER + camel(corner.y) + camel(corner.x) + 'Radius'; return BROWSER.ie < 9 ? 0 : intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) || intCss(elements.tooltip, prop) || 0; }, _invalidColour: function(elem, prop, compare) { var val = elem.css(prop); return !val || compare && val === elem.css(compare) || INVALID.test(val) ? FALSE : val; }, _parseColours: function(corner) { var elements = this.qtip.elements, tip = this.element.css('cssText', ''), borderSide = BORDER + camel(corner[ corner.precedance ]) + camel(COLOR), colorElem = this._useTitle(corner) && elements.titlebar || elements.content, css = this._invalidColour, color = []; // Attempt to detect the background colour from various elements, left-to-right precedance color[0] = css(tip, BG_COLOR) || css(colorElem, BG_COLOR) || css(elements.content, BG_COLOR) || css(elements.tooltip, BG_COLOR) || tip.css(BG_COLOR); // Attempt to detect the correct border side colour from various elements, left-to-right precedance color[1] = css(tip, borderSide, COLOR) || css(colorElem, borderSide, COLOR) || css(elements.content, borderSide, COLOR) || css(elements.tooltip, borderSide, COLOR) || elements.tooltip.css(borderSide); // Reset background and border colours $('*', tip).add(tip).css('cssText', BG_COLOR+':'+TRANSPARENT+IMPORTANT+';'+BORDER+':0'+IMPORTANT+';'); return color; }, _calculateSize: function(corner) { var y = corner.precedance === Y, width = this.options.width, height = this.options.height, isCenter = corner.abbrev() === 'c', base = (y ? width: height) * (isCenter ? 0.5 : 1), pow = Math.pow, round = Math.round, bigHyp, ratio, result, smallHyp = Math.sqrt( pow(base, 2) + pow(height, 2) ), hyp = [ this.border / base * smallHyp, this.border / height * smallHyp ]; hyp[2] = Math.sqrt( pow(hyp[0], 2) - pow(this.border, 2) ); hyp[3] = Math.sqrt( pow(hyp[1], 2) - pow(this.border, 2) ); bigHyp = smallHyp + hyp[2] + hyp[3] + (isCenter ? 0 : hyp[0]); ratio = bigHyp / smallHyp; result = [ round(ratio * width), round(ratio * height) ]; return y ? result : result.reverse(); }, // Tip coordinates calculator _calculateTip: function(corner, size, scale) { scale = scale || 1; size = size || this.size; var width = size[0] * scale, height = size[1] * scale, width2 = Math.ceil(width / 2), height2 = Math.ceil(height / 2), // Define tip coordinates in terms of height and width values tips = { br: [0,0, width,height, width,0], bl: [0,0, width,0, 0,height], tr: [0,height, width,0, width,height], tl: [0,0, 0,height, width,height], tc: [0,height, width2,0, width,height], bc: [0,0, width,0, width2,height], rc: [0,0, width,height2, 0,height], lc: [width,0, width,height, 0,height2] }; // Set common side shapes tips.lt = tips.br; tips.rt = tips.bl; tips.lb = tips.tr; tips.rb = tips.tl; return tips[ corner.abbrev() ]; }, // Tip coordinates drawer (canvas) _drawCoords: function(context, coords) { context.beginPath(); context.moveTo(coords[0], coords[1]); context.lineTo(coords[2], coords[3]); context.lineTo(coords[4], coords[5]); context.closePath(); }, create: function() { // Determine tip corner var c = this.corner = (HASCANVAS || BROWSER.ie) && this._parseCorner(this.options.corner); // If we have a tip corner... this.enabled = !!this.corner && this.corner.abbrev() !== 'c'; if(this.enabled) { // Cache it this.qtip.cache.corner = c.clone(); // Create it this.update(); } // Toggle tip element this.element.toggle(this.enabled); return this.corner; }, update: function(corner, position) { if(!this.enabled) { return this; } var elements = this.qtip.elements, tip = this.element, inner = tip.children(), options = this.options, curSize = this.size, mimic = options.mimic, round = Math.round, color, precedance, context, coords, bigCoords, translate, newSize, border; // Re-determine tip if not already set if(!corner) { corner = this.qtip.cache.corner || this.corner; } // Use corner property if we detect an invalid mimic value if(mimic === FALSE) { mimic = corner; } // Otherwise inherit mimic properties from the corner object as necessary else { mimic = new CORNER(mimic); mimic.precedance = corner.precedance; if(mimic.x === 'inherit') { mimic.x = corner.x; } else if(mimic.y === 'inherit') { mimic.y = corner.y; } else if(mimic.x === mimic.y) { mimic[ corner.precedance ] = corner[ corner.precedance ]; } } precedance = mimic.precedance; // Ensure the tip width.height are relative to the tip position if(corner.precedance === X) { this._swapDimensions(); } else { this._resetDimensions(); } // Update our colours color = this.color = this._parseColours(corner); // Detect border width, taking into account colours if(color[1] !== TRANSPARENT) { // Grab border width border = this.border = this._parseWidth(corner, corner[corner.precedance]); // If border width isn't zero, use border color as fill if it's not invalid (1.0 style tips) if(options.border && border < 1 && !INVALID.test(color[1])) { color[0] = color[1]; } // Set border width (use detected border width if options.border is true) this.border = border = options.border !== TRUE ? options.border : border; } // Border colour was invalid, set border to zero else { this.border = border = 0; } // Determine tip size newSize = this.size = this._calculateSize(corner); tip.css({ width: newSize[0], height: newSize[1], lineHeight: newSize[1]+'px' }); // Calculate tip translation if(corner.precedance === Y) { translate = [ round(mimic.x === LEFT ? border : mimic.x === RIGHT ? newSize[0] - curSize[0] - border : (newSize[0] - curSize[0]) / 2), round(mimic.y === TOP ? newSize[1] - curSize[1] : 0) ]; } else { translate = [ round(mimic.x === LEFT ? newSize[0] - curSize[0] : 0), round(mimic.y === TOP ? border : mimic.y === BOTTOM ? newSize[1] - curSize[1] - border : (newSize[1] - curSize[1]) / 2) ]; } // Canvas drawing implementation if(HASCANVAS) { // Grab canvas context and clear/save it context = inner[0].getContext('2d'); context.restore(); context.save(); context.clearRect(0,0,6000,6000); // Calculate coordinates coords = this._calculateTip(mimic, curSize, SCALE); bigCoords = this._calculateTip(mimic, this.size, SCALE); // Set the canvas size using calculated size inner.attr(WIDTH, newSize[0] * SCALE).attr(HEIGHT, newSize[1] * SCALE); inner.css(WIDTH, newSize[0]).css(HEIGHT, newSize[1]); // Draw the outer-stroke tip this._drawCoords(context, bigCoords); context.fillStyle = color[1]; context.fill(); // Draw the actual tip context.translate(translate[0] * SCALE, translate[1] * SCALE); this._drawCoords(context, coords); context.fillStyle = color[0]; context.fill(); } // VML (IE Proprietary implementation) else { // Calculate coordinates coords = this._calculateTip(mimic); // Setup coordinates string coords = 'm' + coords[0] + ',' + coords[1] + ' l' + coords[2] + ',' + coords[3] + ' ' + coords[4] + ',' + coords[5] + ' xe'; // Setup VML-specific offset for pixel-perfection translate[2] = border && /^(r|b)/i.test(corner.string()) ? BROWSER.ie === 8 ? 2 : 1 : 0; // Set initial CSS inner.css({ coordsize: newSize[0]+border + ' ' + newSize[1]+border, antialias: ''+(mimic.string().indexOf(CENTER) > -1), left: translate[0] - translate[2] * Number(precedance === X), top: translate[1] - translate[2] * Number(precedance === Y), width: newSize[0] + border, height: newSize[1] + border }) .each(function(i) { var $this = $(this); // Set shape specific attributes $this[ $this.prop ? 'prop' : 'attr' ]({ coordsize: newSize[0]+border + ' ' + newSize[1]+border, path: coords, fillcolor: color[0], filled: !!i, stroked: !i }) .toggle(!!(border || i)); // Check if border is enabled and add stroke element !i && $this.html( createVML( 'stroke', 'weight="'+border*2+'px" color="'+color[1]+'" miterlimit="1000" joinstyle="miter"' ) ); }); } // Opera bug #357 - Incorrect tip position // https://github.com/Craga89/qTip2/issues/367 window.opera && setTimeout(function() { elements.tip.css({ display: 'inline-block', visibility: 'visible' }); }, 1); // Position if needed if(position !== FALSE) { this.calculate(corner, newSize); } }, calculate: function(corner, size) { if(!this.enabled) { return FALSE; } var self = this, elements = this.qtip.elements, tip = this.element, userOffset = this.options.offset, position = {}, precedance, corners; // Inherit corner if not provided corner = corner || this.corner; precedance = corner.precedance; // Determine which tip dimension to use for adjustment size = size || this._calculateSize(corner); // Setup corners and offset array corners = [ corner.x, corner.y ]; if(precedance === X) { corners.reverse(); } // Calculate tip position $.each(corners, function(i, side) { var b, bc, br; if(side === CENTER) { b = precedance === Y ? LEFT : TOP; position[ b ] = '50%'; position[MARGIN+'-' + b] = -Math.round(size[ precedance === Y ? 0 : 1 ] / 2) + userOffset; } else { b = self._parseWidth(corner, side, elements.tooltip); bc = self._parseWidth(corner, side, elements.content); br = self._parseRadius(corner); position[ side ] = Math.max(-self.border, i ? bc : userOffset + (br > b ? br : -b)); } }); // Adjust for tip size position[ corner[precedance] ] -= size[ precedance === X ? 0 : 1 ]; // Set and return new position tip.css({ margin: '', top: '', bottom: '', left: '', right: '' }).css(position); return position; }, reposition: function(event, api, pos) { if(!this.enabled) { return; } var cache = api.cache, newCorner = this.corner.clone(), adjust = pos.adjusted, method = api.options.position.adjust.method.split(' '), horizontal = method[0], vertical = method[1] || method[0], shift = { left: FALSE, top: FALSE, x: 0, y: 0 }, offset, css = {}, props; function shiftflip(direction, precedance, popposite, side, opposite) { // Horizontal - Shift or flip method if(direction === SHIFT && newCorner.precedance === precedance && adjust[side] && newCorner[popposite] !== CENTER) { newCorner.precedance = newCorner.precedance === X ? Y : X; } else if(direction !== SHIFT && adjust[side]){ newCorner[precedance] = newCorner[precedance] === CENTER ? adjust[side] > 0 ? side : opposite : newCorner[precedance] === side ? opposite : side; } } function shiftonly(xy, side, opposite) { if(newCorner[xy] === CENTER) { css[MARGIN+'-'+side] = shift[xy] = offset[MARGIN+'-'+side] - adjust[side]; } else { props = offset[opposite] !== undefined ? [ adjust[side], -offset[side] ] : [ -adjust[side], offset[side] ]; if( (shift[xy] = Math.max(props[0], props[1])) > props[0] ) { pos[side] -= adjust[side]; shift[side] = FALSE; } css[ offset[opposite] !== undefined ? opposite : side ] = shift[xy]; } } // If our tip position isn't fixed e.g. doesn't adjust with viewport... if(this.corner.fixed !== TRUE) { // Perform shift/flip adjustments shiftflip(horizontal, X, Y, LEFT, RIGHT); shiftflip(vertical, Y, X, TOP, BOTTOM); // Update and redraw the tip if needed (check cached details of last drawn tip) if(newCorner.string() !== cache.corner.string() || cache.cornerTop !== adjust.top || cache.cornerLeft !== adjust.left) { this.update(newCorner, FALSE); } } // Setup tip offset properties offset = this.calculate(newCorner); // Readjust offset object to make it left/top if(offset.right !== undefined) { offset.left = -offset.right; } if(offset.bottom !== undefined) { offset.top = -offset.bottom; } offset.user = this.offset; // Perform shift adjustments shift.left = horizontal === SHIFT && !!adjust.left; if(shift.left) { shiftonly(X, LEFT, RIGHT); } shift.top = vertical === SHIFT && !!adjust.top; if(shift.top) { shiftonly(Y, TOP, BOTTOM); } /* * If the tip is adjusted in both dimensions, or in a * direction that would cause it to be anywhere but the * outer border, hide it! */ this.element.css(css).toggle( !(shift.x && shift.y || newCorner.x === CENTER && shift.y || newCorner.y === CENTER && shift.x) ); // Adjust position to accomodate tip dimensions pos.left -= offset.left.charAt ? offset.user : horizontal !== SHIFT || shift.top || !shift.left && !shift.top ? offset.left + this.border : 0; pos.top -= offset.top.charAt ? offset.user : vertical !== SHIFT || shift.left || !shift.left && !shift.top ? offset.top + this.border : 0; // Cache details cache.cornerLeft = adjust.left; cache.cornerTop = adjust.top; cache.corner = newCorner.clone(); }, destroy: function() { // Unbind events this.qtip._unbind(this.qtip.tooltip, this._ns); // Remove the tip element(s) if(this.qtip.elements.tip) { this.qtip.elements.tip.find('*') .remove().end().remove(); } } }); TIP = PLUGINS.tip = function(api) { return new Tip(api, api.options.style.tip); }; // Initialize tip on render TIP.initialize = 'render'; // Setup plugin sanitization options TIP.sanitize = function(options) { if(options.style && 'tip' in options.style) { var opts = options.style.tip; if(typeof opts !== 'object') { opts = options.style.tip = { corner: opts }; } if(!(/string|boolean/i).test(typeof opts.corner)) { opts.corner = TRUE; } } }; // Add new option checks for the plugin CHECKS.tip = { '^position.my|style.tip.(corner|mimic|border)$': function() { // Make sure a tip can be drawn this.create(); // Reposition the tooltip this.qtip.reposition(); }, '^style.tip.(height|width)$': function(obj) { // Re-set dimensions and redraw the tip this.size = [ obj.width, obj.height ]; this.update(); // Reposition the tooltip this.qtip.reposition(); }, '^content.title|style.(classes|widget)$': function() { this.update(); } }; // Extend original qTip defaults $.extend(TRUE, QTIP.defaults, { style: { tip: { corner: TRUE, mimic: FALSE, width: 6, height: 6, border: TRUE, offset: 0 } } });