Following this answer to a similar question Make position: fixed behavior like sticky (for Vue2), I have tried to implement it in my application.
The solution was a little bit buggy (in some cases it behaved oddly, especially when opening other tabs and coming back), so I decided to implement it using jQuery and it's working as expected.
Here is the working example:
<template>
<div>
<div class="recap">
<div class="inner" :style="recapStyle">
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ProductRecap',
data() {
return {
scrollY: null,
top: null,
bottom: null,
marginTop: 40,
recapStyle: {},
};
},
methods: {
updatePosition(scroll) {
// using jQuery to calculate amount
const offset = $(this.$el).offset().top;
const scrollAmount = offset - scroll;
const rectHeight = $(this.$el).find('.inner').outerHeight();
if (scrollAmount < this.top) {
let updatedTop = scroll - offset + this.top;
if ((scroll + rectHeight) < this.bottom) {
this.prevScroll = updatedTop;
} else {
updatedTop = this.prevScroll;
}
this.$set(this.recapStyle, 'top', `${updatedTop}px`);
} else {
this.$delete(this.recapStyle, 'top');
}
},
},
watch: {
scrollY(scrollUpdate) {
// call `updatePosition` on scroll
this.updatePosition(scrollUpdate);
},
},
mounted() {
// calculate header size (position: fixed) and add a fixed offset
this.top = $('#main-header').outerHeight() + this.marginTop;
// calculate height of the document (without the footer)
this.bottom = document.querySelector('.global-container').offsetHeight;
// update scrollY position
window.addEventListener('scroll', _.throttle(() => {
this.scrollY = Math.round(window.scrollY);
}, 20, { leading: true }));
},
};
</script>
However I'd like to find a solution that doesn't use jQuery to calculate the offset, so I headed to You Might Not Need jQuery, but if I just replace the offset part with the one that's suggested it's still a bit buggy.
$(el).offset();
Should become:
var rect = el.getBoundingClientRect();
{
top: rect.top + document.body.scrollTop,
left: rect.left + document.body.scrollLeft
}
So I replaced the line:
const offset = $(this.$el).offset().top;
with:
const rect = this.$el.getBoundingClientRect();
const offset = rect.top + document.body.scrollTop;
But the distance of the sidebar from the fixed header increases with the scroll: can anyone explain how to fix it?
Here is a working fiddle (slightly simplified): Fiddle
The short answer is to use these two lines (the first one is yours):
const rect = this.$el.getBoundingClientRect();
const offset = rect.top + window.pageYOffset;
The longer answer of course includes the thought process to achieve this result. I ran
console.log($(this.$el).offset + "");
On your fiddle at the relevant place to see how the offset function is implemented and got this:
function( options ) {
// Preserve chaining for setter
if ( arguments.length ) {
return options === undefined ?
this :
this.each( function( i ) {
jQuery.offset.setOffset( this, options, i );
} );
}
var rect, win,
elem = this[ 0 ];
if ( !elem ) {
return;
}
// Return zeros for disconnected and hidden (display: none) elements (gh-2310)
// Support: IE <=11+
// Running getBoundingClientRect on a
// disconnected node in IE throws an error
if ( !elem.getClientRects().length ) {
return { top: 0, left: 0 };
}
// Get document-relative position by adding viewport scroll to viewport-relative gBCR
rect = elem.getBoundingClientRect();
win = elem.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};
}
The solution is inspired from this line:
top: rect.top + win.pageYOffset,
Related
I have an element following my mouse around the page on mousemove. It works fine but when I scroll the page, the mouse does not follow, how do I fix it so that it follows it on scroll?
https://stackblitz.com/edit/angular-lg2p6f?file=src/app/app.component.html
HTML:
<div
class="wrapper"
(mouseenter)="enter()"
(mousemove)="move($event)"
(mouseleave)="leave()"
>
<p>Start editing to see some magic happen :)</p>
<div class="sites-circle" [ngClass]="mouseMove ? 'onMove' : 'notMove'">
hello
</div>
</div>
TS:
enter(source: string) {
this.tooltip.classList.add('show');
}
mouseStopped() {
this.mouseMove = false;
console.log("mousemove: " + this.mouseMove);
}
move(e: { pageX: number; pageY: number }) {
this.mouseMove = true;
console.log("mousemove: " + this.mouseMove);
var timer;
clearTimeout(timer);
timer = setTimeout(this.mouseStopped, 300);
const tooltipStyle = this.tooltip.style;
tooltipStyle.left = e.pageX + 'px';
tooltipStyle.top = e.pageY + 'px';
}
leave() {
this.tooltip.classList.remove('show');
}
CSS:
.sites-circle.onMove {
transform: scale(3);
}
.sites-circle.notMove {
transform: scale(1) !important;
}
According to this answer, we cannot get mouse current position on scroll we can just get how much it scrolled relative to last position.
For your problem, this can be realized as follows (here is your updated stackblitz):
Add a (window:scroll) listener to your <div>.
<div
class="wrapper"
(mouseenter)="enter()"
(mousemove)="move($event)"
(window:scroll)="onScroll($event)"
(mouseleave)="leave()"
>
Hold the last position, e.g.:
lastPosition = {
x: undefined,
y: undefined,
};
...
// inside your move() function
this.lastPosition.x = e.pageX;
this.lastPosition.y = e.pageY;
Hold the last known "move" page offset:
lastMovePageOffset = {
x: undefined,
y: undefined,
};
...
// inside your move() function
this.lastMovePageOffset.y = window.pageYOffset;
Use the difference of the current scroll position and the last known page "move" page offset to calculate the new position:
onScroll(e: any) {
if (this.lastPosition.y !== undefined) {
let pageOffsetY = 0;
if (this.lastMovePageOffset.y !== undefined) {
pageOffsetY = window.pageYOffset - this.lastMovePageOffset.y;
}
this.tooltip.style.top = this.lastPosition.y + pageOffsetY + 'px';
}
}
This can be handled similarly with the x-Position. The solution works as soon as the cursor is moved once before scrolling (caused by the general problem described in the first sentence).
Good luck!
I have a strange issue which I can only replicate on Microsoft browsers (Edge and IE11 tested).
<style>
body {
height: 5000px;
width: 5000px;
}
</style>
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin()">Click me to scroll!</button>
<script>
function scrollWin() {
window.scrollTo({
left: 1000,
top: 1000,
behavior:"smooth"
});
}
</script>
This code correctly scrolls the window 1000px to the left and down, with a smooth behaviour in Chrome and Firefox. However, on Edge and IE, it does not move at all.
As mentioned before, the Scroll Behavior specification has only been implemented in Chrome, Firefox and Opera.
Here's a one-liner to detect support for the behavior property in ScrollOptions:
const supportsNativeSmoothScroll = 'scrollBehavior' in document.documentElement.style;
And here's a simple implementation for cross-browser smooth scrolling: https://gist.github.com/eyecatchup/d210786daa23fd57db59634dd231f341
Maybe not a true answer in the sense of the word, but I have solved this problem by using this helpful polyfill: https://github.com/iamdustan/smoothscroll which works really well across all browsers.
Example page for pollyfill: http://iamdustan.com/smoothscroll/
Many thanks to the author.
You can detect support for the behavior option in scrollTo using this snippet:
function testSupportsSmoothScroll () {
var supports = false
try {
var div = document.createElement('div')
div.scrollTo({
top: 0,
get behavior () {
supports = true
return 'smooth'
}
})
} catch (err) {}
return supports
}
Tested in Chrome, Firefox, Safari, and Edge, and seems to work correctly. If supports is false, you fall back to a polyfill.
Indeed, they don't support this variant, MDN articles should be updated.
One way to polyfill this method is to run the scroll method in a requestAnimationFrame powered loop. Nothing too fancy here.
The main problem that arises is how to detect when this variant is not supported. actually #nlawson's answer tackles this problem perfectly...
For this, we can use the fact that a call to Window#scroll will fire a ScrollEvent if the viewPort actually did scroll.
This means we can set up an asynchronous test that will:
Attach an event handler to the ScrollEvent,
Call a first time scroll(left , top) variant to be sure the Event will fire,
Overwrite this call with a second one using the options variant.
In the event handler, if we aren't at the correct scroll position, this means we need to attach our polyfill.
So the caveat of this test is that it is an asynchronous test. But since you need to actually wait for the document has loaded before calling this method, I guess in 99% of cases it will be ok.
Now to less burden the main doc, and since it is already an asynchronous test, we can even wrap this test inside an iframe, which gives us something like:
/* Polyfills the Window#scroll(options) & Window#scrollTo(options) */
(function ScrollPolyfill() {
// The asynchronous tester
// wrapped in an iframe (will not work in SO's StackSnippetĀ®)
var iframe = document.createElement('iframe');
iframe.onload = function() {
var win = iframe.contentWindow;
// listen for a scroll event
win.addEventListener('scroll', function handler(e){
// when the scroll event fires, check that we did move
if(win.pageXOffset < 99) { // !== 0 should be enough, but better be safe
attachPolyfill();
}
// cleanup
document.body.removeChild(iframe);
});
// set up our document so we can scroll
var body = win.document.body;
body.style.width = body.style.height = '1000px';
win.scrollTo(10, 0); // force the event
win.scrollTo({left:100, behavior:'instant'}); // the one we actually test
};
// prepare our frame
iframe.src = "about:blank";
iframe.setAttribute('width', 1);
iframe.setAttribute('height', 1);
iframe.setAttribute('style', 'position:absolute;z-index:-1');
iframe.onerror = function() {
console.error('failed to load the frame, try in jsfiddle');
};
document.body.appendChild(iframe);
// The Polyfill
function attachPolyfill() {
var original = window.scroll, // keep the original method around
animating = false, // will keep our timer's id
dx = 0,
dy = 0,
target = null;
// override our methods
window.scrollTo = window.scroll = function polyfilledScroll(user_opts) {
// if we are already smooth scrolling, we need to stop the previous one
// whatever the current arguments are
if(animating) {
clearAnimationFrame(animating);
}
// not the object syntax, use the default
if(arguments.length === 2) {
return original.apply(this, arguments);
}
if(!user_opts || typeof user_opts !== 'object') {
throw new TypeError("value can't be converted to a dictionnary");
}
// create a clone to not mess the passed object
// and set missing entries
var opts = {
left: ('left' in user_opts) ? user_opts.left : window.pageXOffset,
top: ('top' in user_opts) ? user_opts.top : window.pageYOffset,
behavior: ('behavior' in user_opts) ? user_opts.behavior : 'auto',
};
if(opts.behavior !== 'instant' && opts.behavior !== 'smooth') {
// parse 'auto' based on CSS computed value of 'smooth-behavior' property
// But note that if the browser doesn't support this variant
// There are good chances it doesn't support the CSS property either...
opts.behavior = window.getComputedStyle(document.scrollingElement || document.body)
.getPropertyValue('scroll-behavior') === 'smooth' ?
'smooth' : 'instant';
}
if(opts.behavior === 'instant') {
// not smooth, just default to the original after parsing the oject
return original.call(this, opts.left, opts.top);
}
// update our direction
dx = (opts.left - window.pageXOffset) || 0;
dy = (opts.top - window.pageYOffset) || 0;
// going nowhere
if(!dx && !dy) {
return;
}
// save passed arguments
target = opts;
// save the rAF id
animating = anim();
};
// the animation loop
function anim() {
var freq = 16 / 300, // whole anim duration is approximately 300ms #60fps
posX, poxY;
if( // we already reached our goal on this axis ?
(dx <= 0 && window.pageXOffset <= +target.left) ||
(dx >= 0 && window.pageXOffset >= +target.left)
){
posX = +target.left;
}
else {
posX = window.pageXOffset + (dx * freq);
}
if(
(dy <= 0 && window.pageYOffset <= +target.top) ||
(dy >= 0 && window.pageYOffset >= +target.top)
){
posY = +target.top;
}
else {
posY = window.pageYOffset + (dx * freq);
}
// move to the new position
original.call(window, posX, posY);
// while we are not ok on both axis
if(posX !== +target.left || posY !== +target.top) {
requestAnimationFrame(anim);
}
else {
animating = false;
}
}
}
})();
Sorry for not providing a runable demo inside the answer directly, but StackSnippetĀ®'s over-protected iframes don't allow us to access the content of an inner iframe on IE...
So instead, here is a link to a jsfiddle.
Post-scriptum:
Now comes to my mind that it might actually be possible to check for support in a synchronous way by checking for the CSS scroll-behavior support, but I'm not sure it really covers all UAs in the history...
Post-Post-scriptum:
Using #nlawson's detection we can now have a working snippet ;-)
/* Polyfills the Window#scroll(options) & Window#scrollTo(options) */
(function ScrollPolyfill() {
// The synchronous tester from #nlawson's answer
var supports = false
test_el = document.createElement('div'),
test_opts = {top:0};
// ES5 style for IE
Object.defineProperty(test_opts, 'behavior', {
get: function() {
supports = true;
}
});
try {
test_el.scrollTo(test_opts);
}catch(e){};
if(!supports) {
attachPolyfill();
}
function attachPolyfill() {
var original = window.scroll, // keep the original method around
animating = false, // will keep our timer's id
dx = 0,
dy = 0,
target = null;
// override our methods
window.scrollTo = window.scroll = function polyfilledScroll(user_opts) {
// if we are already smooth scrolling, we need to stop the previous one
// whatever the current arguments are
if(animating) {
clearAnimationFrame(animating);
}
// not the object syntax, use the default
if(arguments.length === 2) {
return original.apply(this, arguments);
}
if(!user_opts || typeof user_opts !== 'object') {
throw new TypeError("value can't be converted to a dictionnary");
}
// create a clone to not mess the passed object
// and set missing entries
var opts = {
left: ('left' in user_opts) ? user_opts.left : window.pageXOffset,
top: ('top' in user_opts) ? user_opts.top : window.pageYOffset,
behavior: ('behavior' in user_opts) ? user_opts.behavior : 'auto',
};
if(opts.behavior !== 'instant' && opts.behavior !== 'smooth') {
// parse 'auto' based on CSS computed value of 'smooth-behavior' property
// But note that if the browser doesn't support this variant
// There are good chances it doesn't support the CSS property either...
opts.behavior = window.getComputedStyle(document.scrollingElement || document.body)
.getPropertyValue('scroll-behavior') === 'smooth' ?
'smooth' : 'instant';
}
if(opts.behavior === 'instant') {
// not smooth, just default to the original after parsing the oject
return original.call(this, opts.left, opts.top);
}
// update our direction
dx = (opts.left - window.pageXOffset) || 0;
dy = (opts.top - window.pageYOffset) || 0;
// going nowhere
if(!dx && !dy) {
return;
}
// save passed arguments
target = opts;
// save the rAF id
animating = anim();
};
// the animation loop
function anim() {
var freq = 16 / 300, // whole anim duration is approximately 300ms #60fps
posX, poxY;
if( // we already reached our goal on this axis ?
(dx <= 0 && window.pageXOffset <= +target.left) ||
(dx >= 0 && window.pageXOffset >= +target.left)
){
posX = +target.left;
}
else {
posX = window.pageXOffset + (dx * freq);
}
if(
(dy <= 0 && window.pageYOffset <= +target.top) ||
(dy >= 0 && window.pageYOffset >= +target.top)
){
posY = +target.top;
}
else {
posY = window.pageYOffset + (dx * freq);
}
// move to the new position
original.call(window, posX, posY);
// while we are not ok on both axis
if(posX !== +target.left || posY !== +target.top) {
requestAnimationFrame(anim);
}
else {
animating = false;
}
}
}
})();
// OP's code,
// by the time you click the button, the polyfill should already be set up if needed
function scrollWin() {
window.scrollTo({
left: 1000,
top: 1000,
behavior: 'smooth'
});
}
body {
height: 5000px;
width: 5000px;
}
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin()">Click me to scroll!</button>
Unfortunately, there is no way for that method to work in these two browsers.
You can check open issues here and see that they have done nothing on this issue.
https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15534521/
You can try to use Element.ScrollLeft and Element.ScrollTop property with Window.scrollTo().
Below is the example which is working with Edge and other browsers.
<html>
<style>
body {
height: 5000px;
width: 5000px;
}
</style>
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin(this)">Click me to scroll!</button>
<script>
function scrollWin(pos) {
window.scrollTo(pos.offsetTop+1000,pos.offsetLeft+1000);
}
</script>
</html>
Smooth behavior is not working with this code.
Reference:
Element.scrollLeft
Element.scrollTop
Regards
Deepak
The "smoothscroll" polyfill supports only "smooth" option. To support all options in scrollIntoViewOptions it's better to use seamless-scroll-polyfill (https://www.npmjs.com/package/seamless-scroll-polyfill)
Worked for me.
Here is a link with explanation https://github.com/Financial-Times/polyfill-library/issues/657
I'm using JQuery Draggable to move items round a grid. Objects snap to a 32x32 grid area. I want to be able to cancel a grid snap if an object is in the same position.
The drag cannot be cancelled, it must just be prevented from entering the square. After it is prevented and moved back to the previous position, if the user continues to drag into a new unoccupied grid position, it must snap to that one.
I've created a demo which serves the purpose explained above however the image glitches when it tries to enter the new position but is then cancelled back to the old position.
https://jsfiddle.net/dtx7my4e/1/
Here's the code in that fiddle:
HTML:
<div class="drop-target">
<div class="drag-item" object-id="0"></div>
<div class="drag-item" style="left: 32px;" object-id="1"></div>
</div>
Javascript:
var objects = [
[0, 0],
[1, 1]
];
$(function() {
$(".drag-item").draggable({
grid: [ 32, 32 ],
containment: '.drop-target',
drag: function (event, obj){
let objectId = $(this).attr('object-id');
var objectPositionX = $(this).position().left / 32;
var objectPositionY = $(this).position().top / 32;
var previousPositionX = Math.floor(objects[objectId][0]);
var previousPositionY = Math.floor(objects[objectId][1]);
if (objectPositionX != previousPositionX || objectPositionY != previousPositionY) {
if(!isObjectInPosition(objects, [objectPositionX, objectPositionY])) {
objects[objectId] = [objectPositionX, objectPositionY];
} else {
obj.position.left = previousPositionX * 32;
obj.position.top = previousPositionY * 32;
}
}
}
});
});
function isObjectInPosition(arrayToSearch, positionToFind)
{
for (let i = 0; i < arrayToSearch.length; i++) {
if (arrayToSearch[i][0] == positionToFind[0]
&& arrayToSearch[i][1] == positionToFind[1]) {
return true;
}
}
return false;
}
CSS:
.drag-item {
background-image: url("http://i.imgur.com/lBIWrWw.png");
background-size: 32px auto;
width: 32px;
height: 32px;
cursor: move;
}
.drop-target {
background: whitesmoke url("http://i.imgur.com/uUvTRLx.png") repeat scroll 0 0 / 32px 32px;
border: 1px dashed orange;
height: 736px;
left: 0;
position: absolute;
top: 0;
width: 736px;
}
Thank you, any help is greatly appreciated.
Toby.
If you're willing to modify draggable itself, I think it would make the logic easier to apply. Once the drag event is triggered you can do lots of things, but you have much more control if you modify the _generatePosition method of draggable. It may look more complicated at first but for this kind of behavior, it's sometimes easier to work.
Basically, you can run your isInPosition function after the check for grid and containment has been applied. Normally next step is to set the new position, but if your isInPosition returns true, you prevent dragging. Something like this:
'use strict'
// This is the function generating the position by calculating
// mouse position, different offsets and option.
$.ui.draggable.prototype._generatePosition = function(event, constrainPosition) {
var containment, co, top, left,
o = this.options,
scrollIsRootNode = this._isRootNode(this.scrollParent[0]),
pageX = event.pageX,
pageY = event.pageY;
// Cache the scroll
if (!scrollIsRootNode || !this.offset.scroll) {
this.offset.scroll = {
top: this.scrollParent.scrollTop(),
left: this.scrollParent.scrollLeft()
};
}
/*
* - Position constraining -
* Constrain the position to a mix of grid, containment.
*/
// If we are not dragging yet, we won't check for options
if (constrainPosition) {
if (this.containment) {
if (this.relativeContainer) {
co = this.relativeContainer.offset();
containment = [
this.containment[0] + co.left,
this.containment[1] + co.top,
this.containment[2] + co.left,
this.containment[3] + co.top
];
} else {
containment = this.containment;
}
if (event.pageX - this.offset.click.left < containment[0]) {
pageX = containment[0] + this.offset.click.left;
}
if (event.pageY - this.offset.click.top < containment[1]) {
pageY = containment[1] + this.offset.click.top;
}
if (event.pageX - this.offset.click.left > containment[2]) {
pageX = containment[2] + this.offset.click.left;
}
if (event.pageY - this.offset.click.top > containment[3]) {
pageY = containment[3] + this.offset.click.top;
}
}
if (o.grid) {
//Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950)
top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY;
pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX;
pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
}
if (o.axis === "y") {
pageX = this.originalPageX;
}
if (o.axis === "x") {
pageY = this.originalPageY;
}
}
// This is the only part added to the original function.
// You have access to the updated position after it's been
// updated through containment and grid, but before the
// element is modified.
// If there's an object in position, you prevent dragging.
if (isObjectInPosition(objects, [pageX - this.offset.click.left - this.offset.parent.left, pageY - this.offset.click.top - this.offset.parent.top])) {
return false;
}
return {
top: (
pageY - // The absolute mouse position
this.offset.click.top - // Click offset (relative to the element)
this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.parent.top + // The offsetParent's offset without borders (offset + border)
(this.cssPosition === "fixed" ? -this.offset.scroll.top : (scrollIsRootNode ? 0 : this.offset.scroll.top))
),
left: (
pageX - // The absolute mouse position
this.offset.click.left - // Click offset (relative to the element)
this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.parent.left + // The offsetParent's offset without borders (offset + border)
(this.cssPosition === "fixed" ? -this.offset.scroll.left : (scrollIsRootNode ? 0 : this.offset.scroll.left))
)
};
}
var objects = [
[0, 0],
[1, 1]
];
$(function() {
$(".drag-item").draggable({
grid: [32, 32],
containment: '.drop-target',
// on start you remove coordinate of dragged item
// else it'll check its own coordinates
start: function(event, obj) {
var objectId = $(this).attr('object-id');
objects[objectId] = [null, null];
},
// on stop you update your array
stop: function(event, obj) {
var objectId = $(this).attr('object-id');
var objectPositionX = $(this).position().left / 32;
var objectPositionY = $(this).position().top / 32;
objects[objectId] = [objectPositionX, objectPositionY];
}
});
});
function isObjectInPosition(arrayToSearch, positionToFind) {
for (let i = 0; i < arrayToSearch.length; i++) {
if (arrayToSearch[i][0] === (positionToFind[0] / 32) && arrayToSearch[i][1] === (positionToFind[1] / 32)) {
return true;
}
}
return false;
}
https://jsfiddle.net/bfc4tsrh/1/
how to .postion of jquery in mm rather tham px
$("#status_div").text("Offset Left:"+ui.offset.left.toFixed(0) + " Offset Top:" + ui.offset.top.toFixed(0)
+ " Position Left: "+ui.position.left.toFixed(0) +" Position Top: "+ui.position.top.toFixed(0) );
In order to get the position in millimeters rather than pixels, the first thing you'll have to do is find out how many pixels/millimeter there are in the user's display (in each dimension). You can do that by creating an element, positioning it absolutely in mm, and then getting its position from offset (which will give it to you in pixels). For example:
var div = $("<div>").css({
position: "absolute",
left: "100mm",
top: "100mm"
}).appendTo(document.body);
var pos = div.offset();
div.remove();
var pixelsPerMM = {
x: pos.left / 100,
y: pos.top / 100
};
Live Example | Live Source
Then you can use offset (or position if you want the number relative to the parent positioning element) and do the math.
EXTEND jQuery's position function, I used its jQuery's own code and added px to mm multiplier
Explanation :-
Extend jQuery's Position method.
To extend Download http://code.jquery.com/jquery-1.10.2.js.
copy line code from line number 9632 to 9680 and add some chnage to it
1 px = 0.264583333 mm
While returning the top & left multiply it by 0.264583333
$(function (){
var docElem = document.documentElement;
jQuery.fn.extend({
position: function() {
if ( !this[ 0 ] ) {
return;
}
var offsetParent, offset,
parentOffset = { top: 0, left: 0 },
elem = this[ 0 ];
var pxToMmMultiplier = 0.264583333;
if ( jQuery.css( elem, "position" ) === "fixed" ) {
// we assume that getBoundingClientRect is available when computed position is fixed
offset = elem.getBoundingClientRect();
} else {
// Get *real* offsetParent
offsetParent = this.offsetParent();
// Get correct offsets
offset = this.offset();
if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
parentOffset = offsetParent.offset();
}
// Add offsetParent borders
parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
}
return {
top: (offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true )) * pxToMmMultiplier,
left: (offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)) * pxToMmMultiplier
};
},
offsetParent: function() {
return this.map(function() {
var offsetParent = this.offsetParent || docElem;
while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || docElem;
});
}
});
$("#testPos").text(JSON.stringify(($("#testPos").position())));
$("#testPos2").text(JSON.stringify(($("#testPos2").position())));
});
JSFIDDLE DEMO
This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
jQuery - Check if element is visible after scroling
I'm trying to determine if an element is visible on screen. In order to to this, I'm trying to find the element's vertical position using offsetTop, but the value returned is not correct. In this case, the element is not visible unless you scroll down. But despite of this, offsetTop returns a value of 618 when my screen height is 703, so according to offsetTop the element should be visible.
The code I'm using looks like this:
function posY(obj)
{
var curtop = 0;
if( obj.offsetParent )
{
while(1)
{
curtop += obj.offsetTop;
if( !obj.offsetParent )
{
break;
}
obj = obj.offsetParent;
}
} else if( obj.y )
{
curtop += obj.y;
}
return curtop;
}
Thank you in advance!
--- Shameless plug ---
I have added this function to a library I created
vanillajs-browser-helpers: https://github.com/Tokimon/vanillajs-browser-helpers/blob/master/inView.js
-------------------------------
Intersection Observer
In modern browsers you can use the IntersectionObserver which detects where an element is on the screen or compared to a parent.
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
Today I would probably lean toward this API if I need to detect and react to when an element has entered or exited the screen.
But for a quick test/lookup when you just want to verify if an emelemt is currently on screen I would go with the version just below using the getBoundingClientRect.
Using getBoundingClientRect
Short version
This is a lot shorter and should do it as well:
function checkVisible(elm) {
var rect = elm.getBoundingClientRect();
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
}
with a fiddle to prove it: http://jsfiddle.net/t2L274ty/1/
Longer version
And a version with threshold and mode included:
function checkVisible(elm, threshold, mode) {
threshold = threshold || 0;
mode = mode || 'visible';
var rect = elm.getBoundingClientRect();
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
var above = rect.bottom - threshold < 0;
var below = rect.top - viewHeight + threshold >= 0;
return mode === 'above' ? above : (mode === 'below' ? below : !above && !below);
}
and with a fiddle to prove it: http://jsfiddle.net/t2L274ty/2/
A more traditional way to do it
As BenM stated, you need to detect the height of the viewport + the scroll position to match up with your top position. The function you are using is ok and does the job, though its a bit more complex than it needs to be.
If you don't use jQuery then the script would be something like this:
function posY(elm) {
var test = elm, top = 0;
while(!!test && test.tagName.toLowerCase() !== "body") {
top += test.offsetTop;
test = test.offsetParent;
}
return top;
}
function viewPortHeight() {
var de = document.documentElement;
if(!!window.innerWidth)
{ return window.innerHeight; }
else if( de && !isNaN(de.clientHeight) )
{ return de.clientHeight; }
return 0;
}
function scrollY() {
if( window.pageYOffset ) { return window.pageYOffset; }
return Math.max(document.documentElement.scrollTop, document.body.scrollTop);
}
function checkvisible( elm ) {
var vpH = viewPortHeight(), // Viewport Height
st = scrollY(), // Scroll Top
y = posY(elm);
return (y > (vpH + st));
}
Using jQuery is a lot easier:
function checkVisible( elm, evalType ) {
evalType = evalType || "visible";
var vpH = $(window).height(), // Viewport Height
st = $(window).scrollTop(), // Scroll Top
y = $(elm).offset().top,
elementHeight = $(elm).height();
if (evalType === "visible") return ((y < (vpH + st)) && (y > (st - elementHeight)));
if (evalType === "above") return ((y < (vpH + st)));
}
This even offers a second parameter. With "visible" (or no second parameter) it strictly checks whether an element is on screen. If it is set to "above" it will return true when the element in question is on or above the screen.
See in action: http://jsfiddle.net/RJX5N/2/
I hope this answers your question.
Could you use jQuery, since it's cross-browser compatible?
function isOnScreen(element)
{
var curPos = element.offset();
var curTop = curPos.top;
var screenHeight = $(window).height();
return (curTop > screenHeight) ? false : true;
}
And then call the function using something like:
if(isOnScreen($('#myDivId'))) { /* Code here... */ };