From 7c7943dc5ea04ec78c67c33bfc73af5cf0ef119d Mon Sep 17 00:00:00 2001 From: theKidOfArcrania Date: Fri, 25 Nov 2016 02:31:52 -0600 Subject: [PATCH 1/4] Fix bug if the immediate offset-parent is not scrollable If the immediate parent was not scrollable, the code would attempt to find the first parent that is scrollable. However, it did not account for any offsets in elY that occur as we find the parent that is scrollable. Hence, it would scroll a bit (a lot) higher than where the intended element actually is. --- jquery.scrollIntoView.js | 225 ++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 112 deletions(-) diff --git a/jquery.scrollIntoView.js b/jquery.scrollIntoView.js index 4ade417..78736ba 100644 --- a/jquery.scrollIntoView.js +++ b/jquery.scrollIntoView.js @@ -3,135 +3,136 @@ * The default browser behavior always places the element at the top or bottom of its container. * This override is smart enough to not scroll if the element is already visible. * + * Fix for if the immediate offset-parent is not scrollable + * * Copyright 2011 Arwid Bancewicz * Licensed under the MIT license * http://www.opensource.org/licenses/mit-license.php * * @date 8 Jan 2013 - * @author Arwid Bancewicz http://arwid.ca + * @author Arwid Bancewicz http://arwid.ca, theKidOfArcrania * @version 0.3 */ - (function($) { - $.fn.scrollIntoView = function(duration, easing, complete) { - // The arguments are optional. - // The first argment can be false for no animation or a duration. - // The first argment could also be a map of options. - // Refer to http://api.jquery.com/animate/. - var opts = $.extend({}, - $.fn.scrollIntoView.defaults); - - // Get options - if ($.type(duration) == "object") { - $.extend(opts, duration); - } else if ($.type(duration) == "number") { - $.extend(opts, { duration: duration, easing: easing, complete: complete }); - } else if (duration == false) { - opts.smooth = false; - } + +(function($) { + $.fn.scrollIntoView = function(duration, easing, complete) { + // The arguments are optional. + // The first argment can be false for no animation or a duration. + // The first argment could also be a map of options. + // Refer to http://api.jquery.com/animate/. + var opts = $.extend({}, + $.fn.scrollIntoView.defaults); - // get enclosing offsets - var elY = Infinity, elH = 0; - if (this.size()==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); - else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); - elH -= elY; + // Get options + if ($.type(duration) == "object") { + $.extend(opts, duration); + } else if ($.type(duration) == "number") { + $.extend(opts, { duration: duration, easing: easing, complete: complete }); + } else if (duration == false) { + opts.smooth = false; + } - // start from the common ancester - var pEl = this.commonAncestor().get(0); + // get enclosing offsets + var elY = Infinity, elH = 0; + if (this.length==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); + else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); + elH -= elY; - var wH = $(window).height(); - - // go up parents until we find one that scrolls - while (pEl) { - var pY = pEl.scrollTop, pH = pEl.clientHeight; - if (pH > wH) pH = wH; - - // case: if body's elements are all absolutely/fixed positioned, use window height - if (pH == 0 && pEl.tagName == "BODY") pH = wH; - - if ( - // it wiggles? - (pEl.scrollTop != ((pEl.scrollTop += 1) == null || pEl.scrollTop) && (pEl.scrollTop -= 1) != null) || - (pEl.scrollTop != ((pEl.scrollTop -= 1) == null || pEl.scrollTop) && (pEl.scrollTop += 1) != null)) { - if (elY <= pY) scrollTo(pEl, elY); // scroll up - else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down - else scrollTo(pEl, undefined) // no scroll - return; - } + // start from the common ancester + var pEl = this.commonAncestor().get(0); - // try next parent - pEl = pEl.parentNode; - } + var wH = $(window).height(); + + // go up parents until we find one that scrolls + while (pEl) { + var pY = pEl.scrollTop, pH = pEl.clientHeight; + if (pH > wH) pH = wH; + + // case: if body's elements are all absolutely/fixed positioned, use window height + if (pH == 0 && pEl.tagName == "BODY") pH = wH; + + // Can we scroll this? (simpler check) + if (pEl.scrollHeight > pH) { + if (elY <= pY) scrollTo(pEl, elY); // scroll up + else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down + else scrollTo(pEl, undefined) // no scroll + return; + } - function scrollTo(el, scrollTo) { - if (scrollTo === undefined) { - if ($.isFunction(opts.complete)) opts.complete.call(el); - } else if (opts.smooth) { - $(el).stop().animate({ scrollTop: scrollTo }, opts); - } else { - el.scrollTop = scrollTo; - if ($.isFunction(opts.complete)) opts.complete.call(el); - } - } - return this; - }; + // try next parent + elY += pEl.offsetTop; //add offset within parent object. + pEl = pEl.offsetParent; + } - $.fn.scrollIntoView.defaults = { - smooth: true, - duration: null, - easing: $.easing && $.easing.easeOutExpo ? 'easeOutExpo': null, - // Note: easeOutExpo requires jquery.effects.core.js - // otherwise jQuery will default to use 'swing' - complete: $.noop(), - step: null, - specialEasing: {} // cannot be null in jQuery 1.8.3 - }; + function scrollTo(el, scrollTo) { + if (scrollTo === undefined) { + if ($.isFunction(opts.complete)) opts.complete.call(el); + } else if (opts.smooth) { + $(el).stop().animate({ scrollTop: scrollTo }, opts); + } else { + el.scrollTop = scrollTo; + if ($.isFunction(opts.complete)) opts.complete.call(el); + } + } + return this; + }; + + $.fn.scrollIntoView.defaults = { + smooth: true, + duration: null, + easing: $.easing && $.easing.easeOutExpo ? 'easeOutExpo': null, + // Note: easeOutExpo requires jquery.effects.core.js + // otherwise jQuery will default to use 'swing' + complete: $.noop(), + step: null, + specialEasing: {} // cannot be null in jQuery 1.8.3 + }; - /* - Returns whether the elements are in view - */ - $.fn.isOutOfView = function(completely) { - // completely? whether element is out of view completely - var outOfView = true; - this.each(function() { - var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; - if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} - else if (completely ? (elY + elH) < pY: elY < pY) {} - else outOfView = false; - }); - return outOfView; - }; + /* + Returns whether the elements are in view + */ + $.fn.isOutOfView = function(completely) { + // completely? whether element is out of view completely + var outOfView = true; + this.each(function() { + var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; + if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} + else if (completely ? (elY + elH) < pY: elY < pY) {} + else outOfView = false; + }); + return outOfView; + }; - /* - Returns the common ancestor of the elements. - It was taken from http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children - It has received minimal testing. - */ - $.fn.commonAncestor = function() { - var parents = []; - var minlen = Infinity; + /* + Returns the common ancestor of the elements. + It was taken from http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children + It has received minimal testing. + */ + $.fn.commonAncestor = function() { + var parents = []; + var minlen = Infinity; - $(this).each(function() { - var curparents = $(this).parents(); - parents.push(curparents); - minlen = Math.min(minlen, curparents.length); - }); + $(this).each(function() { + var curparents = $(this).parents(); + parents.push(curparents); + minlen = Math.min(minlen, curparents.length); + }); - for (var i = 0; i < parents.length; i++) { - parents[i] = parents[i].slice(parents[i].length - minlen); - } + for (var i = 0; i < parents.length; i++) { + parents[i] = parents[i].slice(parents[i].length - minlen); + } - // Iterate until equality is found - for (var i = 0; i < parents[0].length; i++) { - var equal = true; - for (var j in parents) { - if (parents[j][i] != parents[0][i]) { - equal = false; - break; - } - } - if (equal) return $(parents[0][i]); + // Iterate until equality is found + for (var i = 0; i < parents[0].length; i++) { + var equal = true; + for (var j in parents) { + if (parents[j][i] != parents[0][i]) { + equal = false; + break; } - return $([]); + } + if (equal) return $(parents[0][i]); } - -})(jQuery); \ No newline at end of file + return $([]); + } +})(jQuery); From e8b8b85f3fd62ef24c097e1e3be31617c06e0147 Mon Sep 17 00:00:00 2001 From: theKidOfArcrania Date: Fri, 25 Nov 2016 12:12:42 -0600 Subject: [PATCH 2/4] Formatting --- jquery.scrollIntoView.js | 214 +++++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/jquery.scrollIntoView.js b/jquery.scrollIntoView.js index 78736ba..06e733b 100644 --- a/jquery.scrollIntoView.js +++ b/jquery.scrollIntoView.js @@ -15,124 +15,124 @@ */ (function($) { - $.fn.scrollIntoView = function(duration, easing, complete) { - // The arguments are optional. - // The first argment can be false for no animation or a duration. - // The first argment could also be a map of options. - // Refer to http://api.jquery.com/animate/. - var opts = $.extend({}, - $.fn.scrollIntoView.defaults); + $.fn.scrollIntoView = function(duration, easing, complete) { + // The arguments are optional. + // The first argment can be false for no animation or a duration. + // The first argment could also be a map of options. + // Refer to http://api.jquery.com/animate/. + var opts = $.extend({}, + $.fn.scrollIntoView.defaults); - // Get options - if ($.type(duration) == "object") { - $.extend(opts, duration); - } else if ($.type(duration) == "number") { - $.extend(opts, { duration: duration, easing: easing, complete: complete }); - } else if (duration == false) { - opts.smooth = false; - } + // Get options + if ($.type(duration) == "object") { + $.extend(opts, duration); + } else if ($.type(duration) == "number") { + $.extend(opts, { duration: duration, easing: easing, complete: complete }); + } else if (duration == false) { + opts.smooth = false; + } - // get enclosing offsets - var elY = Infinity, elH = 0; - if (this.length==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); - else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); - elH -= elY; + // get enclosing offsets + var elY = Infinity, elH = 0; + if (this.length==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); + else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); + elH -= elY; - // start from the common ancester - var pEl = this.commonAncestor().get(0); + // start from the common ancester + var pEl = this.commonAncestor().get(0); - var wH = $(window).height(); - - // go up parents until we find one that scrolls - while (pEl) { - var pY = pEl.scrollTop, pH = pEl.clientHeight; - if (pH > wH) pH = wH; - - // case: if body's elements are all absolutely/fixed positioned, use window height - if (pH == 0 && pEl.tagName == "BODY") pH = wH; - - // Can we scroll this? (simpler check) - if (pEl.scrollHeight > pH) { - if (elY <= pY) scrollTo(pEl, elY); // scroll up - else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down - else scrollTo(pEl, undefined) // no scroll - return; - } + var wH = $(window).height(); + + // go up parents until we find one that scrolls + while (pEl) { + var pY = pEl.scrollTop, pH = pEl.clientHeight; + if (pH > wH) pH = wH; + + // case: if body's elements are all absolutely/fixed positioned, use window height + if (pH == 0 && pEl.tagName == "BODY") pH = wH; + + // Can we scroll this? (simpler check) + if (pEl.scrollHeight > pH) { + if (elY <= pY) scrollTo(pEl, elY); // scroll up + else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down + else scrollTo(pEl, undefined) // no scroll + return; + } - // try next parent - elY += pEl.offsetTop; //add offset within parent object. - pEl = pEl.offsetParent; - } + // try next parent + elY += pEl.offsetTop; //add offset within parent object. + pEl = pEl.offsetParent; + } - function scrollTo(el, scrollTo) { - if (scrollTo === undefined) { - if ($.isFunction(opts.complete)) opts.complete.call(el); - } else if (opts.smooth) { - $(el).stop().animate({ scrollTop: scrollTo }, opts); - } else { - el.scrollTop = scrollTo; - if ($.isFunction(opts.complete)) opts.complete.call(el); - } - } - return this; - }; - - $.fn.scrollIntoView.defaults = { - smooth: true, - duration: null, - easing: $.easing && $.easing.easeOutExpo ? 'easeOutExpo': null, - // Note: easeOutExpo requires jquery.effects.core.js - // otherwise jQuery will default to use 'swing' - complete: $.noop(), - step: null, - specialEasing: {} // cannot be null in jQuery 1.8.3 - }; + function scrollTo(el, scrollTo) { + if (scrollTo === undefined) { + if ($.isFunction(opts.complete)) opts.complete.call(el); + } else if (opts.smooth) { + $(el).stop().animate({ scrollTop: scrollTo }, opts); + } else { + el.scrollTop = scrollTo; + if ($.isFunction(opts.complete)) opts.complete.call(el); + } + } + return this; + }; + + $.fn.scrollIntoView.defaults = { + smooth: true, + duration: null, + easing: $.easing && $.easing.easeOutExpo ? 'easeOutExpo': null, + // Note: easeOutExpo requires jquery.effects.core.js + // otherwise jQuery will default to use 'swing' + complete: $.noop(), + step: null, + specialEasing: {} // cannot be null in jQuery 1.8.3 + }; - /* - Returns whether the elements are in view - */ - $.fn.isOutOfView = function(completely) { - // completely? whether element is out of view completely - var outOfView = true; - this.each(function() { - var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; - if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} - else if (completely ? (elY + elH) < pY: elY < pY) {} - else outOfView = false; - }); - return outOfView; - }; + /* + Returns whether the elements are in view + */ + $.fn.isOutOfView = function(completely) { + // completely? whether element is out of view completely + var outOfView = true; + this.each(function() { + var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; + if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} + else if (completely ? (elY + elH) < pY: elY < pY) {} + else outOfView = false; + }); + return outOfView; + }; - /* - Returns the common ancestor of the elements. - It was taken from http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children - It has received minimal testing. - */ - $.fn.commonAncestor = function() { - var parents = []; - var minlen = Infinity; + /* + Returns the common ancestor of the elements. + It was taken from http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children + It has received minimal testing. + */ + $.fn.commonAncestor = function() { + var parents = []; + var minlen = Infinity; - $(this).each(function() { - var curparents = $(this).parents(); - parents.push(curparents); - minlen = Math.min(minlen, curparents.length); - }); + $(this).each(function() { + var curparents = $(this).parents(); + parents.push(curparents); + minlen = Math.min(minlen, curparents.length); + }); - for (var i = 0; i < parents.length; i++) { - parents[i] = parents[i].slice(parents[i].length - minlen); - } + for (var i = 0; i < parents.length; i++) { + parents[i] = parents[i].slice(parents[i].length - minlen); + } - // Iterate until equality is found - for (var i = 0; i < parents[0].length; i++) { - var equal = true; - for (var j in parents) { - if (parents[j][i] != parents[0][i]) { - equal = false; - break; + // Iterate until equality is found + for (var i = 0; i < parents[0].length; i++) { + var equal = true; + for (var j in parents) { + if (parents[j][i] != parents[0][i]) { + equal = false; + break; + } + } + if (equal) return $(parents[0][i]); } - } - if (equal) return $(parents[0][i]); + return $([]); } - return $([]); - } })(jQuery); From b763dec958c34f5d31bbd69543091d8343c041fc Mon Sep 17 00:00:00 2001 From: theKidOfArcrania Date: Fri, 25 Nov 2016 12:14:39 -0600 Subject: [PATCH 3/4] Formatting --- jquery.scrollIntoView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jquery.scrollIntoView.js b/jquery.scrollIntoView.js index 06e733b..fb39dd0 100644 --- a/jquery.scrollIntoView.js +++ b/jquery.scrollIntoView.js @@ -10,7 +10,7 @@ * http://www.opensource.org/licenses/mit-license.php * * @date 8 Jan 2013 - * @author Arwid Bancewicz http://arwid.ca, theKidOfArcrania + * @author Arwid Bancewicz http://arwid.ca * @version 0.3 */ From 1e8efaa814f2d4534297d633c2b0b54aceffe617 Mon Sep 17 00:00:00 2001 From: theKidOfArcrania Date: Fri, 25 Nov 2016 15:22:33 -0600 Subject: [PATCH 4/4] Fix edge cases and more robust code This accounts for when the immediate scrollable parent isn't also positioned. --- jquery.scrollIntoView.js | 98 ++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/jquery.scrollIntoView.js b/jquery.scrollIntoView.js index fb39dd0..df5e503 100644 --- a/jquery.scrollIntoView.js +++ b/jquery.scrollIntoView.js @@ -15,7 +15,44 @@ */ (function($) { + + function getViewportHeight(elem) + { + var pH = elem.clientHeight; + var wH = $(window).height(); + + if ((pH > wH && elem.tagName == "BODY") || + (pH == 0 && elem.tagName == "BODY")) + return wH; + // case: if body's elements are all absolutely/fixed positioned, use window height + else + return pH; + } + + function getScrollParent(elem) + { + //Find parent of interest, keep track of scroll offsets. + var scrollOffset = 0; + var pEl = elem; + + // go up parents until we find one that scrolls + + while (pEl) { + // Can we scroll this? (simpler check) + if (pEl.scrollHeight > getViewportHeight(pEl)) + return pEl; + + // try next parent + pEl = pEl.parentElement; + } + + return undefined; + } + $.fn.scrollIntoView = function(duration, easing, complete) { + if (this.length == 0) + return; + // The arguments are optional. // The first argment can be false for no animation or a duration. // The first argment could also be a map of options. @@ -32,38 +69,36 @@ opts.smooth = false; } - // get enclosing offsets - var elY = Infinity, elH = 0; - if (this.length==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); - else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); - elH -= elY; - // start from the common ancester + // and find nearest scrollable parent. var pEl = this.commonAncestor().get(0); - - var wH = $(window).height(); + pEl = getScrollParent(pEl); - // go up parents until we find one that scrolls - while (pEl) { - var pY = pEl.scrollTop, pH = pEl.clientHeight; - if (pH > wH) pH = wH; - - // case: if body's elements are all absolutely/fixed positioned, use window height - if (pH == 0 && pEl.tagName == "BODY") pH = wH; - - // Can we scroll this? (simpler check) - if (pEl.scrollHeight > pH) { - if (elY <= pY) scrollTo(pEl, elY); // scroll up - else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down - else scrollTo(pEl, undefined) // no scroll - return; - } - - // try next parent - elY += pEl.offsetTop; //add offset within parent object. - pEl = pEl.offsetParent; + + // get enclosing offsets relative to scrollable parent. + var pY = pEl.getBoundingClientRect().top; + var pH = getViewportHeight(pEl); + var elTop = Infinity, elBot = -Infinity; + if (this.length==1) + { + var rect = this[0].getBoundingClientRect(); + elTop = rect.top - pY; + elBot = rect.bottom - pY; } - + else this.each(function(i,el){ + var rect = el.getBoundingClientRect(); + elTop = Math.min(elTop, rect.top - pY); + elBot = Math.max(elBot, rect.bottom - pY); + }); + + //Scrolling + if (elTop < 0) + scrollTo(pEl, pEl.scrollTop + elTop); // scroll down + else if (elBot > pH) + scrollTo(pEl, pEl.scrollTop + (elBot - pH)); // scroll up + else + scrollTo(pEl, undefined) // no scroll + function scrollTo(el, scrollTo) { if (scrollTo === undefined) { if ($.isFunction(opts.complete)) opts.complete.call(el); @@ -95,7 +130,12 @@ // completely? whether element is out of view completely var outOfView = true; this.each(function() { - var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; + + var pEl = getScrollParent(this.parentNode), + pY = pEl.scrollTop, + pH = getViewportHeight(pEl), + elY = this.offsetTop, + elH = this.offsetHeight; if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} else if (completely ? (elY + elH) < pY: elY < pY) {} else outOfView = false;