Make jQuery UI Tooltip reposition after Ajax loading - javascript

I am trying to implement an ajax-loaded tooltip which should contain an HTML table.
At first the tooltip displays a "loading..." text while the ajax call is performed, and then when the HTML data is received, it is used as the tooltip content.
The problem is that the tooltip position seems to be calculated only at the beginning, when the "loading..." is displayed, and not again when the tooltip content changes.
The tooltips are initialized with the following code:
$('.my_tooltip').each(function() {
var $this = $(this),
tooltip_url = $this.data('tooltip-url'),
cache_id = tooltip_url || '',
opts = $.extend({}, {
content: function(done) {
if(tooltip.cache[cache_id]) {
return tooltip.cache[cache_id];
}
if(tooltip_url) {
Ajax.get($this.data('tooltip-url'), {}, {
dataType: 'html',
success: function(data) {
tooltip.cache[cache_id] = data;
done(tooltip.cache[cache_id]);
},
error: function() {
done('Error while loading data!');
}
});
return 'Loading...';
}
return $this.attr('title');
},
items: $this,
tooltipClass: $this.data('tooltip-class') || ('age-tooltip' + (!tooltip_id && !tooltip_url ? '-auto' : ''))
}, options);
$this.tooltip(opts);
});
Here is the tooltip while loading, at this time the tooltip does fit the viewport.
And here is how it looks after loading, as you see the tooltip did not automatically reposition, so you can see less than half of it.
In this case, if it had moved over the tooltip-ed element, it would be fully visible, however even if I let the tooltip disappear and then move the mouse again to hover, having it loaded from the local tooltip cache, it won't reposition.
I have looked into the position property for the tooltip, however if possible I'd like to avoid the need to manually specify it, and let the plugin handle it automatically.
Is there a way to have the tooltip positioned where the most of it will be visible after the ajax loading?
EDIT:
I tried #Stphane's solution, with the position {my: 'left top+15', at: 'left bottom', of: this, collision: 'flipfit'} however its not positioned correctly, seems to ignore the top/bottom part and instead takes the certer, as shown here:
Looking in the console, I see
Error: no such method 'instance' for tooltip widget instance
which I guess might be due to the fact that we are still using jQuery UI version 1.10.4

You can leverage using property (being a callback/hook) documented on jQuery UI position method
When specified, the actual property setting is delegated to this callback.
The cleaner approach
… is to use the tooltip instance getter to apply the right position as soon as possible:
var tooltip = {cache: []},
posSetter = function () {
// 1.10.4 code
$('.ui-tooltip').position({
// 1.12.1
// this.tooltip('instance').bindings.filter('.ui-tooltip').position({
// Put the values that fit your need
my: 'left top+15', at: 'left bottom', of: this, collision: "flip fit"
})
;
};
$('.my_tooltip').each(function() {
var $this = $(this),
ownPosSetter = posSetter.bind($this),
tooltip_url = $this.data('tooltip-url'),
cache_id = tooltip_url || '',
opts = $.extend({}, {
content: function(done) {
if (tooltip.cache[cache_id]) {
return tooltip.cache[cache_id];
}
if (tooltip_url) {
setTimeout(() => {
let content = "<div style='height:200px;width:300px'>Content Loaded!</div>";
tooltip.cache[cache_id] = content;
done(content);
setTimeout(ownPosSetter, 100);
}, 2000);
return 'Loading...';
}
return $this.attr('title');
},
items: $this,
tooltipClass: 'age-tooltip-auto'
}, {position: {using: ownPosSetter}});
$this.tooltip(opts);
});
div.contents {
min-height: 50px;
margin-top: 10px;
border: 1px solid black
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/css/jquery-ui.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js">
<!--
< link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet" / >
< script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" >
-->
</script>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents my_tooltip" title="Some content" data-tooltip-url="//somewhere.com">Has tooltip</div>
<div class="contents my_tooltip" title="Some content" data-tooltip-url="//somewhereelse.com">Has tooltip</div>
<div class="contents" id="log"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>
<div class="contents"></div>

Related

Replace jQuery animation with Angular animation

One of my ng-views has a next structure
<div class="container-fluid wrapper">
<aside ng-if="asideMenu">
<div ng-include src="'html/partials/stats/aside.html'"></div>
</aside>
<section>
<div ng-include src="'html/partials/stats/grid.html'"></div>
<div ng-include src="'html/partials/stats/tabs.html'"></div>
</section>
where I have a grid and tabs as a major content and if user wants he can open aside bar. The function to open aside bar is
$scope.asideMenu = false;
$scope.aside = function () {
getTree();
var section = document.getElementsByTagName("section");
var aside = document.getElementsByTagName("aside");
$scope.asideMenu = !$scope.asideMenu;
if ($scope.asideMenu == true) {
$(section).animate({
'width': '84%'
}, 500, function () {
$(aside).animate({
'width': '15%'
});
});
}
else {
$(aside).animate({
'width': '0%'
}, 1000, function () {
$(section).animate({
'width': '100%'
});
});
}
};
So currently I'm using jQuery animation to open aside menu and shrink major content.
The question is simple, what is the best way to replace jQuery animation to Angular & CSS animation (without any additional dependencies and libs)??
This is simple example of my page

How to animate ng-repeat items relative to click events causing the change

I'm trying to animate a user selecting items from different sets of items.
The item should animate from the clicked set to it's new position in list of selected items.
In the below demo, consider the pink boxes as available items and the bordered box as the list of selected items (blue boxes). User can select an item by clicking on either of the pink boxes:
angular.module('test', ['ngAnimate'])
.controller('testCtrl', function($scope) {
$scope.products = [{}, {}, {}, {}];
$scope.purchased = [{}];
$scope.purchase = function(dir) {
$scope.direction = dir
$scope.purchased.push($scope.products.pop());
};
})
.directive('testDir', function($animate) {
return {
link: function(scope, element) {
$animate.on('enter', element, function(element, phase) {
$target = scope.direction == 'left' ? $('.stock:first') : $('.stock:last');
element.position({
my: 'center',
at: 'center',
of: $target,
using: function(pos, data) {
$(this).css(pos);
$(this).animate({
top: 0,
left: 0
});
}
});
});
}
};
});
.stock {
display: inline-block;
width: 50px;
height: 50px;
background: hotpink;
}
.stock.right {
margin-left: 100px;
}
.product {
height: 50px;
width: 50px;
border: 1px solid;
}
.purchased {
height: 60px;
margin-top: 100px;
border: 2px dotted;
}
.purchased .product {
display: inline-block;
margin: 5px;
background: dodgerblue;
}
<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
<script src="https://code.angularjs.org/1.4.8/angular.js"></script>
<script src="https://code.angularjs.org/1.4.8/angular-animate.js"></script>
<div ng-app="test" ng-controller="testCtrl">
<div class="stock" ng-click="purchase('left')"></div>
<div class="stock right" ng-click="purchase('right')"></div>
<div class="purchased clearfix">
<div class="product" ng-repeat="product in purchased" data-test-dir>
</div>
</div>
</div>
Well, it kind of works - but I'm using jQuery-ui to find out the starting position (The position of pink boxes will wary in a responsive design) and jquery animate method to animate the element.
Also I have to store the clicked direction in scope and I'm setting both the initial position and animating to end position in the enter event listener.
I have been reading and experimenting a lot with built in animation hooks in angular, but couldn't figure out a proper way to animate elements from relative/dynamic positions.
Is there a better way to achieve the same user experience in angular js way..?
if I've understood your question correctly(tell me if not); i think one way to handle the problem, goes like this:
while assuming the size(width) of your products to be constant -set to 50px or something- ; you can set the pink elements' position to absolute; then use ng-repeat for pink elements, with a brief ng-style attribute inside the html like this:
<div ng-repeat="item in products" ng-style="{'left': $index*50 + 'px'}" ng-click="add-to-purchased($index)"></div>
and about the purchased products: instead of using ng-repeat on the "purchased" array, inside "add-to-purchased" function, after pushing the product to the "purchased" array, you can simply animate the product to the "top: 'the height distance to the bordered element'" and "left" equal to {$scope.purchased.length*50 + 'px'}. then add a class using ng-class (with a toggle) for coloring and other css stuff... (you can also consider transition for color changes. as you probably know)
i also think that you can handle different heights and tops problem(in case that the number of products becomes more than one line's capacity) with an ng-class which adds classes with new "top" values based on: ($index > some-number), and another ng-class for the upper element(the element that's on top of the bordered element), changing it's height ...
i hope this was helpful
Update:
unfortunately i hadn't understood the question well. but looking at the problem now, i think there is a way of doing this more dynamically.
inside the $scope.purchase function, you can message your directive with $broadcast and passing the clicked element like this (for any element in stock, either it's created with ng-repeat or not):
<div class="stock" ng-click="purchase($event)"></div>
and:
$scope.purchase = function(event) {
$scope.purchased.push($scope.products.pop());
$scope.$broadcast('purchaseHappened', event.target);
};
and inside your directive, put the event listener:
scope.$on('purchaseHappened', function(event, target) {
//catch target in here, and then use it's position to animate the new elements...
})
i think you can also use target.getBoundingClientRect() to get the element's position, relative to the viewport (.top , .left ,...) instead of jquery-ui's .position if you want...
is it closer to the solution?
This solution is an improvement in that it eliminates the need to add information to the scope in the Purchase function and it avoids mixing model data and UI details by using a "source" directive and storing origin information in a controller property. The example is simplified and can of course be improved. The key point is that the data required to manage the process is never exposed via the scope.
If the target element is subject to being removed from the DOM (i.e it's part of an ng-repeat, then this solution would have to be modified slightly to calculate and store the animation start positions as part of the monitor.click handler rather than store the target element itself.
The method of animation seems to me to be arbitrary. This example uses the OP's jqueryUI animation method, but it would work just as well with css transitions or using $animate.
A full example is here
angular.module('app', [])
.controller('main', function($scope) {
$scope.products = [{},{}];
$scope.purchased = [{}];
$scope.Purchase = function() {
$scope.purchased.push({});
};
})
.directive('source', function(){
return {
controller: function($scope) {
}
};
})
.directive('originator', function(){
return{
require: '^source',
priority: 1,
link:{
pre: function(scope, element, attr, ctrl){
element.on('click', function(evt){
ctrl.target = evt.target;
});
}
}
};
})
.directive('sink', function(){
return {
require: '^source',
link: function(scope, element, attr, ctrl){
var target = ctrl.target;
if(target){
var $target = $(target);
//animate from target to current position
element.position({
my: 'center',
at: 'center',
of: $target,
using: function(pos, data) {
$(this).css(pos);
$(this).animate({
top: 0,
left: 0
});
}
});
ctrl.target = undefined;
}
}
};
});

Tick label position of seiyria/bootstrap-slider are not correct

I am using seiyria bootstrap slider in which tick labels are dynamic. Here you can see the ticks are correctly aligned but their labels are not.
following is the sample markup
<div class="container">
<div class="examples">
<div class='slider-example'>
<h3>Example 13:</h3>
<p>Using ticks.</p>
<div class="well">
<input id="ex13" type="text" data-slider-ticks="[100, 150, 180, 300, 400]" data-slider-ticks-labels='["$100", "$150", "$180", "$300", "$400"]' />
</div>
</div>
</div>
Here is a jsFiddle showing the issue
Here is an image to show the issue:
This is a limitation of the bootstrap-slider plugin in its official state (confirmed by the author in the comments below).
However, with some tweaking to the plugin, you could make it do what you want.
jsFiddle Example
Specifically, this is what I changed to achieve this effect:
I changed this CSS rule (you should really override the original rule for your specific container rather than changing the plugin CSS as I have done for simplicity sake):
.slider.slider-horizontal {
width: 100%; /*changed*/
height: 20px;
}
I added this default option to the plugin:
Slider.prototype = {
//....
defaultOptions: {
//...
modify_label_layout:false // added this option
},
//....
Then, I added these two functions to the plugin:
_modifyLabelOffset: function(){
var positions = [];
var $sliderContainer=$(this.element).prevAll('.slider');
var $ticks = $sliderContainer.find('.slider-tick');
var $labels = $sliderContainer.find('.slider-tick-label');
$sliderContainer.css('margin-bottom', '12px');
$ticks.each(function () {
var $this = $(this);
var tickWidth = $this.width();
var tickLeft = $this.position().left;
positions.push(tickLeft - (tickWidth))
});
$labels.each(function (i,e) {
var $this = $(this);
$this.css('width', 'auto');
$this.css('position', 'absolute');
$this.css('left', positions[i]+'px');
});
$this=this;
$( window ).resize(function() {
$this._delay(function(){
$this._modifyLabelOffset();
}, 500);
});
},
_delay: (function(){
var timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})(),
I changed the initialization of the plugin to this:
$("#ex13").slider({
ticks: [100, 150, 180, 300, 400],
ticks_labels: ['$100', '$150', '$180', '$300', '$400'],
ticks_snap_bounds: 30,
modify_label_layout:true // added this option to the plugin, if true the layout of the lables will be modified
}).slider('setValue', 0); // starts out at 5 for some reason, weird, for now , just set it manually to 0
Note that this is a bit of a hack and could undoubtedly be incorporated into the plugin in a better way. Also, I have ONLY tested this in chrome. It may not function correctly in other browsers but this shows proof of concept so Ill let you take it from there :)
Original Answer:
I believe this is a limitation of the bootstrap-slider plugin.
Note that if you use evenly spaced numbers like so:
data-slider-ticks="[100, 200, 300, 400, 500]"
the labels are correctly positioned. see this jsFiddle
The plugin will let you use small numbers relatively close together as you have done here but even their own example page displays issues with the layout when doing so:

Appending div to dynamic grid layout

I am using this jquery plugin http://packery.metafizzy.co/ to create a dynamic grid layout. The plugin is working perfectly.
The problem is :
I am trying to create an option to add a dynamic grid on click event. If I hand code the html code for grid then the grid shows up perfectly but if I append the grid code to my template layout using jquery then the grid shows up from the very top of the layout and the plugin doesn't seem to adjust the grid position.
You can check my code and see the problem live here: http://jsfiddle.net/A7ubj/2/
I think the plugin is not adjusting the grid because my jquery append code is making the grid seat on the top.
Could you please tell me how to append the grid so that the plugin can adjust the grid perfectly?
This is how I am appending:
$(function(){
$('#myID').click(function(){
$( "#container" ).append( $( '<div class="item diff">Grid</div>' ) );
});
});
And this is my entire code:
JS:
<script src="http://code.jquery.com/jquery-1.10.2.min.js" type="text/javascript"></script>
<script src="packery.pkgd.min.js" type="text/javascript"></script>
<script>
$( document ).ready(function() {
var $container = $('#container');
// initialize
$container.packery({
itemSelector: '.item',
gutter: 10
});
});
</script>
<script>
$(function(){
$('#myID').click(function(){
$( "#container" ).append( $( '<div class="item diff">Grid</div>' ) );
});
});
</script>
CSS:
#container{
background-color:#F00;
width: 1130px;
margin:0px auto;
}
.item { width: 275px; background-color:#0C0;}
.item.w2 { width: 560px; background-color:#036; }
.diff{
min-height:300px;
}
.diff2{
min-height:250px;
}
HTML:
<button id="myID">Click</button>
<div id="container">
<div class="item diff">Grid</div>
<div class="item w2 diff2">Grid</div>
</div>
You can call packery.reload() or just use the packery initialization function again after you have append the images and to calculate every new image position. I use the masonry plugin and masonry.reload().
Update: To make masonry work with infinitescroll use a callback function: How to enable infinite scrolling with masonry?
Here is the code from my website. I use jquery templates and prepend. You can see that it call masonry('reload') after the prepend. It also init masonry from the new container. It also correct the width and the height of each image because I think there is an error in masonry. I think it's not really what you need because I don't cache the brick but I rebuild the entire container when I need it to show a new category. In your example you physically add a brick but I don't understand why it didn't work. The result is the same but not so clean.
$j.getJSON($j("#tag") function(response)
{
// Start masonry animated
if (response && response.length)
{
var container = $j('#container');
container.masonry({
itemSelector: '.brick',
columnWidth: brick_width,
isAnimated: !Modernizr.csstransitions
});
boxCount = response.length;
counter = 0;
$j.each(response, function(idx, ele)
{
container.prepend($j("#brickTemplate").tmpl(ele)).masonry('reload');
}
container.imagesLoaded(function()
{
$j("#brick").each(function()
{
var content = $j(this).find(">div");
var height = $j(this).find("img").attr("height");
if ( height == undefined )
{
content.css({
height: "300px"
});
} else {
content.css({
height: height
});
}
// bricks fade in
$j(this).delay(Math.floor(Math.random() * 1600)).fadeIn('slow');
// Bind Mousemove
$j(this).mousemove(function()
{
.....
}
}
}
}

YUI skinning scrollbar used in container

How can I implement custom-looking scrollbars in YUI containers?
Using YUI3, you can change the scroll bars of an internal frame. If you ment the external scroll bars of the actual browser -- just drop the idea. It is not worth any headache. Too many browsers simply won't let you.
Here is example of an internal setup skinned in YUI 3.4
CSS:
<style type="text/css" media="screen">
#video-playlist-scroll-bar-container{text-align:right}
#video-playlist-scroll-bar{position:relative;width:14px;border:1px solid #e0e0e0;height:379px;margin:0 auto}
#drag-handle-container-wrap{position:relative;top:17px;bottom:17px;left:0;width:14px;height:345px}
#drag-handle-container-wrap .yui3-slider-content{position:absolute;top:0;left:0}
#drag-handle-draggable{position:absolute;left:0;background-color:#eaeaea;text-align:center;cursor:move;width:14px}
#drag-handle-up,#drag-handle-down{position:absolute;display:block;width:14px;height:16px;cursor:pointer}
#drag-handle-up{top:0;left:0}
#drag-handle-down{bottom:0;left:0}
</style>
HTML:
<div class="yui3-u-1-12" id="video-playlist-scroll-bar-container">
<div id="video-playlist-scroll-bar">
<div id="drag-handle-up"><img src="/assets/images/rebrand/drag-arrow-top.gif"></div>
<div id="drag-handle-container-wrap">
<span class="yui3-widget yui3-sliderbase yui3-slider" style=""><span class="yui3-slider-content yui3-slider-y"><div id="drag-handle-container" style="height: 345px; ">
<div id="drag-handle-draggable" class="yui3-dd-draggable" style="top: 0px; left: 0px; ">
<img src="/assets/images/rebrand/drag-handle.gif" width="9" height="100">
</div></div></span></span></div>
<div id="drag-handle-down"><img src="/assets/images/rebrand/drag-arrow-bottom.gif"></div>
</div>
</div>
YUI:
YUI().use("base","node",'slider',function(Y){
var CLICK = "click",
ID = "#",
_scrollingBuffer = 75,
_maxScrollRegion = 0;
// Slider
var _tmp = Y.one(ID+'playlist-container'+" .video-playlist-item"),
_nodeBuffer = Y.one(ID+'playlist-container').get('children').slice(-5),
_bufferFunction = function() {
var _height = 0;
_nodeBuffer.each(function(i) {
_height = _height + i.get('region').height;
});
return _height;
},
_buffer = _bufferFunction(),
_maxScrollRegion = Y.one(ID+'playlist-container').get("region").height - _buffer;
var listScroll = new Y.Slider({
axis : 'y',
min : 0, // reverse min and max to make the top
max : _maxScrollRegion,
value : 0,
length: '345px'
});
listScroll.renderRail = function () {
return Y.one( "#drag-handle-container" );
};
listScroll.renderThumb = function () {
return this.rail.one( "#drag-handle-draggable" );
};
listScroll.render( "#drag-handle-container-wrap" );
listScroll.on('valueChange', Y.bind(function (e) {
//scroll something?
}));
Y.one("#drag-handle-up").on(CLICK, Y.bind(function (e) {
e.preventDefault();
if (listScroll.get('value') >= _scrollingBuffer) {
listScroll.setValue(listScroll.get('value') - _scrollingBuffer);
} else {
listScroll.setValue(0);
}
}));
Y.one("#drag-handle-down").on(CLICK, Y.bind(function (e) {
e.preventDefault();
if (listScroll.get('value') <= Math.round(_maxScrollRegion - _scrollingBuffer)) {
listScroll.setValue(listScroll.get('value') + _scrollingBuffer);
} else {
listScroll.setValue(_maxScrollRegion);
}
}));
});
Note, this is pretty much a copy/paste from a project of mine -- with a quick removal of any identifiers. It may not work as a copy/paste .. but you'll get the jist.
Final product:
You could go even further by not using a "overflow: auto;" CSS and use the YUI 3 ScrollView along with it. I use the Paged version on YUI myself. It is easy enough to do a up & down by page & percentages.
Hopefully this is what you were looking for, not the browser's scroll bar.

Categories

Resources