There's a fixed container that serves as the viewport to my content. The content is a <div> element of a fixed size. The viewport size is bound to the window and may change if the user resizes the window. I'm using the jQuery.panzoom library to handle panning and zooming to let the user view the parts of the content they want. Now I need the following features and can't find them:
The content must never be smaller than required to be completely visible within the viewport. This seems to be done with the minScale option. I just need to wire up the resize events to update the minscale value then.
The content must never be dragged out to one edge if there would be an empty space on the opposite edge. That means, if the content is small enough to be completely visible, it must be completely visible. This must be centered so that the content is always at the same position when zoomed out.
It's like a picture viewer that initially zooms the image to fit and centers it. The user can zoom in, but not zoom out further than the initial view. Only that my content is not an image but a more complex HTML element.
Here's what I've done so far:
<div id="room-wrapper" style="position: fixed; top: 0; left: 150px; right: 0; bottom: 0;">
<div id="room" style="width: 800px; height: 600px;">
Content here.
</div>
</div>
<script>
var roomWrapper = $("#room-wrapper");
// Set up options
var minScaleX = roomWrapper.innerWidth() / 800;
var minScaleY = roomWrapper.innerHeight() / 600;
var minScale = Math.min(minScaleX, minScaleY);
var panzoom = $("#room").panzoom({
startTransform: "scale(" + minScale + ")",
minScale: minScale,
//contain: "invert",
increment: 0.1
});
// Compensate strange offset, doesn't work
panzoom.panzoom("pan", 150, 0, {
animate: false,
silent: true
});
// Mouse wheel zooming
panzoom.parent().on("mousewheel.focal", function (e) {
e.preventDefault();
var delta = e.delta || e.originalEvent.wheelDelta;
var zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;
panzoom.panzoom("zoom", zoomOut, {
animate: true,
focal: e
});
});
</script>
The content is initially zoomed correctly, but offset somewhere to the top and left. I have no idea where that offset comes from. Also, there's no panning contraint yet. The commented out contain may be related but I don't understand any of its documentation.
What do I need to fix in my code to make it meet the above requirements? I guess these are pretty basic and many people would need them but there's no example for that.
Related
I created Parallax script for some elements (rect & circles) so when I scroll from top to bottom, elements should move to the top
Elements starting position is added in HTML directly using < style >
The problem is when I scroll back elements move down, but they are not in the same starting position when I reach the top of the page.
HTML:
<img style="top: 62%; left: 46.3%;" class="l-parallax_item" src="./path/to/img">
<img style="top: 74%; left: 42.7%;" class="l-parallax_item" src="./path/to/img">
CSS:
Parent has position relative, l-parallax_item is position absolute
JS:
var parallaxElements = document.getElementsByClassName("l-parallax_item");
var lastScrollTop = 0;
window.onscroll = function() {
var st = window.pageYOffset || document.documentElement.scrollTop;
if (st > lastScrollTop){
console.log("bottom");
for(i = 0; i < parallaxElements.length; i++) {
var position = parallaxElements[i].offsetTop;
var movePx = parallaxElements[i].getAttribute("data-px-per-scroll");
parallaxElements[i].style.top = (position - parseInt(movePx))+"px";
}
} else {
console.log("top");
for(i = 0; i < parallaxElements.length; i++) {
var position = parallaxElements[i].offsetTop;
var movePx = parallaxElements[i].getAttribute("data-px-per-scroll");
parallaxElements[i].style.top = (position + parseInt(movePx))+"px";
}
}
lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling
}
I noticed when I scroll from top to bottom I get more console.log("bottom") messages. * When I scroll top > bottom and vise versa.
So I guess that is the reason why the element is not in the same position too when I go bottom > top.
http://prntscr.com/ka9osq
How can I fix this?
EDIT: http://jsfiddle.net/r9751p8q/6/
Try to scroll to the bottom and then back you will see that some element disappear
Here is a possible solution of your problem.
HTML
<div class="section">
<div style="top: 100px; left: 46.3%;" data-px-per-scroll="0.5" data-initial-position="100" class="l-parallax_item"></div>
<div style="top: 300px; left: 37.7%;" data-px-per-scroll="0.9" data-initial-position="300" class="l-parallax_item"></div>
<div style="top: 80px; left: 56%;" data-px-per-scroll="0.3" data-initial-position="80" class="l-parallax_item"></div>
<div style="top: 230px; left: 75%;" data-px-per-scroll="0.8" data-initial-position="230" class="l-parallax_item"></div>
<div style="top: 60px;left: 7.1%;" data-px-per-scroll="0.5" data-initial-position="60" class="l-parallax_item"></div>
</div>
JS
function parallaxBlocks(){
let parallaxElements = document.getElementsByClassName("l-parallax_item");
let st = window.pageYOffset || document.documentElement.scrollTop;
let elementsLength = parallaxElements.length;
for(i = 0; i < elementsLength; i++) {
let movePx = 1 + parseFloat(parallaxElements[i].dataset["pxPerScroll"]);
let position = 1 + parseInt(parallaxElements[i].dataset["initialPosition"]);
parallaxElements[i].style.top = Math.floor(position - (st * movePx)) + 'px';
}
}
window.addEventListener('scroll', parallaxBlocks)
But I also changed your html. For working example check this fiddle .
And some explanation what I'm actually doing in the JS code:
In this code we don't need to check if we scroll up or down because the logic of the positioning of the elements is shifted form what was their current position (as in your code) to what was their initial position.
I'm using let to declare variables instead of var for scope reasons (and because I love new things). Still if you are going to support older browsers you might want to check caniuse for compatibilities.
For the top css property I'm using fixed values, and I store their original top offset in a new data attribute called initial-position. This initial position will be used later to calculate the new position of each element. If you need a % values then you can keep the top property with % value, but you will need also another loop to go through all the .l-parallax_item and check their offset from the top and record this value in their data-initial-position.
Note that I'm using dataset instead getAttribute. dataset is made for all data attributes. And see how the cebab-case became camelCase. More to read here
Also the px-per-scroll is no longer a fixed amount of pixels instead it is a ratio from the scroll offset from top. You can play with the fiddle to see how it works.
Bonus: why I added another variable for the elements length instead just using it in the for loop arguments.
I believe that there is another way to do this but hope that this one will help you.
The reason it's not working:
Scrolling is not precise and because you base your position on the total sum of scrolls the positioning will end up unpredictable. To show you what I mean:
Your console.log() fires every time you scroll. If you change both logs to console.log(parallaxElements[0].style.top) it will show you the top position of the first parallaxed element each time you make a scroll move. Now take your mouse and scroll one "tick" down, then one "tick" up and repeat many times. The position numbers will not be the same every time, and therein lies the problem.
Solution:
Base your parallax elements position on the actual pageYOffset. Since Ale already posted a working solution with this in mind one more code example is reduntant.
Using JavaScript, I dynamically create a <div>(call it 'popup'), populate it with content, insert it into the DOM, and then attempt to position it relative to (.clientX, .clientY) of the click event.
The positioning scheme is simple. If .clientX is in the left half of the view port, I want popup's left edge to be at .clientX. If .clientX is in the right half of the view port, I want popup's right edge to be at .clientX. Similarly for .clientY. If it is in the top half of the view port, I want popup's top edge at .clientY; if in the bottom half, popup's bottom edge should be at .clientY.
I have the horizontal alignment working correctly but can not get the vertical to work.
The algorithm I'm using is:
function positionPopupOnPage( evt ) {
var vpWH = [];
var vpW, vpH;
var coordX = evt.clientX;
var coordY = evt.clientY;
vpWH = getViewPortWidthHeight();
vpW = vpWH[0];
vpH = vpWH[1];
popup.style.position = 'absolute';
// if not display: block, .offsetWidth & .offsetHeight === 0
popup.style.display = 'block';
popup.style.zIndex = '10100';
if ( coordX > vpW/2 ) { coordX -= popup.offsetWidth; }
if ( coordY > vpH/2 ) { coordY -= popup.offsetHeight; }
popup.style.top = coordY + 'px';
popup.style.left = coordX + 'px';
} // end fn positionPopupOnPage
The function call was positionPopupOnPage(event).The function getViewPortWidthHeight() is the one given in answer to the stackoverflow question Find the exact height and width of the viewport in a cross-browser way (no Prototype/jQuery).
The problem is that popup's top/bottom edge does not align with .clientY. In the screenshot below, (.clientX, .clientY) was the the "C" in "Charlie Fowler" which is where the mouse clicked. But popup's bottom edge is way above that position.
.
--> EDIT 0 <-- (in response to #user2330270's remarks)
Popup is inserted as high up the DOM tree as possible, as the first child of <body>. The function call is:
// insert popup above the first child of <body>
parentNode = document.getElementsByTagName("body")[0];
targetNode = parentNode.children[0];
insertPopup( parentNode, targetNode );
The function definition is:
function insertPopup( parentNode, targetNode ) {
parentNode.insertBefore(popup, targetNode);
popup.classList.add( 'popup')
existsPopup = true;
} // end fn insertPopup
There is a Pen, Table Play, at CodePen. It is the full code. The definition of positionPopupOnPage() is the third up from the bottom in the JS area, beginning at line 233.
The only CSS reaching popup is:
.popup {
position: absolute;
border: 2px solid #000;
border-radius: 10px;
width: 200px;
height: 250px;
overflow-y: auto;
background-color: rgba(0, 0, 0, .5);
color: #fff;
z-index: 1000;
display: none;
}
and the JS style assignments in positionPopupOnPage() as given above.
--> End Edit 0 <--
-->Edit 1<--
Correct the statement of the problem.The function does not work in Safari or Firefox as was initially erroneously reported. Update the positioning function to the one currently used in the Pen.
-->End Edit 1<--
Can someone help determine what is happening and how I can get popup's top/bottom edge to align with .clientY in Chrome?
Your effort and interest in my question are much appreciated. Thank you.
-Steve
From looking at your algorithm it looks right. What I would suggest is set the top position as 0px and see where it lies. If it is not at the top of the page, then you know you have CSS stopping it working correctly.
Maybe try making a simplified fiddle and you could be assisted further. The popup should be as top a level as possible in the HTML tree to keep things easy and reliable. This may also fix your issue if it is not.
The trick, for me, was to realize 4 things:
An absolutely positioned element, which the div popup is, is positioned from the page top not the view port top.
As the page is scrolled up, one has to account for the distance scrolled up, i.e. the distance from the view port top to the page top.
That distance is obtained by document.body.scrollTop.
Add that distance to the distance from the click point to the view port top, i.e. event.clientY, to get the total distance to use in setting popup's CSS top property.
The correct function to solve the problem then becomes:
// positon popup on page relative to cursor
// position at time of click event
function positionPopupOnPage( evt ) {
var VPWH = []; // view port width / height
var intVPW, intVPH; // view port width / height
var intCoordX = evt.clientX;
var intCoordY = evt.clientY; // distance from click point to view port top
var intDistanceScrolledUp = document.body.scrollTop;
// distance the page has been scrolled up from view port top
var intPopupOffsetTop = intDistanceScrolledUp + intCoordY;
// add the two for total distance from click point y to top of page
var intDistanceScrolledLeft = document.body.scrollLeft;
var intPopupOffsetLeft = intDistanceScrolledLeft + intCoordX;
VPWH = getViewPortWidthHeight(); // view port Width/Height
intVPW = VPWH[0];
intVPH = VPWH[1];
popup.style.position = 'absolute';
// if not display: block, .offsetWidth & .offsetHeight === 0
popup.style.display = 'block';
popup.style.zIndex = '10100';
if ( intCoordX > intVPW/2 ) { intPopupOffsetLeft -= popup.offsetWidth; }
// if x is in the right half of the viewport, pull popup left by its width
if ( intCoordY > intVPH/2 ) { intPopupOffsetTop -= popup.offsetHeight; }
// if y is in the bottom half of view port, pull popup up by its height
popup.style.top = intPopupOffsetTop + 'px';
popup.style.left = intPopupOffsetLeft + 'px';
} // end fn positionPopupOnPage
With thanks to user ershwetabansal on CSS Tricks for leading me to point 2 above.
I'm currently working on an iPad JS/HTML app, using native scrolling to view a large bar chart svg.
I'm looking to have the labeling along the x and y axes persist along the top and left of the graph. Basically, the graph should slide around freely underneath the labels, which themselves will only move in the appropriate direction (eg, the x axis header will shift its labels over as you scroll left, and the y axes labelings for vertical scrolling)
I currently have some css to do this, but the native scrolling moves a lot faster than my javascript to sync the panels up. There's sort of this elastic interplay as one element is dragged faster than the other. It all plops into the correct place once the scroll animation stops, but the interaction looks pretty janky when scrolling is going on.
Is there a better way to tackle this problem? Are their other events I could tap into? Is there a way to force multiple scrollable divs to react to the same scroll event without manual js position calculations? Or is the lag unavoidable due to native scrolling being offloaded to the gpu?
/* CSS */
.fixedaxis {
position:absolute;
top: 0px;
left: 0px;
background: blue;
}
#chartheader {
z-index:5;
}
#sidebar {
z-index:6;
}
.scroll {
overflow: auto;
-webkit-overflow-scrolling: touch;
}
/* relevant html */
<div id="content" style="position: relative;">
<div id="chartheader" class="fixedaxis"></div>
<div id="sidebar" class="fixedaxis"></div>
<div class="scroll">
<section id="barchart">
</section>
</div>
</div>
/*javascript */
var scroller = $('.scroll');
var tableHeader = $('#chartheader');
var sidebar = $('#sidebar');
scroller.on('scroll', function() {
console.log("scrolling: " + this.scrollLeft);
tableHeader.css('left', (-1 * this.scrollLeft) + 'px');
sidebar.css('top', (-1 * this.scrollTop) + 'px');
});
So it turns out, you can't. At least, not at this point in time.
According to the Apple Developer docs
https://developer.apple.com/library/ios/#documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html
You receive events for scroll when the user's finger is on the screen, and one final scroll event once momentum stops. If the content is gliding around between those two periods, you receive no events.
What we ended up doing is making the sidebar content semi-transparent on the second scroll event, and then making it opaque again after a timeout, with an allowance for repositioning if a single "momentum-end" scroll event fired again.
Something along the lines of:
var lastTimeout;
var numScrolls = 1;
var startTop = 0;
function(event) {
var elem = event.target;
startTop = startTop || elem.scrollTop;
if (lastTimeout) {
clearTimeout(lastTimeout);
} else if (numScrolls == -1) {
/* I've omitted some short circuit logic for other scrolling cases
* but that's why we're going off of -1 here
*/
// stray scroll from native scroll end
$labels[0].scrollTop = elem.scrollTop;
$labels.css('opacity', '1');
startTop = null;
}
// wait for two consecutive scrolls
if (numScrolls > 0) {
$labels.css('opacity', '0.3');
}
++numScrolls;
lastTimeout = setTimeout(function() {
console.log(elem.scrollTop);
$labels[0].scrollTop = elem.scrollTop;
$labels.css('opacity', '1');
lastTimeout = null;
numScrolls = -1;
startTop = null;
}, 1000);
};
I'm facing an strange problem.
I capture the mouse movements with:
var mmoves = [];
jQuery(document).mousemove(function(event) {
mmoves.push({x:event.pageX, y:event.pageY})
}
Then I attach a div to the page like:
$("body").append('<div id="mouseemul" style="padding:0; margin:0; color: red; background-color: blue; width: 1px; height: 1px;">*</div>');
and then try to playback the moves
It works ok on most pages but on some pages the playback starts ("*" initial position) some pixels to the right (x). The y is ok but the x is about 120px to the right. On other pages it is accurate. On the not accurate pages, when the mouse goes close the right scrollbar it goes beyond the right page border and produces a horizontal scrollbar.
I think this has to do with some css styling of the page being playback.
Does anybody has an idea what may be causing this ?
How could I get the actual offset (in case there is an offset for such pages) ?
Thanks a lot,
Hernan
--Edited--
It is obvious that the x displacement is due to the positioning of the main document. The first element gives a $.position() of 0,134 and if I SUBSTRACT that amount from the recorded data the playback is accurate. The problem is that this displacement does not happen in every page and I dont know how to figure out when the displacement occurs and when not (to correct it by substracting).
Recording
If you want to capture and replay mouse movement you can try "recording" from the document.
This would use the x and y chords from the window.
To do this you can use the document DOM element:
var m = [];
// Using the document instead of body might solve your issue
$( document ).mousemove(function( e ){
m.push({ x : e.pageX, y : e.pageY });
});
Replaying
HTML/CSS
Your HTML/CSS should be a div on the page set with position: fixed which should match your javascript chord samples as fixed is absolutely positioned to the window:
<style>
.replay {
/* Use position fixed to match window chords */
position: fixed;
top: 0;
left: 0;
/* These are just for show */
border-radius: 20px;
background: red;
width: 10px;
height: 10px;
}
</style>
<div class="replay"></div>
Javascript
To replay your captured chords you can use something like this:
var $replay = $('.replay'), // Get mouse simulator
i = 0, l = m.length,
pos, t;
// Recursive animation function
function anim(){
// Cache current position
pos = m[i];
// Move to next position
$replay.css({ top: pos.y, left: pos.x });
i++;
// Exit recursive loop
if ( i === l )
clearTimeout( t );
// Or keep going
else
t = setTimeout(anim, 100); // Timeout speed controls animation speed
}
// Start animation loop
anim();
Demo
Try it out on this demo.
I have something vaguely like the following:
<div id="body">
surrounding text
<div id="pane" style="overflow: auto; height: 500px; width: 500px;">
lots and lots of text here
<span id="some_bit">tooltip appears below-right of here</span>
</div>
more surrounding text (should be overlapped by tooltip)
</div>
and:
<div id="tooltip" style="width: 100px; height: 100px;">Whee</div>
What I want to do is insert the tooltip such that it is positioned above the pane it's in. If it's attached to an element that's next to the pane boundary (like above), then it should be visible above the pane, and above the text surrounding the pane.
It should NOT a) extend the pane, such that you have to scroll down to see the tooltip (like in http://saizai.com/css_overlap.png), or b) be cut off, so you can't see all of the tooltip.
I'm inserting this with JS, so I can add a wrapper position:relative div or the like if needed, calculate offsets and make it position:absolute, etc. I would prefer to not assume anything about the pane's position property - the tooltip should be insertable with minimal assumptions of possible page layout. (This is just one example case.)
It's for a prototype tooltip library I'm writing that will be open source.
ETA: http://jsfiddle.net/vCb2y/5/ behaves visually like I want (if you keep re-hovering the trigger text), but would require me to update the position of the tooltip on all DOM changes and scrolling behavior. I would rather the tooltip be positioned with pure CSS/HTML so that it has the same visual behavior (i.e. it overlaps all other elements) but stays in its position relative to the target under DOM changes, scrolling, etc.
ETA 2: http://tjkdesign.com/articles/z-index/teach_yourself_how_elements_stack.asp (keep defaults except set cyan div 'a' to position:relative; imagine 'A' is the pane and 'a' the tooltip) seems to more closely behave as I want, but I've not been able to get it to work elsewhere. Note that if you make 'A' overflow: auto, it breaks the overlapping behavior of 'a'.
I can't think of a pure HTML/CSS solution for this.
The overflow declaration is the issue here. If the tooltip is in #pane:
you establish a positioning context within #pane, then the tooltip shows next to #some_bit (regardless of scrolling, etc.) but it gets cut-off.
you do not establish a positioning context, then the tooltip is not clipped but it has no clue where #some_bit is on the page.
I'm afraid you'll need JS to monitor where #some_bit is on the page and position the tooltip accordingly. You'd also need to kill that tooltip as soon as #some_bit is outside of the viewing area (not an issue if the trigger is mouseover).
Actually, if the trigger is mouseover then you may want to use the cursor coordinates to position the tooltip (versus calculating the position of #some_bit).
I would just put the tooltip outside of the #pane div and position it absolutely using JavaScript since you're using JS anyway.
I don't use Prototype so I don't know how it's done in Prototype, but in jQuery, you'd use $(element).position() to get the element position. If you have to do it manually, it's a little more complicated.
And you'll probably want to add a little extra logic to prevent the tooltip from extending outside of the document.
Edit: CSS used
#tooltip {
z-index: 9999;
display: none;
position: absolute;
}
JS used
Note: in jQuery, but it should be easy to change it to Prototype syntax.
$('#some_bit').hover(function() {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
// hovered element
var offset = $(this).offset();
var top = offset.top + docViewTop;
var left = offset.left;
var width = $(this).width();
var height = $(this).height();
var right = left + width;
var bottom = top + height;
// pane
var poffset = $('#pane').offset();
var ptop = poffset.top + docViewTop;
var pleft = poffset.left;
var pwidth = $('#pane').width();
var pheight = $('#pane').height();
var pright = pleft + pwidth;
var pbottom = ptop + pheight;
// tooltip
var ttop = bottom;
var tleft = right;
var twidth = $('#tooltip').width();
var theight = $('#tooltip').height();
var tright = tleft + twidth;
var tbottom = ttop + theight;
if (tright > pright)
tleft = pright - twidth;
if (tbottom > pbottom)
ttop = pbottom - theight;
if (tbottom > docViewBottom)
ttop = docViewBottom - theight;
$('#tooltip').offset({top: ttop, left: tleft});
$('#tooltip').css('display', 'block');
}, function() {
$('#tooltip').hide();
});
Edit: See it here.