PageSourceSearch

https://keziahjones.com/js/tooltip.js

js keziahjones.com collected 2026-09-26 20:32:28 UTC 18,052 bytes, 595 lines download raw bytes

1/* Requires jQuery
2 *
3 * This plugin will create div.jsTooltip elements (or configure your own!) in body for every tooltip on the page. Some
4 * basic CSS is applied automagically, but you'll want to style it on your own from there. This code will be applied to
5 * every element in your .v_tooltip() selector, so giving it a common selector like '.tooltip' is ideal.
6 *
7 * Options:
8 * - location: Where the tooltip should spawn in relation to it's parent
9 * - offsetN: How many pixels to add
10 * - trackMouse: Should we track the mouse cursor instead of the parent?
11 * - suppressOnClick: Should we hide if a user clicks the target?
12 * - suppressWhileToggled: Should we ignore events if the target has the 'toggled' class?
13 * - tooltipClass: css class to apply to tooltip elements
14 * - fadeSpeed:	Time (in milliseconds) to spend fading in/out. Set to 0 to disable.
15 * - allowHover: Should we keep the tooltip open if we mouse directly on to the tooltip? (Your tooltip will need to spawn inside it's owner's box for this to work)
16 * - tooltipParent: More generally useless properties for supernav: Lets us specify which element to parent the tooltips to. YOU PROBABLY DON'T NEED THIS.
17 * - correctForScreenSize: Adjust tooltip position to ensure it doesn't render outside of the viewport
18 * - sizeCorrectionXPadding: How far we should keep the tooltip from the window edge
19 * - useClickEvent: Should we use the mouse click event instead of hover?
20 * - inheritParentMinWidth: Should we set min-width based on our parent's width?
21 * - parentActiveCSSClass: What CSS class should we add to our parent while we're visible?
22 * - childActiveCSSClass: What CSS class should we add to the tooltip when active (Mostly useful for triggering CSS transitions
23 * - funcName: Global name of a function to call on hover (It searches the global window object; not safe inside "use strict"
24 * - func: Actual JS code to run.
25 * - responsiveMode: enable when in responsive mode/mobile/tablet, requires tooltip elements to be decorated with data-tooltip-responsive to enable in responsive mode
26 */
27/* <script> */
28(function( $ ){
29	let g_nextTooltipId = 0;
30	var methods = {
31
32		init : function( options ) {
33
34			var settings = $.extend( {
35				'location'			: 'top',
36				'offsetX'			: 0,
37				'offsetY'			: -10,
38				'trackMouse'		: false,
39				'trackMouseCentered': true,
40				'suppressOnClick'	: true,
41				'suppressWhileToggled': true,
42				'tooltipClass'		: 'jsTooltip',
43				'fadeSpeed'			: 150,
44				'allowHover'		: true,
45				'tooltipParent'		: 'body',
46				'correctForScreenSize': true,
47				'sizeCorrectionXPadding': 15,
48				'sizeCorrectionYPadding': 10,
49				'useClickEvent'		: false,
50				'useContextMenuEvent'	: false,
51				'useMouseEnterEvent'	: true,
52				'useMouseLeaveEvent'	: true,
53				'preventDefault'	: true,
54				'stopPropagation'	: false,
55				'inheritParentMinWidth'	: false,
56				'parentActiveCSSClass'	: false,
57				'dataName'			: 'tooltipContent',
58				'funcName'			: 'tooltipFunc',
59				'func'				: false,
60				'disableOnTouchDevice'	: false,
61				'childActiveCSSClass' : false,
62				'destroyWhenDone': true,
63				'createOnLoad': false,
64				'replaceExisting': true, // should we stomp on existing tooltips?
65				'defaultType': 'html', // default tooltip type if not specified.
66				'responsiveMode': false,
67				'appearanceDelayMs': 0, // How long do we wait after the trigger to show the tooltip (or to start fade-in)
68			}, options);
69
70
71
72
73			return this.each(function(){
74
75				var $target = $(this);
76
77				if ( settings.responsiveMode && !this.hasAttribute('data-tooltip-responsive' ) )
78					return;
79
80				if( $target.data('tooltip.settings') && !settings.replaceExisting)
81					return;
82
83				if( settings.useClickEvent )
84				{
85					$target.bind('click.tooltip', methods.show);
86				}
87				else
88				{
89					$target.bind('vgp_onfocus.tooltip', methods.show );
90					$target.bind('vgp_onblur.tooltip', methods.hide );
91				}
92
93				if( settings.useContextMenuEvent )
94				{
95					$target.bind('contextmenu.tooltip', methods.show);
96				}
97				if( settings.useMouseEnterEvent )
98				{
99					$target.bind('mouseenter.tooltip', methods.show);
100				}
101				if( settings.useMouseLeaveEvent )
102				{
103					$target.bind('mouseleave.tooltip', methods.hide);
104				}
105
106				if ( settings.disableOnTouchDevice )
107				{
108					// if we get a touch event, disable tooltip events on this object.  This fires before mouse events, so we
109					//	can prevent the hover from showing.
110					//	(we could preventDefault(), but that will prevent the actual click event that's coming too)
111					$target.bind('touchstart.tooltip', function(e) { $target.data('inTouchEvent', true); });
112					$target.bind('mouseup.tooltip', function(e) { $target.data('inTouchEvent', false); });
113				}
114
115				$target.data('tooltip.settings', settings);
116				$target.data('tooltip.id', g_nextTooltipId++);
117
118				if( settings.createOnLoad )
119					methods.gettooltip( this, settings );
120			});
121
122		},
123		destroy : function() {
124
125			return this.each(function(){
126				$(window).unbind('.tooltip');
127			})
128
129		},
130		gettooltip : function( element, settings ) {
131			var $element= $(element);
132			var toolDiv = $element.data("tooltip.element");
133			var toolId = $element.data("tooltip.id");
134			if ( !toolDiv )
135			{
136				toolDiv = $('<div />');
137
138				if( settings.suppressOnClick )
139				{
140					toolDiv.bind('click.tooltip', jQuery.proxy(methods.hide, element));
141				}
142				toolDiv.hide();
143				toolDiv.addClass(settings.tooltipClass);
144				if ( $element.data( 'tooltip-class' ) )
145				{
146					toolDiv.addClass( $element.data( 'tooltip-class' ) );
147				}
148				toolDiv.css({
149					position: 'absolute',
150					'z-index': 1500
151				});
152				toolDiv.attr('id', 'tooltip-' + toolId);
153				var type = $element.data('tooltip-type');
154				if( !type )
155					type = settings.defaultType;
156
157				var content = '';
158				if ( settings.dataAttr )
159					content = $element.attr( settings.dataAttr );
160				else if ( settings.dataName )
161					content = $element.data(settings.dataName);
162
163				if( type == 'text')
164				{
165					toolDiv.text( content );
166				}
167				else if ( type == 'selector' )
168				{
169					var toolElement = $( content, $element.parent() );
170					toolDiv.data( 'originalParent', toolElement.parent() );
171					toolDiv.append( toolElement.show() );
172					toolDiv.data( 'preserveContent', true );
173				}
174				else
175				{
176					toolDiv.html( content );
177				}
178
179				if ( typeof( g_TextFilter ) !== 'undefined' && g_TextFilter != null )
180				{
181					g_TextFilter.FilterElement( toolDiv );
182				}
183
184				$( settings.tooltipParent || $element.parent() ).append(toolDiv);
185				$element.data("tooltip.element", toolDiv);
186
187			}
188
189			var funcName = settings.funcName && $(element).data( settings.funcName );
190			if( funcName )
191			{
192				if( type == 'text')
193					toolDiv.text( window[funcName](element) );
194				else
195					toolDiv.html( window[funcName](element) );
196			}
197
198			if( settings.func )
199				settings.func.bind(toolDiv)(element);
200
201			return toolDiv;
202		},
203		updateposition : function() {
204		    var newPosition = {};
205		    var settings = $(this).data('tooltip.settings');
206		    var toolDiv = methods.gettooltip( this, settings );
207
208		    var parentPosition = $(this).offset();
209
210		    const bRTL = toolDiv[0].matches( ':dir(rtl)' );
211		    switch( settings.location )
212		    {
213				case 'top':
214					if ( bRTL )
215					{
216						newPosition = {
217							left: parentPosition.left + $(this).outerWidth() - toolDiv.outerWidth() + settings.offsetX,
218							top: parentPosition.top - toolDiv.outerHeight() + settings.offsetY
219						};
220					}
221					else
222					{
223						newPosition = {
224							left: parentPosition.left + settings.offsetX,
225							top: parentPosition.top - toolDiv.outerHeight() + settings.offsetY
226						};
227					}
228					break;
229
230				case 'bottom':
231					if ( bRTL )
232					{
233						newPosition = {
234							left: parentPosition.left + $(this).outerWidth() - toolDiv.outerWidth() + settings.offsetX,
235							top: parentPosition.top + $(this).outerHeight() + settings.offsetY
236						};
237					}
238					else
239					{
240						newPosition = {
241							left: parentPosition.left + settings.offsetX,
242							top: parentPosition.top + $(this).outerHeight() + settings.offsetY
243						};
244					}
245					break;
246
247				case 'bottom left':
248					if ( bRTL )
249					{
250						newPosition = {
251							left: parentPosition.left + settings.offsetX,
252							top: parentPosition.top + $(this).outerHeight() + settings.offsetY
253						};
254					}
255					else
256					{
257						newPosition = {
258							left: parentPosition.left + settings.offsetX - toolDiv.outerWidth() + $J(this).outerWidth(),
259							top: parentPosition.top + $(this).outerHeight() + settings.offsetY
260						};
261					}
262					break;
263
264				default:
265					console.log("Invalid location passed to v_tooltip: %s", settings.location);
266		    }
267		    // Correct for window size
268		    if( settings.correctForScreenSize )
269		    {
270		        var rightEdge = newPosition.left + toolDiv.width();
271		        var windowRightEdge = $(window).width() - settings.sizeCorrectionXPadding + $(window).scrollLeft();
272		        var windowLeftEdge =  $(window).scrollLeft() + settings.sizeCorrectionXPadding;
273
274		        if( rightEdge > windowRightEdge )
275		            newPosition.left = windowRightEdge - toolDiv.width() - settings.sizeCorrectionXPadding;
276
277		        if( newPosition.left < windowLeftEdge )
278		            newPosition.left = windowLeftEdge;
279
280				if ( newPosition.top < 0 )
281					newPosition.top = parentPosition.top + $(this).height() + settings.sizeCorrectionYPadding - settings.offsetY;
282		    }
283
284		    toolDiv.css(newPosition);
285		},
286		reposition : function(event) {
287			var newPosition = {};
288			var settings = $(this).data('tooltip.settings');
289			var toolDiv = methods.gettooltip( this, settings );
290
291			var parentPosition = $(this).offset();
292
293			if( settings.trackMouse )
294			{
295				if ( settings.trackMouseCentered )
296					newPosition.left = event.pageX - toolDiv.outerWidth() / 2;
297				else
298					newPosition.left = event.pageX + settings.offsetY;
299
300				if ( settings.location == 'top' )
301					newPosition.top = event.pageY - toolDiv.outerHeight() + settings.offsetY;
302				else
303					newPosition.top = event.pageY + settings.offsetY;
304
305			} else {
306				const bRTL = toolDiv[0].matches( ':dir(rtl)' );
307				switch( settings.location )
308				{
309					case 'top':
310						if ( bRTL )
311						{
312							newPosition = {
313								left: parentPosition.left + $(this).outerWidth() - toolDiv.outerWidth() + settings.offsetX,
314								top: parentPosition.top - toolDiv.outerHeight() + settings.offsetY
315							};
316						}
317						else
318						{
319							newPosition = {
320								left: parentPosition.left + settings.offsetX,
321								top: parentPosition.top - toolDiv.outerHeight() + settings.offsetY
322							};
323						}
324						break;
325
326					case 'bottom':
327						if ( bRTL )
328						{
329							newPosition = {
330								left: parentPosition.left + $(this).outerWidth() - toolDiv.outerWidth() + settings.offsetX,
331								top: parentPosition.top + $(this).outerHeight() + settings.offsetY
332							};
333						}
334						else
335						{
336							newPosition = {
337								left: parentPosition.left + settings.offsetX,
338								top: parentPosition.top + $(this).outerHeight() + settings.offsetY
339							};
340						}
341						break;
342
343					case 'bottom left':
344						if ( bRTL )
345						{
346							newPosition = {
347								left: parentPosition.left + settings.offsetX,
348								top: parentPosition.top + $(this).outerHeight() + settings.offsetY
349							};
350						}
351						else
352						{
353							newPosition = {
354								left: parentPosition.left + settings.offsetX - toolDiv.outerWidth() + $J(this).outerWidth(),
355								top: parentPosition.top + $(this).outerHeight() + settings.offsetY
356							};
357						}
358						break;
359
360					case 'mouse':
361						newPosition = {
362							left: event.pageX + settings.offsetX,
363							top: event.pageY + settings.offsetY
364						}
365
366					default:
367						console.log("Invalid location passed to v_tooltip: %s", settings.location);
368				}
369			}
370			// Correct for window size
371			if( settings.correctForScreenSize )
372			{
373				var rightEdge = newPosition.left + toolDiv.width();
374				var windowRightEdge = $(window).width() - settings.sizeCorrectionXPadding + $(window).scrollLeft();
375				var windowLeftEdge =  $(window).scrollLeft() + settings.sizeCorrectionXPadding;
376
377				if( rightEdge > windowRightEdge )
378					newPosition.left = windowRightEdge - toolDiv.width() - settings.sizeCorrectionXPadding;
379
380				if( newPosition.left < windowLeftEdge )
381					newPosition.left = windowLeftEdge;
382
383				if ( newPosition.top < 0 )
384					newPosition.top = parentPosition.top + $(this).height() + settings.sizeCorrectionYPadding - settings.offsetY;
385			}
386
387			toolDiv.css(newPosition);
388		},
389		show : function(event) {
390			var settings = $(this).data('tooltip.settings') || {};
391
392			if ( settings.disableOnTouchDevice && $(this ).data('inTouchEvent') )
393			{
394				return;
395			}
396
397			var toolDiv = methods.gettooltip( this, settings );
398
399			if( toolDiv.is(':empty') )
400				return;
401
402			if( event.type == "click" && event.currentTarget != this )
403				return;
404
405			if( settings.suppressWhileToggled && $(this).hasClass('toggled') )
406				return false;
407
408			var tipElem = this;
409			if ( !settings.trackMouse )
410			{
411				toolDiv.find( 'img' ).on( 'load', function() { jQuery.proxy(methods.updateposition, tipElem)() } );
412			}
413
414			if( settings.preventDefault )
415				event.preventDefault();
416
417			if( settings.stopPropagation )
418				event.stopPropagation();
419
420			if( settings.parentActiveCSSClass )
421				$(this).addClass(settings.parentActiveCSSClass);
422
423			if( settings.inheritParentMinWidth )
424			{
425				var parentWidth = $(this).outerWidth();
426				var localPadding = toolDiv.outerWidth() - toolDiv.width();
427				toolDiv.css({'min-width': + (parentWidth - localPadding) + "px"});
428			}
429
430			toolDiv[0].popover = 'manual';
431			toolDiv.css('inset', 'unset');
432			toolDiv.css('border', 'none');
433			toolDiv.css('overflow', 'initial');
434
435			toolDiv.stop(true, true);
436			if( settings.fadeSpeed > 0 )
437				toolDiv.delay( settings.appearanceDelayMs ).fadeTo( settings.fadeSpeed, 1 );
438			else
439				toolDiv.delay( settings.appearanceDelayMs ).show();
440
441			if( settings.allowHover )
442			{
443				if( settings.useClickEvent )
444					toolDiv.bind('click.tooltip', jQuery.proxy(methods.show, this));
445				else if( settings.useContextMenuEvent )
446					toolDiv.bind('contextmenu.tooltip', jQuery.proxy(methods.show, this));
447				else
448					toolDiv.bind('mouseenter.tooltip', jQuery.proxy(methods.show, this));
449				toolDiv.bind('mouseleave.tooltip', jQuery.proxy(methods.hide, this));
450			}
451
452			if( settings.trackMouse )
453				$(this).bind('mousemove.tooltip', methods.reposition);
454			jQuery.proxy(methods.reposition, this)(event);
455
456			if( settings.childActiveCSSClass )
457			{
458				toolDiv.css('opacity');
459				toolDiv.addClass(settings.childActiveCSSClass);
460
461			}
462
463			toolDiv[0].showPopover();
464
465			toolDiv.css('pointer-events','auto');
466
467			$(this).trigger('v_tooltip_shown', [ toolDiv ] );
468
469			$(this).attr('aria-describedby', 'tooltip-' + $(this).data("tooltip.id"));
470
471		},
472		hide : function(event) {
473			var toolDiv = $(this).data('tooltip.element');
474			var settings = $(this).data('tooltip.settings') || {};
475
476			// the element may not have been created yet - in which case there is nothing to hide
477			if ( !toolDiv || !toolDiv.length )
478				return;
479
480
481			if( event && event.type != 'click' )
482			{
483				// Moving between the click target and it's children
484				if( event.relatedTarget && ( this == event.relatedTarget || this.contains(event.relatedTarget) ) )
485					return;
486				// Moving to the tooltip or one of it's children
487				else if( event.relatedTarget && ( event.relatedTarget == toolDiv[0] || toolDiv[0].contains(event.relatedTarget ) ) )
488					return;
489			}
490
491			if( settings.trackMouse )
492				$(this).unbind('mousemove.tooltip');
493
494			toolDiv.unbind('mouseenter.tooltip');
495			toolDiv.unbind('mouseleave.tooltip');
496
497			if( settings.parentActiveCSSClass )
498				$(this).removeClass(settings.parentActiveCSSClass);
499
500
501			// Clear the animation queue so appearance delays and fading are aborted
502			toolDiv.stop( true );
503			if( settings.fadeSpeed > 0 && toolDiv.is(':visible') )
504			{
505				toolDiv.fadeTo( settings.fadeSpeed, 0, function() {
506					if ( settings.destroyWhenDone )
507					{
508						methods.destroytooltip( toolDiv );
509					}
510					if ( toolDiv[0].popover )
511					{
512						toolDiv[0].hidePopover();
513					}
514				} );
515			} else if( settings.childActiveCSSClass )
516			{
517				toolDiv.css('opacity');
518				toolDiv.removeClass(settings.childActiveCSSClass);
519				// Note: This callback isn't reliable, so we may end up junking up the dom with a few spare copies
520				// Not a huge deal in most cases, but keep in mind if you're using this on pages which may exist for
521				// long periods of time without reloading.
522				if( settings.destroyWhenDone )
523				{
524					toolDiv.on ( "transitionend", function () {
525						methods.destroytooltip ( toolDiv );
526						if ( toolDiv[0].popover )
527						{
528							toolDiv[0].hidePopover();
529						}
530					} );
531
532					// Transitionend may not fire under various conditions, so JUST IN CASE....
533					setTimeout( function(){
534						methods.destroytooltip ( toolDiv );
535						if ( toolDiv[0].popover )
536						{
537							toolDiv[0].hidePopover();
538						}
539					}, 1000);
540				}
541				else
542				{
543					if ( toolDiv[0].popover )
544					{
545						toolDiv[0].hidePopover();
546					}
547				}
548			}
549			else
550			{
551				if( settings.destroyWhenDone )
552					methods.destroytooltip( toolDiv );
553
554				if ( toolDiv[0].popover )
555				{
556					toolDiv[0].hidePopover();
557				}
558			}
559			if( settings.destroyWhenDone )
560				$(this).removeData('tooltip.element');
561			else
562				toolDiv.css('pointer-events','none');
563
564			$(this).trigger( 'v_tooltip_hidden' );
565		},
566		destroytooltip: function( toolDiv )
567		{
568			if ( toolDiv )
569			{
570				if ( $(toolDiv ).data('preserveContent') )
571				{
572					$( toolDiv.data( 'originalParent') ).append( $(toolDiv ).children().hide());
573				}
574
575				$(toolDiv).remove();
576			}
577		}
578	};
579
580	$.fn.v_tooltip = function( method ) {
581
582		if ( methods[method] ) {
583			return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
584		} else if ( typeof method === 'object' || ! method ) {
585			return methods.init.apply( this, arguments );
586		} else {
587			$.error( 'Method ' +  method + ' does not exist on jQuery.tooltip' );
588		}
589
590	};
591
592
593})( jQuery );
594
595

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.