Simple jQuery Tooltip (position distorted on iPad) - javascript

I've used simple jQuery UI tooltip on form-fields of my webpage(viz responsive), its working perfectly on desktop on every browser, but on iPad its get distorted when I tap on form-fields as keypad swipe-up. Also header section of my webpage gets fixed on scroll.
I've used below code for the custom jQuery Tooltip.
$(function () {
$('.form-control').tooltip({
disabled: true,
position: {
my: "left top",
at: "left top-50",
using: function( position, feedback ) {
$( this ).css( position );
$( "<div>" )
.addClass( "arrow" )
.addClass( feedback.vertical )
.addClass( feedback.horizontal )
.appendTo( this );
}
}
}).on("focusin", function () {
$(this)
.tooltip("enable")
.tooltip("open");
}).on("focusout", function () {
$(this)
.tooltip("close")
.tooltip("disable");
});
});
I've written this code to re-initialize the tooltip for the focused field by calling its focusin trigger manually when document size changed. It is working as expected on Desktop browsers but on iPad tooltip is being re-initialized at same place again viz incorrect.
var toolTipEl;
$('#inputSuccess, #inputWarning').tooltip({
open: function (event, ui) {
toolTipEl = event.target;
}
});
function checkDocumentHeight(callback){
var lastHeight = document.body.clientHeight, newHeight, timer;
(function run(){
newHeight = document.body.clientHeight;
if( lastHeight != newHeight )
callback();
lastHeight = newHeight;
timer = setTimeout(run, 100);
})();
}
function doSomthing(){
console.log('document resized');
setTimeout(function() {
if ($(toolTipEl).is(':focus')) {
$(toolTipEl).trigger('focusout').trigger('focusin');
}
}, 500);
}
checkDocumentHeight(doSomthing);
Please help me to find out the solution for this.

You will need to reinitialize active Tooltip after completion of any event which cause change in the content (actually height) of your document affecting your Tooltip position.
First keep reference of the element for which the tooltip is active by listening to its events by adding following codes in your tooltip initialization (Keep your existing code as it is. Just add these additional statements).
var toolTipEl = undefined;
$(function () {
$('.form-control').tooltip({
open: function (event, ui) {
toolTipEl = event.target;
}
}).on("focusin", function () {
toolTipEl = undefined;
}).on("focusout", function () {
toolTipEl = undefined;
}).on("mouseleave", function () {
toolTipEl = undefined;
});
});
Please note, as you are displaying tooltip on focusin event as well. So, you will also need to release the variable if you do not want tooltip pop-upped if focus has changed/left.
And then create a function for resetting tooltip like below.
function resetTooltip() {
if (toolTipEl) {
$(toolTipEl).trigger('focusout').trigger('focusin');
};
}
Call this function in any events which is causing change in the document height. For example if content is coming from an ajax request. You can call restTooltip function by listing to the gloab ajax events. See example below.
$( document ).ajaxComplete(function() {
resetTooltip();
});
I hope it will help you. Ask me if you need any further clarification.

Related

Resize function events fire 2 times more each time the window was resized

On my site, I am trying to fix the navigation so that when the browser is getting resized from desktop to mobile size, the mobile menu works. I have the mobile menu working on initial load, and the desktop navigation working on initial load, but when I run the script in a $(window).on('resize', function() {} and click an item as depicted in my script, the event fires always +1 each time the window was rested after a resize.
What I mean is, if I load the page, scale it into mobile size, click the menu and a dropdown item, the click event will fire once. Resize the window out and then back in, the click event will fire now 2 times, then 3, and so on, depending on how many times the browser was resized.
I'm not sure exactly what is going on in my resize script that is screwing everything up and I'm at my wits end at trying to figure it out. Normally people aren't sitting there resizing their browser from desktop to mobile, but my boss does when he show's clients a beta of their site and wants this to never be an issue.
Here is my resize script:
(function( $ ) {
var id,
$body = $('body'),
$window = $( window ),
$navSlider = $('.nav-slider'),
$navMask = $( '.nav-mask' ),
$navToggler = $( '.navbar-toggler' ),
$parent = $( '.menu-item-has-children' ),
$parentLink = $( '.dropdown-toggle' ),
$childContainer = $( '.dropdown-menu' );
$window.on( 'resize', function( e ) {
clearTimeout(id);
id = setTimeout(function() {
close();
var width = $window.width();
if ( width < 992 ) {
setHeightToNav();
$navMask.on( 'click', function() { close() } );
$navToggler.on( 'click', function() { open() } );
$parentLink.on( 'click', function( e ) {
e.preventDefault();
var $this = $( this );
$this.data( 'clicked', true );
console.log( $this.parent() );
} )
}
if ( width >= 992 ) {
resetNavHeight();
console.clear();
}
}, 500 );
} );
function setHeightToNav() {
if ( $body.hasClass( 'logged-in' ) ) {
var $wpAdminBar = $( '#wpadminbar' ).outerHeight();
$navSlider.css( { top: $wpAdminBar + 'px' } );
}
var $navHeight = $( '#header-container' ).outerHeight();
$navSlider.css( { marginTop: $navHeight + 'px' } );
}
function resetNavHeight() {
if ( $body.hasClass( 'logged-in' ) ) {
$navSlider.css( { top: 0 + 'px' } );
}
$navSlider.css( { marginTop: 0 + 'px' } );
}
function close() {
$body.removeClass( 'has-active-menu' );
setTimeout( function() {
$navSlider.removeClass( 'toggling' );
$parent.removeClass( 'show' );
$parentLink.attr( 'aria-expanded', false );
$childContainer.removeClass( 'show' ).removeAttr( 'style' );
$parentLink.data('clicked', false);
}, 250 );
console.log('close()');
}
function open() {
$body.addClass( 'has-active-menu' );
$navSlider.addClass( 'toggling' );
}
})( jQuery );
I've tried my script both with AND without the setTimeout function and it happens exactly the same.
On the project, we are using Bootstrap 4, with the Bootstraps Dropdown._clearMenus(); function commented out in the right places as it was causing conflicts with the functionality I wanted with the navigation.
A link to a site where you can see this is here. It's a WordPress site as well if that matters for anything.
Any help is appreciated. I've been at this for several hours and am at my wits end.
.on( 'click', function ) does not set the event listener, it adds an event listener. Try doing off('click') before setting it if you really need to set this listener here.
But note that any other 'click' listener for this element will also be removed.
That's for the quick fix. You could do better, but that would require more work (track with a boolean if you just changed "display mode", and add or remove the event listeners only then, for example).

how to drag element automatically jquery ui

I'm using jquery UI draggable. I do some works in drag function. for example I scale the dragging element according to it's position. I want to drag elements automatically to certain (x, y) (something like jquery animate({left:x, top:y}, 1000)); but I want to trigger drag function and scale element when is animating. how can I do this?
I suggest another approach to do that.
Use an external function to do the scale effect, and call it from both events (drag and animate):
var $myDraggable = $('#draggable').draggable({
drag: function( event, ui ) {
scale(ui.offset.left, ui.offset.top);
}
});
$('button').on('click', function(){
$myDraggable.animate(
{ left:100, top:100 },
{
duration: 1000,
progress: function(draggable){
scale(draggable.elem.offsetLeft, draggable.elem.offsetTop);
}
});
});
function scale(left, top){
//your scaling logic here
console.log("scaling", left, top);
}
See this example: FIDDLE
https://jsfiddle.net/moongod101/8gdvz9jL/
PS:This code offer a button toggle function
$(function(){
$button = $('button')
$box = $('.box')
$click = 0
$button.click(function(){
if($click !=0){
$click ++
$box.removeClass('active')
}else{
$click --
$box.addClass('active')
}
});
});

JQuery plugin not working when used in multiple places in a single page

I am writing a JQuery plugin for a project I'm working on which turns from tabbed content on desktop devices to an accordion on mobile devices. I've used JQuery Boilerplate (https://github.com/jquery-boilerplate/jquery-boilerplate/blob/master/dist/jquery.boilerplate.js) as an initial pattern for my plugin.
The plugin is called on any element with the class ".tabs2accordion" as shown here:
$(".tabs2accordion").tabs2Accordion({state:"desktop"});
The plugin works as expected if there is only one element with ".tabs2accordion" class on a page but starts to malfunction as soon as another element with the same class is added to the page. I've created a codepen of the basic code to demo the issue. To show the issue, on a window size of >768px try clicking any of the titles and observe how the content below changes as each title is clicked. Next uncomment the block of HTML and try clicking on the titles again.
http://codepen.io/decodedcreative/pen/MyjpRj
I have tried looping through each element with the class "tabs2accordion" like this:
$(".tabs2accordion").each(function(){
$(this).tabs2Accordion({state:"desktop"});
});
But this didn't fix the issue either.
Any ideas?
I have not used jQuery Boilerplate, but I believe the problem here is with your variable called plugin.
Nowhere in your code do you declare a variable called plugin. When I stop the debugger in Plugin.prototype.showTabContent, I can evaluate window.plugin and it returns the global value for plugin.
In the constructor for Plugin, the first line reads plugin= this;. Since plugin is not defined, it is declaring the variable at global scope on the window object.
The fix is to pass a reference to the plugin object when setting up the $().on() hook. The data passed is available in the event handlers via the event parameter that is passed in the data property.
Here is the solution (at http://codepen.io/shhQuiet/pen/JXEjMV)
(function($, window, document, undefined) {
var pluginName = "tabs2Accordion",
defaults = {
menuSelector: ".tabs2accordion-menu",
tabContentSelector: ".tabs2accordion-content"
};
function Plugin(element, options) {
this.element = element;
this.$element = $(this.element);
this.options = $.extend({}, defaults, options);
this.$menu = $(this.element).find(this.options.menuSelector),
this.$tabs = $(this.element).find(this.options.tabContentSelector),
this.$accordionTriggers = $(this.element).find(this.$tabs).find("h3");
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function() {
//Set all the tab states to inactive
this.$tabs.attr("data-active", false);
//Set the first tab to active
this.$tabs.first().attr("data-active", true);
//If you click on a tab, show the corresponding content
this.$menu.on("click", "li", this, this.showTabContent);
//Set the dimensions (height) of the plugin
this.resizeTabs2Accordion({
data: this
});
//If the browser resizes, adjust the dimensions (height) of the plugin
$(window).on("resize", this, this.resizeTabs2Accordion);
//Add a loaded class to the plugin which will fade in the plugin's content
this.$element.addClass("loaded");
console.log(this.$element);
},
resizeTabs2Accordion: function(event) {
var contentHeight;
var plugin = event.data;
if (!plugin.$element.is("[data-nested-menu]")) {
contentHeight = plugin.$tabs.filter("[data-active='true']").outerHeight() + plugin.$menu.outerHeight();
} else {
contentHeight = plugin.$tabs.filter("[data-active='true']").outerHeight();
}
plugin.$element.outerHeight(contentHeight);
},
showTabContent: function(event) {
var $target;
var plugin = event.data;
plugin.$menu.children().find("a").filter("[data-active='true']").attr("data-active", false);
plugin.$tabs.filter("[data-active='true']").attr("data-active", false);
$target = $($(this).children("a").attr("href"));
$(this).children("a").attr("data-active", true);
$target.attr("data-active", true);
plugin.resizeTabs2Accordion({data: plugin});
return false;
},
showAccordionContent: function(event) {
var plugin = event.data;
$("[data-active-mobile]").not($(this).parent()).attr("data-active-mobile", false);
if ($(this).parent().attr("data-active-mobile") === "false") {
$(this).parent().attr("data-active-mobile", true);
} else {
$(this).parent().attr("data-active-mobile", false);
}
}
};
$.fn[pluginName] = function(options) {
return this.each(function() {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName, new Plugin(this, options));
}
});
};
})(jQuery, window, document);
$(window).on("load", function() {
$(".tabs2accordion").tabs2Accordion({
state: "desktop"
});
});
I rewrote your code following jQuery's Plugin creation standard.
http://codepen.io/justinledouxmusique/pen/GZrMgB
Basically, I did two things:
Moved away from using data attributes for styling (switched to using an .active class instead)
Moved away from using this everywhere, as it bring a whole wave of binding issues...
$.fn.tabs2Accordion loops through all the selectors, and applies $.tabs2Accordion. It also returns the selector for chaining (it's a standard in jQuery).
Then, all the internal methods are function expressions which are in the same scope as all your old this "variables". This simplifies the code greatly as you can refer to those variables without passing them in as a parameter or without having to .bind( this ) somehow.
Finally, the old init() function is gone. Instead, I put the code at the end of the $.tabs2Accordion function.
Hope this helps!
(function ( window, $ ) {
$.tabs2Accordion = function ( node, options ) {
var options = $.extend({}, {
menuSelector: '.tabs2accordion-menu',
tabContentSelector: '.tabs2accordion-content'
}, options )
var $element = $( node ),
$menu = $element.find( options.menuSelector ),
$tabs = $element.find( options.tabContentSelector ),
$accordionTriggers = $tabs.find( 'h3' )
var resizeTabs2Accordion = function () {
$element.outerHeight( !$element.is( '[data-nested-menu]' )
? $element.find( 'div.active' ).outerHeight() + $menu.outerHeight()
: $element.find( 'div.active' ).outerHeight() )
}
var showTabContent = function () {
var $this = $( this ) // This will be the clicked element
$menu
.find( '.active' )
.removeClass( 'active' )
$element
.find( '.active' )
.removeClass( 'active' )
$( $this.find( 'a' ).attr( 'href' ) )
.addClass( 'active' )
$this
.find( 'a' )
.addClass( 'active' )
resizeTabs2Accordion()
return false
}
var showAccordionContent = function () {
var $this = $( this ),
$parent = $this.parent(),
mobileIsActive = $parent.data( 'active-mobile' )
$( '[data-active-mobile]' )
.not( $parent )
.data( 'active-mobile', false )
$parent
.data( 'active-mobile', mobileIsActive ? false : true )
}
// The equivalent of init()
$tabs
.removeClass( 'active' )
.first()
.addClass( 'active' )
$element.addClass( 'loaded' )
$menu.on( 'click', 'li', showTabContent )
$( window ).on( 'resize', resizeTabs2Accordion )
resizeTabs2Accordion()
console.log( $element )
}
$.fn.tabs2Accordion = function ( options ) {
this.each( function ( index, node ) {
$.tabs2Accordion( node, options )
})
return this
}
})( window, jQuery )
$( window ).on( 'load', function () {
$( '.tabs2accordion' ).tabs2Accordion({
state: 'desktop'
})
})

Detect hover in jquery drag event

I'm using a Jquery UI drag/drop effect, and while i'm dragging the element I need to execute a piece of code that detects if an element is hovered.
Update: Live Example: here
Here is my Drag code:
<script type="text/javascript">
$(function(){
$('.list-disc').sortable({
connectWith: '.list-disc',
drag: function(event, ui){
$(".prof-name").hover(function(event) {
var $obj = event.target;
if (!timeoutId) {
timeoutId = window.setTimeout(function() {
timeoutId = null;
$($obj).next().slideToggle();
}, 2000);
}
},
function (event) {
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
else {
$(event.target).next().slideToggle();
}
});
},
start: function (event, ui){
//SomeCode
},
stop: function (event, ui){
//SomeCode
}
})
</script>
The documentation says:
drag( event, ui )
Triggered while the mouse is moved during the dragging, immediately before the current move happens.
Maybe I've understood it wrong or something is wrong with the code...
As you can see, I can make that code inside drag:: event works when executing it inside my $(document).ready(function(){}. But I need to do it while draggin the element, test if the dragged element is hover that object for 2 seconds then i'll slideToggle() it.
Also tried to write a simple console.log() to verify if that's firing, but nothing...
*Update:*Tried Ron's answer: (Still not working - Nothing happens)
over: function(event, ui){
var timeoutId;
$(".prof-name").hover(function(event) {
var $obj = event.target;
if (!timeoutId) {
timeoutId = window.setTimeout(function() {
timeoutId = null;
$(this).next().slideToggle();
}, 2000);
}
},
function (event) {
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
else {
$(event.target).next().slideToggle();
}
});
}
Try this:
$( ".list-disc").sortable({
over: function( event, ui ) { // $(this) will be the current hovered element }
});

Add image swipe support for mobile touch devices to Shadowbox.js

I'm using Shadowbox.js (web-based viewer like lightbox) on various websites and it works great so far. The only problem is the bad support for mobile devices.
It is not possible to change images within the galery by swiping the current image, so the user have to use the very small navigation buttons at the bottom of the viewer-viewport. (keyboard navigation is also possible, but not on mobile devices)
It's not just the difficult usage of these navigation buttons. Every tap on the navigation button triggers a zoom function with a detail view of the focused area - and a second tap is needed to confirm the switch to the next or previous image.
It would be great, if it would be possible to make the galery behave like the default android galery viewer or similar mobile apps.
I'm using the jquery version of shadowbox.
There's an example script how to improve Shadowbox.js:
First you need the jQuery version of Shadowbox.js. Also jQuery UI is required and the jQuery UI Touch Punch plugin (size: 584 bytes). Without Punch-plugin jQuery UI does not support touch events.
Add the required libraries if not already done:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script>
Now we have to look at the required default-<script>-area of Shadowbox.js:
<link rel="stylesheet" type="text/css" href="plugins/shadowbox-3.0.3/shadowbox.css">
<script type="text/javascript" src="plugins/shadowbox-3.0.3/shadowbox.js"></script>
<script type="text/javascript">
Shadowbox.init( { onOpen: function() { }, players: ['html','img', 'iframe' ,'inline'] } );
</script>
This is the minimum javascript call to initialize the Shadowbox.js viewer. onOpen is one of the events, which allows to run code at a specific point. In our case it's required to use a event, which is fired after loading the enlarged image. We use the event onFinish and run custom jQuery UI Code to make the image viewport of the viewer draggable to the x-axies.
So the user is able to drag the image from left to right or vice versa:
<script type="text/javascript">
Shadowbox.init(
{
/* Add call to hook into the event */
onFinish : function ()
{
/* Make the image viewport draggable */
$( "img#sb-player" ).draggable({ axis: "x" });
},
onOpen: function()
{
},
players: ['html','img', 'iframe' ,'inline'] } );
</script>
But the user is not able to swipe through the galery, because there is no function call to go to the next or previous image. So we have to add them. That requires a variable at the beginning of the code to save the current position of the image without dragging. Then we have to add an event when the user stops dragging the image to get the image position after dragging to calculate the distance between start and end position. We can use the calculated value to get the direction (left or right) of the swipe gesture.
<script type="text/javascript">
Shadowbox.init(
{
/* Add call to hook into the event */
onFinish : function ()
{
/* Make the image viewport draggable */
$( "img#sb-player" ).draggable({ axis: "x" });
/* Save the current position of the image in a global variable */
leftrightDirection = $( "img#sb-player" ).offset().left;
/** Execute the calculation of the direction and call the next/prev functions **/
$( "img#sb-player" ).draggable({ stop: function( event, ui )
{
var leftrightDirectionCalculated = leftrightDirection - $( "img#sb-player" ).offset().left;
if ( Math.abs( leftrightDirectionCalculated ) > 30 )
{
if ( leftrightDirectionCalculated < 0 )
{
/* Go to the previous image */
Shadowbox.previous()
}
else
{
/* Go to the next image */
Shadowbox.next();
}
}
}});
},
onOpen: function()
{
},
players: ['html','img', 'iframe' ,'inline'] } );
</script>
We're almost done. This works so far, but the problem are Shadowbox galeries with only one single image. In this case the image must not be draggable.
Another problem is the first and the last image of the galery. For example if the first image is visible, it's possible to drag to the right - even if there is no previous image (because it's the first one!). That's the reason, we have to check if the current image is the first or the last one and deny a drag of the image to the left or right.
On every called drag event (for every pixel the image moves) we have to calculate the distance and check, if it's allowed to move the image. If not, we just set the start position. So the first image for example can only be moved in one direction.
if ( Shadowbox.hasNext() == false )
{
if(ui.position.left < startPosition)
{
ui.position.left = startPosition;
}
else if(ui.position.left > 250)
{
ui.position.left = 250;
}
startPosition = ui.position.left;
}
if ( Shadowbox.current == 0 )
{
if(ui.position.left > startPosition)
{
ui.position.left = startPosition;
}
else if(ui.position.left < -250)
{
ui.position.left = -250;
}
startPosition = ui.position.left;
}
Now we're done. It's actualy very easy to modify current Shadowbox installations. The code is not the best, but maybe it's a good basis for further improvements (for example a gesture for closing the enlarged viewer).
Complete script:
Shadowbox.init( { onFinish : function () {
if ( Shadowbox.hasNext() == false && Shadowbox.current == 0 )
{
// If there is only one image at the galery - just do nothing!
}
else
{
/* Save the current image position */
leftrightDirection = $( "img#sb-player" ).offset().left;
/* Make the image draggable, but only in x-direction */
$( "img#sb-player" ).draggable({ axis: "x" });
/* After dragging go to previous / next image in the galery */
$( "img#sb-player" ).draggable({ stop: function( event, ui )
{
var leftrightDirectionCalculated = leftrightDirection - $( "img#sb-player" ).offset().left;
if ( Math.abs( leftrightDirectionCalculated ) > 30 )
{
if ( leftrightDirectionCalculated < 0 )
{
Shadowbox.previous()
}
else
{
Shadowbox.next();
}
}
}});
$( "img#sb-player" ).draggable({ start: function( event, ui )
{
startPosition = ui.position.left;
}});
/* Improve behaviour for the first or last image */
$( "img#sb-player" ).draggable({ drag: function( event, ui )
{
if ( Shadowbox.hasNext() == false )
{
if(ui.position.left < startPosition)
{
ui.position.left = startPosition;
}
else if(ui.position.left > 250)
{
ui.position.left = 250;
}
startPosition = ui.position.left;
}
if ( Shadowbox.current == 0 )
{
if(ui.position.left > startPosition)
{
ui.position.left = startPosition;
}
else if(ui.position.left < -250)
{
ui.position.left = -250;
}
startPosition = ui.position.left;
}
}});
} // Ende Fallunterscheidung zwischen Galerie und 1-Bild-Lightbox
}, onOpen: function() { }, players: ['html','img', 'iframe' ,'inline'] , resizeDuration: 0 } );

Categories