scroll-behavior-polyfill
Advanced tools
Comparing version 1.0.2 to 2.0.1
@@ -1,17 +0,22 @@ | ||
<a name="1.0.2"></a> | ||
## 1.0.2 (2017-09-08) | ||
# [2.0.0](https://github.com/wessberg/scroll-behavior-polyfill/compare/v1.0.2...v2.0.0) (2019-01-09) | ||
* 1.0.2 ([dba31c2](https://github.com/wessberg/scroll-behavior-polyfill/commit/dba31c2)) | ||
* Bumped version ([372ea1e](https://github.com/wessberg/scroll-behavior-polyfill/commit/372ea1e)) | ||
* Fixed an issue ([5c19034](https://github.com/wessberg/scroll-behavior-polyfill/commit/5c19034)) | ||
### Features | ||
* **release:** new major version and rewritten from scratch. ([5647eb3](https://github.com/wessberg/scroll-behavior-polyfill/commit/5647eb3)) | ||
<a name="1.0.1"></a> | ||
### BREAKING CHANGES | ||
* **release:** CSS Stylesheets will no longer be parsed. Instead, you must either set inline styles, an attribute with the same name, or set it imperatively. Of course, you can still use the imperative API. | ||
## [1.0.2](https://github.com/wessberg/scroll-behavior-polyfill/compare/v1.0.1...v1.0.2) (2017-09-08) | ||
## 1.0.1 (2017-09-08) | ||
* 1.0.1 ([212446f](https://github.com/wessberg/scroll-behavior-polyfill/commit/212446f)) | ||
* First commit ([b701121](https://github.com/wessberg/scroll-behavior-polyfill/commit/b701121)) | ||
2467
dist/index.js
(function () { | ||
'use strict'; | ||
'use strict'; | ||
/*! | ||
* Polyfill.js - v0.1.0 | ||
* | ||
* Copyright (c) 2015 Philip Walton <http://philipwalton.com> | ||
* Released under the MIT license | ||
* | ||
* Date: 2015-06-21 | ||
*/ | ||
(function(window, document, undefined){ | ||
/** | ||
* Is true if the browser natively supports the 'scroll-behavior' CSS-property. | ||
* @type {boolean} | ||
*/ | ||
var SUPPORTS_SCROLL_BEHAVIOR = "scrollBehavior" in document.documentElement.style; | ||
'use strict'; | ||
/*! ***************************************************************************** | ||
Copyright (c) Microsoft Corporation. All rights reserved. | ||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use | ||
this file except in compliance with the License. You may obtain a copy of the | ||
License at http://www.apache.org/licenses/LICENSE-2.0 | ||
var reNative = RegExp('^' + | ||
String({}.valueOf) | ||
.replace(/[.*+?\^${}()|\[\]\\]/g, '\\$&') | ||
.replace(/valueOf|for [^\]]+/g, '.+?') + '$' | ||
); | ||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED | ||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, | ||
MERCHANTABLITY OR NON-INFRINGEMENT. | ||
See the Apache Version 2.0 License for specific language governing permissions | ||
and limitations under the License. | ||
***************************************************************************** */ | ||
/** | ||
* Trim any leading or trailing whitespace | ||
*/ | ||
function trim(s) { | ||
return s.replace(/^\s+|\s+$/g,'') | ||
} | ||
var __assign = function() { | ||
__assign = Object.assign || function __assign(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
function __read(o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
} | ||
/** | ||
* Detects the presence of an item in an array | ||
*/ | ||
function inArray(target, items) { | ||
var item | ||
, i = 0; | ||
if (!target || !items) return false | ||
while(item = items[i++]) { | ||
if (target === item) return true | ||
} | ||
return false | ||
} | ||
var ELEMENT_ORIGINAL_SCROLL = Element.prototype.scroll; | ||
var ELEMENT_ORIGINAL_SCROLL_BY = Element.prototype.scrollBy; | ||
/** | ||
* Determine if a method is support natively by the browser | ||
*/ | ||
function isNative(fn) { | ||
return reNative.test(fn) | ||
} | ||
var ELEMENT_ORIGINAL_SCROLL_TO = Element.prototype.scrollTo; | ||
/** | ||
* Determine if a URL is local to the document origin | ||
* Inspired form Respond.js | ||
* https://github.com/scottjehl/Respond/blob/master/respond.src.js#L90-L91 | ||
*/ | ||
var isLocalURL = (function() { | ||
var base = document.getElementsByTagName("base")[0] | ||
, reProtocol = /^([a-zA-Z:]*\/\/)/; | ||
return function(url) { | ||
var isLocal = (!reProtocol.test(url) && !base) | ||
|| url.replace(RegExp.$1, "").split("/")[0] === location.host; | ||
return isLocal | ||
} | ||
}()); | ||
var styleDeclarationPropertyName = "scrollBehavior"; | ||
var styleAttributePropertyName = "scroll-behavior"; | ||
var styleAttributePropertyNameRegex = new RegExp(styleAttributePropertyName + ":\\s*([^;]*)"); | ||
/** | ||
* Determines the scroll behavior to use, depending on the given ScrollOptions and the position of the Element | ||
* within the DOM | ||
* @param {Element|HTMLElement|Window} inputTarget | ||
* @param {ScrollOptions} [options] | ||
* @returns {ScrollBehavior} | ||
*/ | ||
function getScrollBehavior(inputTarget, options) { | ||
// If the given 'behavior' is 'smooth', apply smooth scrolling no matter what | ||
if (options != null && options.behavior === "smooth") | ||
return "smooth"; | ||
var target = "style" in inputTarget ? inputTarget : document.scrollingElement != null ? document.scrollingElement : document.documentElement; | ||
var value; | ||
if ("style" in target) { | ||
// Check if scroll-behavior is set as a property on the CSSStyleDeclaration | ||
var scrollBehaviorPropertyValue = target.style[styleDeclarationPropertyName]; | ||
// Return it if it is given and has a proper value | ||
if (scrollBehaviorPropertyValue != null && scrollBehaviorPropertyValue !== "") { | ||
value = scrollBehaviorPropertyValue; | ||
} | ||
} | ||
if (value == null) { | ||
var attributeValue = target.getAttribute("scroll-behavior"); | ||
if (attributeValue != null && attributeValue !== "") { | ||
value = attributeValue; | ||
} | ||
} | ||
if (value == null) { | ||
// Otherwise, check if it is set as an inline style | ||
var styleAttributeValue = target.getAttribute("style"); | ||
if (styleAttributeValue != null && styleAttributeValue.includes(styleAttributePropertyName)) { | ||
var match = styleAttributeValue.match(styleAttributePropertyNameRegex); | ||
if (match != null) { | ||
var _a = __read(match, 2), behavior = _a[1]; | ||
if (behavior != null && behavior !== "") { | ||
value = behavior; | ||
} | ||
} | ||
} | ||
} | ||
if (value == null) { | ||
// Take the computed style for the element and see if it contains a specific 'scroll-behavior' value | ||
var computedStyle = getComputedStyle(target); | ||
var computedStyleValue = computedStyle.getPropertyValue("scrollBehavior"); | ||
if (computedStyleValue != null && computedStyleValue !== "") { | ||
value = computedStyleValue; | ||
} | ||
} | ||
// In all other cases, use the value from the CSSOM | ||
return value; | ||
} | ||
var supports = { | ||
// true with either native support or a polyfil, we don't care which | ||
matchMedia: window.matchMedia && window.matchMedia( "only all" ).matches, | ||
// true only if the browser supports window.matchMeida natively | ||
nativeMatchMedia: isNative(window.matchMedia) | ||
}; | ||
var HALF = 0.5; | ||
/** | ||
* The easing function to use when applying the smooth scrolling | ||
* @param {number} k | ||
* @returns {number} | ||
*/ | ||
function ease(k) { | ||
return HALF * (1 - Math.cos(Math.PI * k)); | ||
} | ||
var DownloadManager = (function() { | ||
/** | ||
* The duration of a smooth scroll | ||
* @type {number} | ||
*/ | ||
var SCROLL_TIME = 15000; | ||
/** | ||
* Performs a smooth repositioning of the scroll | ||
* @param {ISmoothScrollOptions} options | ||
*/ | ||
function smoothScroll(options) { | ||
var startTime = options.startTime, startX = options.startX, startY = options.startY, endX = options.endX, endY = options.endY, method = options.method; | ||
var timeLapsed = 0; | ||
var distanceX = endX - startX; | ||
var distanceY = endY - startY; | ||
var speed = Math.max(Math.abs(distanceX / 1000 * SCROLL_TIME), Math.abs(distanceY / 1000 * SCROLL_TIME)); | ||
requestAnimationFrame(function animate(timestamp) { | ||
timeLapsed += timestamp - startTime; | ||
var percentage = Math.max(0, Math.min(1, speed === 0 ? 0 : (timeLapsed / speed))); | ||
var positionX = Math.floor(startX + (distanceX * ease(percentage))); | ||
var positionY = Math.floor(startY + (distanceY * ease(percentage))); | ||
method(positionX, positionY); | ||
if (positionX !== endX || positionY !== endY) { | ||
requestAnimationFrame(animate); | ||
} | ||
}); | ||
} | ||
var cache = {} | ||
, queue = [] | ||
, callbacks = [] | ||
, requestCount = 0 | ||
, xhr = (function() { | ||
var method; | ||
try { method = new window.XMLHttpRequest(); } | ||
catch (e) { method = new window.ActiveXObject( "Microsoft.XMLHTTP" ); } | ||
return method | ||
}()); | ||
/** | ||
* Returns a High Resolution timestamp if possible, otherwise fallbacks to Date.now() | ||
* @returns {number} | ||
*/ | ||
function now() { | ||
if ("performance" in window) | ||
return performance.now(); | ||
return Date.now(); | ||
} | ||
// return function(urls, callback) { | ||
var WINDOW_ORIGINAL_SCROLL_TO = window.scrollTo; | ||
function addURLsToQueue(urls) { | ||
var url | ||
, i = 0; | ||
while (url = urls[i++]) { | ||
if (!cache[url] && !inArray(url, queue)) { | ||
queue.push(url); | ||
} | ||
} | ||
} | ||
/** | ||
* Gets the Smooth Scroll Options to use for the step function | ||
* @param {Element|Window} element | ||
* @param {number} x | ||
* @param {number} y | ||
* @param {ScrollMethodName} kind | ||
* @returns {ISmoothScrollOptions} | ||
*/ | ||
function getSmoothScrollOptions(element, x, y, kind) { | ||
var startTime = now(); | ||
if (!(element instanceof Element)) { | ||
// Use window as the scroll container | ||
var scrollX_1 = window.scrollX, pageXOffset_1 = window.pageXOffset, scrollY_1 = window.scrollY, pageYOffset_1 = window.pageYOffset; | ||
var startX = scrollX_1 == null || scrollX_1 === 0 ? pageXOffset_1 : scrollX_1; | ||
var startY = scrollY_1 == null || scrollY_1 === 0 ? pageYOffset_1 : scrollY_1; | ||
return { | ||
startTime: startTime, | ||
startX: startX, | ||
startY: startY, | ||
endX: Math.floor(kind === "scrollBy" | ||
? startX + x | ||
: x), | ||
endY: Math.floor(kind === "scrollBy" | ||
? startY + y | ||
: y), | ||
method: WINDOW_ORIGINAL_SCROLL_TO.bind(window) | ||
}; | ||
} | ||
else { | ||
var scrollLeft = element.scrollLeft, scrollTop = element.scrollTop; | ||
var startX = scrollLeft; | ||
var startY = scrollTop; | ||
return { | ||
startTime: startTime, | ||
startX: startX, | ||
startY: startY, | ||
endX: kind === "scrollBy" | ||
? startX + x | ||
: x, | ||
endY: kind === "scrollBy" | ||
? startY + y | ||
: y, | ||
method: ELEMENT_ORIGINAL_SCROLL_TO.bind(element) | ||
}; | ||
} | ||
} | ||
function processQueue() { | ||
// don't process the next one if we're in the middle of a download | ||
if (!(xhr.readyState === 0 || xhr.readyState === 4)) return | ||
/** | ||
* Gets the scrollLeft version of an element. If a window is provided, the 'pageXOffset' is used. | ||
* @param {Element | Window} element | ||
* @returns {number} | ||
*/ | ||
function getScrollLeft(element) { | ||
if (element instanceof Element) | ||
return element.scrollLeft; | ||
return element.pageXOffset; | ||
} | ||
var url; | ||
if (url = queue[0]) { | ||
downloadStylesheet(url); | ||
} | ||
if (!url) { | ||
invokeCallbacks(); | ||
} | ||
} | ||
/** | ||
* Ensures that the given value is numeric | ||
* @param {number} value | ||
* @return {number} | ||
*/ | ||
function ensureNumeric(value) { | ||
if (value == null) | ||
return 0; | ||
else if (typeof value === "number") { | ||
return value; | ||
} | ||
else if (typeof value === "string") { | ||
return parseFloat(value); | ||
} | ||
else { | ||
return 0; | ||
} | ||
} | ||
/** | ||
* Make the requests | ||
* | ||
* TODO: Get simultaneous downloads working, it can't be that hard | ||
*/ | ||
function downloadStylesheet(url) { | ||
requestCount++; | ||
xhr.open("GET", url, true); | ||
xhr.onreadystatechange = function () { | ||
if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) { | ||
cache[url] = xhr.responseText; | ||
queue.shift(); | ||
processQueue(); | ||
} | ||
}; | ||
xhr.send(null); | ||
} | ||
/** | ||
* Gets the scrollTop version of an element. If a window is provided, the 'pageYOffset' is used. | ||
* @param {Element | Window} element | ||
* @returns {number} | ||
*/ | ||
function getScrollTop(element) { | ||
if (element instanceof Element) | ||
return element.scrollTop; | ||
return element.pageYOffset; | ||
} | ||
/** | ||
* Check the cache to make sure all requests are complete | ||
*/ | ||
function downloadsFinished(urls) { | ||
var url | ||
, i = 0 | ||
, len = 0; | ||
while (url = urls[i++]) { | ||
if (cache[url]) len++; | ||
} | ||
return (len === urls.length) | ||
} | ||
/** | ||
* Returns true if the given value is some ScrollToOptions | ||
* @param {number | ScrollToOptions} value | ||
* @return {value is ScrollToOptions} | ||
*/ | ||
function isScrollToOptions(value) { | ||
return value != null && typeof value === "object"; | ||
} | ||
/** | ||
* Invoke each callback and remove it from the list | ||
*/ | ||
function invokeCallbacks() { | ||
var callback; | ||
while (callback = callbacks.shift()) { | ||
invokeCallback(callback.urls, callback.fn); | ||
} | ||
} | ||
var WINDOW_ORIGINAL_SCROLL = window.scroll; | ||
/** | ||
* Put the stylesheets in the proper order and invoke the callback | ||
*/ | ||
function invokeCallback(urls, callback) { | ||
var stylesheets = [] | ||
, url | ||
, i = 0; | ||
while (url = urls[i++]) { | ||
stylesheets.push(cache[url]); | ||
} | ||
callback.call(null, stylesheets); | ||
} | ||
var WINDOW_ORIGINAL_SCROLL_BY = window.scrollBy; | ||
return { | ||
request: function(urls, callback) { | ||
// Add the callback to the list | ||
callbacks.push({urls: urls, fn: callback}); | ||
/** | ||
* Handles a scroll method | ||
* @param {Element|Window} element | ||
* @param {ScrollMethodName} kind | ||
* @param {number | ScrollToOptions} optionsOrX | ||
* @param {number} y | ||
*/ | ||
function handleScrollMethod(element, kind, optionsOrX, y) { | ||
// If only one argument is given, and it isn't an options object, throw a TypeError | ||
if (y === undefined && !isScrollToOptions(optionsOrX)) { | ||
throw new TypeError("Failed to execute 'scroll' on 'Element': parameter 1 ('options') is not an object."); | ||
} | ||
// Scroll based on the primitive values given as arguments | ||
if (!isScrollToOptions(optionsOrX)) { | ||
var _a = normalizeScrollCoordinates(optionsOrX, y, element, kind), left = _a.left, top_1 = _a.top; | ||
onScrollPrimitive(left, top_1, element, kind); | ||
} | ||
// Scroll based on the received options object | ||
else { | ||
onScrollWithOptions(__assign({}, normalizeScrollCoordinates(optionsOrX.left, optionsOrX.top, element, kind), { behavior: optionsOrX.behavior == null ? "auto" : optionsOrX.behavior }), element, kind); | ||
} | ||
} | ||
/** | ||
* Gets the original non-patched prototype method for the given kind | ||
* @param {ScrollMethodName} kind | ||
* @param {Element|Window} element | ||
* @return {Function} | ||
*/ | ||
function getOriginalPrototypeMethodForKind(kind, element) { | ||
switch (kind) { | ||
case "scroll": | ||
return element instanceof Element ? ELEMENT_ORIGINAL_SCROLL : WINDOW_ORIGINAL_SCROLL; | ||
case "scrollBy": | ||
return element instanceof Element ? ELEMENT_ORIGINAL_SCROLL_BY : WINDOW_ORIGINAL_SCROLL_BY; | ||
case "scrollTo": | ||
return element instanceof Element ? ELEMENT_ORIGINAL_SCROLL_TO : WINDOW_ORIGINAL_SCROLL_TO; | ||
} | ||
} | ||
/** | ||
* Invoked when a 'ScrollToOptions' dict is provided to 'scroll()' as the first argument | ||
* @param {ScrollToOptions} options | ||
* @param {Element|Window} element | ||
* @param {ScrollMethodName} kind | ||
*/ | ||
function onScrollWithOptions(options, element, kind) { | ||
var behavior = getScrollBehavior(element, options); | ||
// If the behavior is 'auto' apply instantaneous scrolling | ||
if (behavior == null || behavior === "auto") { | ||
getOriginalPrototypeMethodForKind(kind, element).call(element, options.left, options.top); | ||
} | ||
else { | ||
smoothScroll(getSmoothScrollOptions(element, options.left, options.top, kind)); | ||
} | ||
} | ||
/** | ||
* Invoked when 'scroll()' is invoked with primitive x or y values | ||
* @param {number} x | ||
* @param {number} y | ||
* @param {ScrollMethodName} kind | ||
* @param {Element|Window} element | ||
*/ | ||
function onScrollPrimitive(x, y, element, kind) { | ||
// noinspection SuspiciousTypeOfGuard | ||
return onScrollWithOptions({ | ||
left: x, | ||
top: y, | ||
behavior: "auto" | ||
}, element, kind); | ||
} | ||
/** | ||
* Normalizes the given scroll coordinates | ||
* @param {number?} x | ||
* @param {number?} y | ||
* @param {Element|Window} element | ||
* @param {ScrollMethodName} kind | ||
* @return {Required<Pick<ScrollToOptions, "top" | "left">>} | ||
*/ | ||
function normalizeScrollCoordinates(x, y, element, kind) { | ||
switch (kind) { | ||
case "scrollBy": | ||
return { | ||
left: getScrollLeft(element) + ensureNumeric(x), | ||
top: getScrollTop(element) + ensureNumeric(y) | ||
}; | ||
default: | ||
return { | ||
left: ensureNumeric(x), | ||
top: ensureNumeric(y) | ||
}; | ||
} | ||
} | ||
if (downloadsFinished(urls)) { | ||
invokeCallbacks(); | ||
} else { | ||
addURLsToQueue(urls); | ||
processQueue(); | ||
} | ||
}, | ||
clearCache: function() { | ||
cache = {}; | ||
}, | ||
_getRequestCount: function() { | ||
return requestCount | ||
} | ||
} | ||
/** | ||
* Patches the 'scroll' method on the Element prototype | ||
*/ | ||
function patchElementScroll() { | ||
Element.prototype.scroll = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scroll", optionsOrX, y); | ||
}; | ||
} | ||
}()); | ||
/** | ||
* Patches the 'scrollBy' method on the Element prototype | ||
*/ | ||
function patchElementScrollBy() { | ||
Element.prototype.scrollBy = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scrollBy", optionsOrX, y); | ||
}; | ||
} | ||
var StyleManager = { | ||
/** | ||
* Patches the 'scrollTo' method on the Element prototype | ||
*/ | ||
function patchElementScrollTo() { | ||
Element.prototype.scrollTo = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scrollTo", optionsOrX, y); | ||
}; | ||
} | ||
_cache: {}, | ||
clearCache: function() { | ||
StyleManager._cache = {}; | ||
}, | ||
/** | ||
* Parse a string of CSS | ||
* optionaly pass an identifier for caching | ||
* | ||
* Adopted from TJ Holowaychuk's | ||
* https://github.com/visionmedia/css-parse | ||
* | ||
* Minor changes include removing the "stylesheet" root and | ||
* using String.charAt(i) instead of String[i] for IE7 compatibility | ||
*/ | ||
parse: function(css, identifier) { | ||
/** | ||
* Opening brace. | ||
*/ | ||
function open() { | ||
return match(/^\{\s*/) | ||
} | ||
/** | ||
* Closing brace. | ||
*/ | ||
function close() { | ||
return match(/^\}\s*/) | ||
} | ||
/** | ||
* Parse ruleset. | ||
*/ | ||
function rules() { | ||
var node; | ||
var rules = []; | ||
whitespace(); | ||
comments(rules); | ||
while (css.charAt(0) != '}' && (node = atrule() || rule())) { | ||
rules.push(node); | ||
comments(rules); | ||
} | ||
return rules | ||
} | ||
/** | ||
* Match `re` and return captures. | ||
*/ | ||
function match(re) { | ||
var m = re.exec(css); | ||
if (!m) return | ||
css = css.slice(m[0].length); | ||
return m | ||
} | ||
/** | ||
* Parse whitespace. | ||
*/ | ||
function whitespace() { | ||
match(/^\s*/); | ||
} | ||
/** | ||
* Parse comments | ||
*/ | ||
function comments(rules) { | ||
rules = rules || []; | ||
var c; | ||
while (c = comment()) rules.push(c); | ||
return rules | ||
} | ||
/** | ||
* Parse comment. | ||
*/ | ||
function comment() { | ||
if ('/' == css[0] && '*' == css[1]) { | ||
var i = 2; | ||
while ('*' != css[i] || '/' != css[i + 1]) ++i; | ||
i += 2; | ||
var comment = css.slice(2, i - 2); | ||
css = css.slice(i); | ||
whitespace(); | ||
return { comment: comment } | ||
} | ||
} | ||
/** | ||
* Parse selector. | ||
*/ | ||
function selector() { | ||
var m = match(/^([^{]+)/); | ||
if (!m) return | ||
return trim(m[0]).split(/\s*,\s*/) | ||
} | ||
/** | ||
* Parse declaration. | ||
*/ | ||
function declaration() { | ||
// prop | ||
var prop = match(/^(\*?[\-\w]+)\s*/); | ||
if (!prop) return | ||
prop = prop[0]; | ||
// : | ||
if (!match(/^:\s*/)) return | ||
// val | ||
var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)\s*/); | ||
if (!val) return | ||
val = trim(val[0]); | ||
// | ||
match(/^[;\s]*/); | ||
return { property: prop, value: val } | ||
} | ||
/** | ||
* Parse keyframe. | ||
*/ | ||
function keyframe() { | ||
var m; | ||
var vals = []; | ||
while (m = match(/^(from|to|\d+%|\.\d+%|\d+\.\d+%)\s*/)) { | ||
vals.push(m[1]); | ||
match(/^,\s*/); | ||
} | ||
if (!vals.length) return | ||
return { | ||
values: vals, | ||
declarations: declarations() | ||
} | ||
} | ||
/** | ||
* Parse keyframes. | ||
*/ | ||
function keyframes() { | ||
var m = match(/^@([\-\w]+)?keyframes */); | ||
if (!m) return | ||
var vendor = m[1]; | ||
// identifier | ||
var m = match(/^([\-\w]+)\s*/); | ||
if (!m) return | ||
var name = m[1]; | ||
if (!open()) return | ||
comments(); | ||
var frame; | ||
var frames = []; | ||
while (frame = keyframe()) { | ||
frames.push(frame); | ||
comments(); | ||
} | ||
if (!close()) return | ||
var obj = { | ||
name: name, | ||
keyframes: frames | ||
}; | ||
// don't include vendor unles there's a match | ||
if (vendor) obj.vendor = vendor; | ||
return obj | ||
} | ||
/** | ||
* Parse supports. | ||
*/ | ||
function supports() { | ||
var m = match(/^@supports *([^{]+)/); | ||
if (!m) return | ||
var supports = trim(m[1]); | ||
if (!open()) return | ||
comments(); | ||
var style = rules(); | ||
if (!close()) return | ||
return { supports: supports, rules: style } | ||
} | ||
/** | ||
* Parse media. | ||
*/ | ||
function media() { | ||
var m = match(/^@media *([^{]+)/); | ||
if (!m) return | ||
var media = trim(m[1]); | ||
if (!open()) return | ||
comments(); | ||
var style = rules(); | ||
if (!close()) return | ||
return { media: media, rules: style } | ||
} | ||
/** | ||
* Parse paged media. | ||
*/ | ||
function atpage() { | ||
var m = match(/^@page */); | ||
if (!m) return | ||
var sel = selector() || []; | ||
var decls = []; | ||
if (!open()) return | ||
comments(); | ||
// declarations | ||
var decl; | ||
while (decl = declaration() || atmargin()) { | ||
decls.push(decl); | ||
comments(); | ||
} | ||
if (!close()) return | ||
return { | ||
type: "page", | ||
selectors: sel, | ||
declarations: decls | ||
} | ||
} | ||
/** | ||
* Parse margin at-rules | ||
*/ | ||
function atmargin() { | ||
var m = match(/^@([a-z\-]+) */); | ||
if (!m) return | ||
var type = m[1]; | ||
return { | ||
type: type, | ||
declarations: declarations() | ||
} | ||
} | ||
/** | ||
* Parse import | ||
*/ | ||
function atimport() { | ||
return _atrule('import') | ||
} | ||
/** | ||
* Parse charset | ||
*/ | ||
function atcharset() { | ||
return _atrule('charset') | ||
} | ||
/** | ||
* Parse namespace | ||
*/ | ||
function atnamespace() { | ||
return _atrule('namespace') | ||
} | ||
/** | ||
* Parse non-block at-rules | ||
*/ | ||
function _atrule(name) { | ||
var m = match(new RegExp('^@' + name + ' *([^;\\n]+);\\s*')); | ||
if (!m) return | ||
var ret = {}; | ||
ret[name] = trim(m[1]); | ||
return ret | ||
} | ||
/** | ||
* Parse declarations. | ||
*/ | ||
function declarations() { | ||
var decls = []; | ||
if (!open()) return | ||
comments(); | ||
// declarations | ||
var decl; | ||
while (decl = declaration()) { | ||
decls.push(decl); | ||
comments(); | ||
} | ||
if (!close()) return | ||
return decls | ||
} | ||
/** | ||
* Parse at rule. | ||
*/ | ||
function atrule() { | ||
return keyframes() | ||
|| media() | ||
|| supports() | ||
|| atimport() | ||
|| atcharset() | ||
|| atnamespace() | ||
|| atpage() | ||
} | ||
/** | ||
* Parse rule. | ||
*/ | ||
function rule() { | ||
var sel = selector(); | ||
if (!sel) return | ||
comments(); | ||
return { selectors: sel, declarations: declarations() } | ||
} | ||
/** | ||
* Check the cache first, otherwise parse the CSS | ||
*/ | ||
if (identifier && StyleManager._cache[identifier]) { | ||
return StyleManager._cache[identifier] | ||
} else { | ||
// strip comments before parsing | ||
css = css.replace(/\/\*[\s\S]*?\*\//g, ""); | ||
return StyleManager._cache[identifier] = rules() | ||
} | ||
}, | ||
/** | ||
* Filter a ruleset by the passed keywords | ||
* Keywords may be either selector or property/value patterns | ||
*/ | ||
filter: function(rules, keywords) { | ||
var filteredRules = []; | ||
/** | ||
* Concat a2 onto a1 even if a1 is undefined | ||
*/ | ||
function safeConcat(a1, a2) { | ||
if (!a1 && !a2) return | ||
if (!a1) return [a2] | ||
return a1.concat(a2) | ||
} | ||
/** | ||
* Add a rule to the filtered ruleset, | ||
* but don't add empty media or supports values | ||
*/ | ||
function addRule(rule) { | ||
if (rule.media == null) delete rule.media; | ||
if (rule.supports == null) delete rule.supports; | ||
filteredRules.push(rule); | ||
} | ||
function containsKeyword(string, keywordList) { | ||
if (!keywordList) return | ||
var i = keywordList.length; | ||
while (i--) { | ||
if (string.indexOf(keywordList[i]) >= 0) return true | ||
} | ||
} | ||
function matchesKeywordPattern(declaration, patternList) { | ||
var wildcard = /\*/ | ||
, pattern | ||
, parts | ||
, reProp | ||
, reValue | ||
, i = 0; | ||
while (pattern = patternList[i++]) { | ||
parts = pattern.split(":"); | ||
reProp = new RegExp("^" + trim(parts[0]).replace(wildcard, ".*") + "$"); | ||
reValue = new RegExp("^" + trim(parts[1]).replace(wildcard, ".*") + "$"); | ||
if (reProp.test(declaration.property) && reValue.test(declaration.value)) { | ||
return true | ||
} | ||
} | ||
} | ||
function matchSelectors(rule, media, supports) { | ||
if (!keywords.selectors) return | ||
if (containsKeyword(rule.selectors.join(","), keywords.selectors)) { | ||
addRule({ | ||
media: media, | ||
supports: supports, | ||
selectors: rule.selectors, | ||
declarations: rule.declarations | ||
}); | ||
return true | ||
} | ||
} | ||
function matchesDeclaration(rule, media, supports) { | ||
if (!keywords.declarations) return | ||
var declaration | ||
, i = 0; | ||
while (declaration = rule.declarations[i++]) { | ||
if (matchesKeywordPattern(declaration, keywords.declarations)) { | ||
addRule({ | ||
media: media, | ||
supports: supports, | ||
selectors: rule.selectors, | ||
declarations: rule.declarations | ||
}); | ||
return true | ||
} | ||
} | ||
} | ||
function filterRules(rules, media, supports) { | ||
var rule | ||
, i = 0; | ||
while (rule = rules[i++]) { | ||
if (rule.declarations) { | ||
matchSelectors(rule, media, supports) || matchesDeclaration(rule, media, supports); | ||
} | ||
else if (rule.rules && rule.media) { | ||
filterRules(rule.rules, safeConcat(media, rule.media), supports); | ||
} | ||
else if (rule.rules && rule.supports) { | ||
filterRules(rule.rules, media, safeConcat(supports, rule.supports)); | ||
} | ||
} | ||
} | ||
// start the filtering | ||
filterRules(rules); | ||
// return the results | ||
return filteredRules | ||
} | ||
}; | ||
var MediaManager = (function() { | ||
var reMinWidth = /\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ | ||
, reMaxWidth = /\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ | ||
// a cache of the active media query info | ||
, mediaQueryMap = {} | ||
// the value of an `em` as used in a media query, | ||
// not necessarily the base font-size | ||
, emValueInPixels | ||
, currentWidth; | ||
/** | ||
* Get the pixel value of 1em for use in parsing media queries | ||
* ems in media queries are not affected by CSS, instead they | ||
* are the value of the browsers default font size, usually 16px | ||
*/ | ||
function getEmValueInPixels() { | ||
// cache this value because it probably won't change and | ||
// it's expensive to lookup | ||
if (emValueInPixels) return emValueInPixels | ||
var html = document.documentElement | ||
, body = document.body | ||
, originalHTMLFontSize = html.style.fontSize | ||
, originalBodyFontSize = body.style.fontSize | ||
, div = document.createElement("div"); | ||
// 1em is the value of the default font size of the browser | ||
// reset html and body to ensure the correct value is returned | ||
html.style.fontSize = "1em"; | ||
body.style.fontSize = "1em"; | ||
// add a test element and measure it | ||
body.appendChild(div); | ||
div.style.width = "1em"; | ||
div.style.position = "absolute"; | ||
emValueInPixels = div.offsetWidth; | ||
// remove the test element and restore the previous values | ||
body.removeChild(div); | ||
body.style.fontSize = originalBodyFontSize; | ||
html.style.fontSize = originalHTMLFontSize; | ||
return emValueInPixels | ||
} | ||
/** | ||
* Use the browsers matchMedia function or existing shim | ||
*/ | ||
function matchMediaNatively(query) { | ||
return window.matchMedia(query) | ||
} | ||
/** | ||
* Try to determine if a mediaQuery matches by | ||
* parsing the query and figuring it out manually | ||
* TODO: cache current width for repeated invocations | ||
*/ | ||
function matchMediaManually(query) { | ||
var minWidth | ||
, maxWidth | ||
, matches = false; | ||
// recalculate the width if it's not set | ||
// if (!currentWidth) currentWidth = document.documentElement.clientWidth | ||
currentWidth = document.documentElement.clientWidth; | ||
// parse min and max widths from query | ||
if (reMinWidth.test(query)) { | ||
minWidth = RegExp.$2 === "em" | ||
? parseFloat(RegExp.$1) * getEmValueInPixels() | ||
: parseFloat(RegExp.$1); | ||
} | ||
if (reMaxWidth.test(query)) { | ||
maxWidth = RegExp.$2 === "em" | ||
? parseFloat(RegExp.$1) * getEmValueInPixels() | ||
: parseFloat(RegExp.$1); | ||
} | ||
// if both minWith and maxWidth are set | ||
if (minWidth && maxWidth) { | ||
matches = (minWidth <= currentWidth && maxWidth >= currentWidth); | ||
} else { | ||
if (minWidth && minWidth <= currentWidth) matches = true; | ||
if (maxWidth && maxWidth >= currentWidth) matches = true; | ||
} | ||
// return fake MediaQueryList object | ||
return { | ||
matches: matches, | ||
media: query | ||
} | ||
} | ||
return { | ||
/** | ||
* Similar to the window.matchMedia method | ||
* results are cached to avoid expensive relookups | ||
* @returns MediaQueryList (or a faked one) | ||
*/ | ||
matchMedia: function(query) { | ||
return supports.matchMedia | ||
? matchMediaNatively(query) | ||
: matchMediaManually(query) | ||
// return mediaQueryMap[query] || ( | ||
// mediaQueryMap[query] = supports.matchMedia | ||
// ? matchMediaNatively(query) | ||
// : matchMediaManually(query) | ||
// ) | ||
}, | ||
clearCache: function() { | ||
// we don't use cache when the browser supports matchMedia listeners | ||
if (!supports.nativeMatchMedia) { | ||
currentWidth = null; | ||
} | ||
} | ||
} | ||
}()); | ||
var EventManager = (function() { | ||
var MediaListener = (function() { | ||
var listeners = []; | ||
return { | ||
add: function(polyfill, mql, fn) { | ||
var listener | ||
, i = 0; | ||
// if the listener is already in the array, return false | ||
while (listener = listeners[i++]) { | ||
if ( | ||
listener.polyfill == polyfill | ||
&& listener.mql === mql | ||
&& listener.fn === fn | ||
) { | ||
return false | ||
} | ||
} | ||
// otherwise add it | ||
mql.addListener(fn); | ||
listeners.push({ | ||
polyfill: polyfill, | ||
mql: mql, | ||
fn: fn | ||
}); | ||
}, | ||
remove: function(polyfill) { | ||
var listener | ||
, i = 0; | ||
while (listener = listeners[i++]) { | ||
if (listener.polyfill === polyfill) { | ||
listener.mql.removeListener(listener.fn); | ||
listeners.splice(--i, 1); | ||
} | ||
} | ||
} | ||
} | ||
}()); | ||
var ResizeListener = (function(listeners) { | ||
function onresize() { | ||
var listener | ||
, i = 0; | ||
while (listener = listeners[i++]) { | ||
listener.fn(); | ||
} | ||
} | ||
return { | ||
add: function(polyfill, fn) { | ||
if (!listeners.length) { | ||
if (window.addEventListener) { | ||
window.addEventListener("resize", onresize, false); | ||
} else { | ||
window.attachEvent("onresize", onresize); | ||
} | ||
} | ||
listeners.push({ | ||
polyfill: polyfill, | ||
fn: fn | ||
}); | ||
}, | ||
remove: function(polyfill) { | ||
var listener | ||
, i = 0; | ||
while (listener = listeners[i++]) { | ||
if (listener.polyfill === polyfill) { | ||
listeners.splice(--i, 1); | ||
} | ||
} | ||
if (!listeners.length) { | ||
if (window.removeEventListener) { | ||
window.removeEventListener("resize", onresize, false); | ||
} else if (window.detachEvent) { | ||
window.detachEvent("onresize", onresize); | ||
} | ||
} | ||
} | ||
} | ||
}([])); | ||
/** | ||
* Simple debounce function | ||
*/ | ||
function debounce(fn, wait) { | ||
var timeout; | ||
return function() { | ||
clearTimeout(timeout); | ||
timeout = setTimeout(fn, wait); | ||
} | ||
} | ||
return { | ||
removeListeners: function(polyfill) { | ||
supports.nativeMatchMedia | ||
? MediaListener.remove(polyfill) | ||
: ResizeListener.remove(polyfill); | ||
}, | ||
addListeners: function(polyfill, callback) { | ||
var queries = polyfill._mediaQueryMap | ||
, state = {};(function() { | ||
for (var query in queries) { | ||
if (!queries.hasOwnProperty(query)) continue | ||
state[query] = MediaManager.matchMedia(query).matches; | ||
} | ||
}()); | ||
/** | ||
* Register the listeners to detect media query changes | ||
* if the browser doesn't support this natively, use resize events instead | ||
*/ | ||
function addListeners() { | ||
if (supports.nativeMatchMedia) { | ||
for (var query in queries) { | ||
if (queries.hasOwnProperty(query)) { | ||
// a closure is needed here to keep the variable reference | ||
(function(mql, query) { | ||
MediaListener.add(polyfill, mql, function() { | ||
callback.call(polyfill, query, mql.matches); | ||
}); | ||
}(queries[query], query)); | ||
} | ||
} | ||
} else { | ||
var fn = debounce((function(polyfill, queries) { | ||
return function() { | ||
updateMatchedMedia(polyfill, queries); | ||
} | ||
}(polyfill, queries)), polyfill._options.debounceTimeout || 100); | ||
ResizeListener.add(polyfill, fn); | ||
} | ||
} | ||
/** | ||
* Check each media query to see if it still matches | ||
* Note: this is only invoked when the browser doesn't | ||
* natively support window.matchMedia addListeners | ||
*/ | ||
function updateMatchedMedia(polyfill, queries) { | ||
var query | ||
, current = {}; | ||
// clear the cache since a resize just happened | ||
MediaManager.clearCache(); | ||
// look for media matches that have changed since the last inspection | ||
for (query in queries) { | ||
if (!queries.hasOwnProperty(query)) continue | ||
current[query] = MediaManager.matchMedia(query).matches; | ||
if (current[query] != state[query]) { | ||
callback.call(polyfill, query, MediaManager.matchMedia(query).matches); | ||
} | ||
} | ||
state = current; | ||
} | ||
addListeners(); | ||
} | ||
} | ||
}()); | ||
function Ruleset(rules) { | ||
var i = 0 | ||
, rule; | ||
this._rules = []; | ||
while (rule = rules[i++]) { | ||
this._rules.push(new Rule(rule)); | ||
} | ||
} | ||
Ruleset.prototype.each = function(iterator, context) { | ||
var rule | ||
, i = 0; | ||
context || (context = this); | ||
while (rule = this._rules[i++]) { | ||
iterator.call(context, rule); | ||
} | ||
}; | ||
Ruleset.prototype.size = function() { | ||
return this._rules.length | ||
}; | ||
Ruleset.prototype.at = function(index) { | ||
return this._rules[index] | ||
}; | ||
function Rule(rule) { | ||
this._rule = rule; | ||
} | ||
Rule.prototype.getDeclaration = function() { | ||
var styles = {} | ||
, i = 0 | ||
, declaration | ||
, declarations = this._rule.declarations; | ||
while (declaration = declarations[i++]) { | ||
styles[declaration.property] = declaration.value; | ||
} | ||
return styles | ||
}; | ||
Rule.prototype.getSelectors = function() { | ||
return this._rule.selectors.join(", ") | ||
}; | ||
Rule.prototype.getMedia = function() { | ||
return this._rule.media.join(" and ") | ||
}; | ||
function Polyfill(options) { | ||
if (!(this instanceof Polyfill)) return new Polyfill(options) | ||
// set the options | ||
this._options = options; | ||
// allow the keywords option to be the only object passed | ||
if (!options.keywords) this._options = { keywords: options }; | ||
this._promise = []; | ||
// then do the stuff | ||
this._getStylesheets(); | ||
this._downloadStylesheets(); | ||
this._parseStylesheets(); | ||
this._filterCSSByKeywords(); | ||
this._buildMediaQueryMap(); | ||
this._reportInitialMatches(); | ||
this._addMediaListeners(); | ||
} | ||
/** | ||
* Fired when the media change and new rules match | ||
*/ | ||
Polyfill.prototype.doMatched = function(fn) { | ||
this._doMatched = fn; | ||
this._resolve(); | ||
return this | ||
}; | ||
/** | ||
* Fired when the media changes and previously matching rules no longer match | ||
*/ | ||
Polyfill.prototype.undoUnmatched = function(fn) { | ||
this._undoUnmatched = fn; | ||
this._resolve(); | ||
return this | ||
}; | ||
/** | ||
* Get all the rules the match the current media | ||
*/ | ||
Polyfill.prototype.getCurrentMatches = function() { | ||
var i = 0 | ||
, rule | ||
, media | ||
, matches = []; | ||
while (rule = this._filteredRules[i++]) { | ||
// rules are considered matches if they they have | ||
// no media query or the media query curently matches | ||
media = rule.media && rule.media.join(" and "); | ||
if (!media || MediaManager.matchMedia(media).matches) { | ||
matches.push(rule); | ||
} | ||
} | ||
return new Ruleset(matches) | ||
}; | ||
/** | ||
* Destroy the instance | ||
* Remove any bound events and send all current | ||
* matches to the callback as unmatches | ||
*/ | ||
Polyfill.prototype.destroy = function() { | ||
if (this._undoUnmatched) { | ||
this._undoUnmatched(this.getCurrentMatches()); | ||
EventManager.removeListeners(this); | ||
} | ||
return | ||
}; | ||
/** | ||
* Defer a task until after a condition is met | ||
*/ | ||
Polyfill.prototype._defer = function(condition, callback) { | ||
condition.call(this) | ||
? callback.call(this) | ||
: this._promise.push({condition: condition, callback: callback}); | ||
}; | ||
/** | ||
* Invoke any functions that have been deferred | ||
*/ | ||
Polyfill.prototype._resolve = function() { | ||
var promise | ||
, i = 0; | ||
while (promise = this._promise[i]) { | ||
if (promise.condition.call(this)) { | ||
this._promise.splice(i, 1); | ||
promise.callback.call(this); | ||
} else { | ||
i++; | ||
} | ||
} | ||
}; | ||
/** | ||
* Get a list of <link> tags in the head | ||
* optionally filter by the include/exclude options | ||
*/ | ||
Polyfill.prototype._getStylesheets = function() { | ||
var i = 0 | ||
, id | ||
, ids | ||
, link | ||
, links | ||
, inline | ||
, inlines | ||
, stylesheet | ||
, stylesheets = []; | ||
if (this._options.include) { | ||
// get only the included stylesheets link tags | ||
ids = this._options.include; | ||
while (id = ids[i++]) { | ||
if (link = document.getElementById(id)) { | ||
// if this tag is an inline style | ||
if (link.nodeName === "STYLE") { | ||
stylesheet = { text: link.textContent }; | ||
stylesheets.push(stylesheet); | ||
continue | ||
} | ||
// ignore print stylesheets | ||
if (link.media && link.media == "print") continue | ||
// ignore non-local stylesheets | ||
if (!isLocalURL(link.href)) continue | ||
stylesheet = { href: link.href }; | ||
link.media && (stylesheet.media = link.media); | ||
stylesheets.push(stylesheet); | ||
} | ||
} | ||
} | ||
else { | ||
// otherwise get all the stylesheets stylesheets tags | ||
// except the explicitely exluded ones | ||
ids = this._options.exclude; | ||
links = document.getElementsByTagName( "link" ); | ||
while (link = links[i++]) { | ||
if ( | ||
link.rel | ||
&& (link.rel == "stylesheet") | ||
&& (link.media != "print") // ignore print stylesheets | ||
&& (isLocalURL(link.href)) // only request local stylesheets | ||
&& (!inArray(link.id, ids)) | ||
) { | ||
stylesheet = { href: link.href }; | ||
link.media && (stylesheet.media = link.media); | ||
stylesheets.push(stylesheet); | ||
} | ||
} | ||
inlines = document.getElementsByTagName('style'); | ||
i = 0; | ||
while (inline = inlines[i++]){ | ||
stylesheet = { text: inline.textContent }; | ||
stylesheets.push(stylesheet); | ||
} | ||
} | ||
return this._stylesheets = stylesheets | ||
}; | ||
/** | ||
* Download each stylesheet in the _stylesheetURLs array | ||
*/ | ||
Polyfill.prototype._downloadStylesheets = function() { | ||
var self = this | ||
, stylesheet | ||
, urls = [] | ||
, i = 0; | ||
while (stylesheet = this._stylesheets[i++]) { | ||
urls.push(stylesheet.href); | ||
} | ||
DownloadManager.request(urls, function(stylesheets) { | ||
var stylesheet | ||
, i = 0; | ||
while (stylesheet = stylesheets[i]) { | ||
self._stylesheets[i++].text = stylesheet; | ||
} | ||
self._resolve(); | ||
}); | ||
}; | ||
Polyfill.prototype._parseStylesheets = function() { | ||
this._defer( | ||
function() { | ||
return this._stylesheets | ||
&& this._stylesheets.length | ||
&& this._stylesheets[0].text }, | ||
function() { | ||
var i = 0 | ||
, stylesheet; | ||
while (stylesheet = this._stylesheets[i++]) { | ||
stylesheet.rules = StyleManager.parse(stylesheet.text, stylesheet.url); | ||
} | ||
} | ||
); | ||
}; | ||
Polyfill.prototype._filterCSSByKeywords = function() { | ||
this._defer( | ||
function() { | ||
return this._stylesheets | ||
&& this._stylesheets.length | ||
&& this._stylesheets[0].rules | ||
}, | ||
function() { | ||
var stylesheet | ||
, media | ||
, rules = [] | ||
, i = 0; | ||
while (stylesheet = this._stylesheets[i++]) { | ||
media = stylesheet.media; | ||
// Treat stylesheets with a media attribute as being contained inside | ||
// a single @media block, but ignore `all` and `screen` media values | ||
// since they're basically meaningless in this context | ||
if (media && media != "all" && media != "screen") { | ||
rules.push({rules: stylesheet.rules, media: stylesheet.media}); | ||
} else { | ||
rules = rules.concat(stylesheet.rules); | ||
} | ||
} | ||
this._filteredRules = StyleManager.filter(rules, this._options.keywords); | ||
} | ||
); | ||
}; | ||
Polyfill.prototype._buildMediaQueryMap = function() { | ||
this._defer( | ||
function() { return this._filteredRules }, | ||
function() { | ||
var i = 0 | ||
, media | ||
, rule; | ||
this._mediaQueryMap = {}; | ||
while (rule = this._filteredRules[i++]) { | ||
if (rule.media) { | ||
media = rule.media.join(" and "); | ||
this._mediaQueryMap[media] = MediaManager.matchMedia(media); | ||
} | ||
} | ||
} | ||
); | ||
}; | ||
Polyfill.prototype._reportInitialMatches = function() { | ||
this._defer( | ||
function() { | ||
return this._filteredRules && this._doMatched | ||
}, | ||
function() { | ||
this._doMatched(this.getCurrentMatches()); | ||
} | ||
); | ||
}; | ||
Polyfill.prototype._addMediaListeners = function() { | ||
this._defer( | ||
function() { | ||
return this._filteredRules | ||
&& this._doMatched | ||
&& this._undoUnmatched | ||
}, | ||
function() { | ||
EventManager.addListeners( | ||
this, | ||
function(query, isMatch) { | ||
var i = 0 | ||
, rule | ||
, matches = [] | ||
, unmatches = []; | ||
while (rule = this._filteredRules[i++]) { | ||
if (rule.media && rule.media.join(" and ") == query) { | ||
(isMatch ? matches : unmatches).push(rule); | ||
} | ||
} | ||
matches.length && this._doMatched(new Ruleset(matches)); | ||
unmatches.length && this._undoUnmatched(new Ruleset(unmatches)); | ||
} | ||
); | ||
} | ||
); | ||
}; | ||
Polyfill.modules = { | ||
DownloadManager: DownloadManager, | ||
StyleManager: StyleManager, | ||
MediaManager: MediaManager, | ||
EventManager: EventManager | ||
}; | ||
Polyfill.constructors = { | ||
Ruleset: Ruleset, | ||
Rule: Rule | ||
}; | ||
window.Polyfill = Polyfill; | ||
}(window, document)); | ||
/** | ||
* Is true if the browser natively supports the 'scroll-behavior' CSS-property. | ||
* @type {boolean} | ||
*/ | ||
var SUPPORTS_SCROLL_BEHAVIOR = "scrollBehavior" in document.documentElement.style; | ||
/** | ||
* A Map between elements and their IElementWrapper | ||
* @type {Map<any, any>} | ||
*/ | ||
var ELEMENT_WRAPPER = new Map(); | ||
/** | ||
* Adds an IElementWrapper | ||
* @param {HTMLElement} element | ||
* @param {IElementWrapper} wrapper | ||
*/ | ||
function addWrapper(element, wrapper) { | ||
ELEMENT_WRAPPER.set(element, wrapper); | ||
} | ||
/** | ||
* Gets an IElementWrapper, if any exists for the provided element | ||
* @param {HTMLElement} element | ||
* @returns {IElementWrapper} | ||
*/ | ||
function getWrapper(element) { | ||
return ELEMENT_WRAPPER.get(element); | ||
} | ||
/** | ||
* Unwraps the scroll method | ||
* @param {HTMLElement} element | ||
*/ | ||
function unwrapScroll(element) { | ||
var wrapper = getWrapper(element); | ||
// If there is no wrapper, do nothing | ||
if (wrapper == null) | ||
return; | ||
// Otherwise, re-associate the original prototype methods with the element | ||
element.scroll = wrapper.scroll.original; | ||
element.scrollTo = wrapper.scrollTo.original; | ||
} | ||
/** | ||
* Gets all HTMLElements matched by the provided selectors | ||
* @param {string[]} selectors | ||
* @returns {HTMLElement[]} | ||
*/ | ||
function getElementsForSelectors(selectors) { | ||
// Match all of the selectors | ||
var elements = []; | ||
selectors.forEach(function (selector) { | ||
var element = getElementForSelector(selector); | ||
if (element != null) | ||
elements.push(element); | ||
}); | ||
return elements; | ||
} | ||
/** | ||
* Gets an element for the provided selector | ||
* @param {string} selector | ||
* @returns {HTMLElement} | ||
*/ | ||
function getElementForSelector(selector) { | ||
return document.querySelector(selector); | ||
} | ||
/** | ||
* Disposes all elements that has once had a 'scroll-behavior' CSS property value of 'smooth' but hasn't anymore | ||
* @param {string[]} selectors | ||
*/ | ||
function disposeElements(selectors) { | ||
getElementsForSelectors(selectors).forEach(function (element) { return disposeElement(element); }); | ||
} | ||
/** | ||
* Disposes an element that has once had a 'scroll-behavior' CSS property value of 'smooth' but hasn't anymore | ||
* @param {HTMLElement} element | ||
*/ | ||
function disposeElement(element) { | ||
unwrapScroll(element); | ||
} | ||
var ScrollBehaviorKind; | ||
(function (ScrollBehaviorKind) { | ||
ScrollBehaviorKind["AUTO"] = "auto"; | ||
ScrollBehaviorKind["SMOOTH"] = "smooth"; | ||
})(ScrollBehaviorKind || (ScrollBehaviorKind = {})); | ||
var HALF = 0.5; | ||
/** | ||
* The easing function to use when applying the smooth scrolling | ||
* @param {number} k | ||
* @returns {number} | ||
*/ | ||
function ease(k) { | ||
return HALF * (1 - Math.cos(Math.PI * k)); | ||
} | ||
/** | ||
* Returns a High Resolution timestamp if possible, otherwise fallbacks to Date.now() | ||
* @returns {number} | ||
*/ | ||
function now() { | ||
if ("performance" in window) | ||
return performance.now(); | ||
return Date.now(); | ||
} | ||
/** | ||
* The duration of a smooth scroll | ||
* @type {number} | ||
*/ | ||
var SCROLL_TIME = 200; | ||
/** | ||
* Performs a smooth repositioning of the scroll | ||
* @param {ISmoothScrollOptions} options | ||
*/ | ||
function smoothScroll(options) { | ||
var startTime = options.startTime, startX = options.startX, startY = options.startY, x = options.x, y = options.y, method = options.method, element = options.element; | ||
var currentTime = now(); | ||
var value; | ||
var currentX; | ||
var currentY; | ||
var elapsed = (currentTime - startTime) / SCROLL_TIME; | ||
// avoid elapsed times higher than one | ||
elapsed = elapsed > 1 ? 1 : elapsed; | ||
// apply easing to elapsed time | ||
value = ease(elapsed); | ||
currentX = startX + (x - startX) * value; | ||
currentY = startY + (y - startY) * value; | ||
method.call(element, currentX, currentY); | ||
// scroll more if we have not reached our destination | ||
if (currentX !== x || currentY !== y) { | ||
requestAnimationFrame(function () { return smoothScroll(options); }); | ||
/** | ||
* Patches the 'scroll' method on the Window prototype | ||
*/ | ||
function patchWindowScroll() { | ||
window.scroll = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scroll", optionsOrX, y); | ||
}; | ||
} | ||
} | ||
/** | ||
* Updates the scroll position of an element | ||
* @param {HTMLElement} element | ||
* @param {number} x | ||
* @param {number} y | ||
*/ | ||
function updateScrollPosition(element, x, y) { | ||
element.scrollLeft = x; | ||
element.scrollTop = y; | ||
} | ||
/** | ||
* Gets the Smooth Scroll Options to use for the step function | ||
* @param {HTMLElement|Window} element | ||
* @param {number} x | ||
* @param {number} y | ||
* @param {Function} originalFunction | ||
* @returns {ISmoothScrollOptions} | ||
*/ | ||
function getSmoothScrollOptions(element, x, y, originalFunction) { | ||
var startTime = now(); | ||
if (!(element instanceof Element)) { | ||
// Use window as the scroll container | ||
var scrollX_1 = window.scrollX, pageXOffset_1 = window.pageXOffset, scrollY_1 = window.scrollY, pageYOffset_1 = window.pageYOffset; | ||
return { | ||
element: window, | ||
startX: scrollX_1 == null || scrollX_1 === 0 ? pageXOffset_1 : scrollX_1, | ||
startY: scrollY_1 == null || scrollY_1 === 0 ? pageYOffset_1 : scrollY_1, | ||
method: originalFunction, | ||
startTime: startTime, | ||
x: x, | ||
y: y | ||
/** | ||
* Patches the 'scrollBy' method on the Window prototype | ||
*/ | ||
function patchWindowScrollBy() { | ||
window.scrollBy = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scrollBy", optionsOrX, y); | ||
}; | ||
} | ||
else { | ||
var scrollLeft = element.scrollLeft, scrollTop = element.scrollTop; | ||
return { | ||
element: element, | ||
startX: scrollLeft, | ||
startY: scrollTop, | ||
x: x, | ||
y: y, | ||
startTime: startTime, | ||
method: updateScrollPosition.bind(element, element) | ||
/** | ||
* Patches the 'scrollTo' method on the Window prototype | ||
*/ | ||
function patchWindowScrollTo() { | ||
window.scrollTo = function (optionsOrX, y) { | ||
handleScrollMethod(this, "scrollTo", optionsOrX, y); | ||
}; | ||
} | ||
} | ||
/** | ||
* Gets the scrollLeft version of an element. If a window is provided, the 'pageXOffset' is used. | ||
* @param {HTMLElement | Window} element | ||
* @returns {number} | ||
*/ | ||
function getScrollLeft(element) { | ||
if (element instanceof Element) | ||
return element.scrollLeft; | ||
return element.pageXOffset; | ||
} | ||
// tslint:disable:no-any | ||
/** | ||
* Gets the parent of an element, taking into account DocumentFragments, ShadowRoots, as well as the root context (window) | ||
* @param {EventTarget} currentElement | ||
* @returns {EventTarget | null} | ||
*/ | ||
function getParent(currentElement) { | ||
if ("nodeType" in currentElement && currentElement.nodeType === 1) { | ||
return currentElement.parentNode; | ||
} | ||
if ("ShadowRoot" in window && (currentElement instanceof window.ShadowRoot)) { | ||
return currentElement.host; | ||
} | ||
else if (currentElement === document) { | ||
return window; | ||
} | ||
else if (currentElement instanceof Node) | ||
return currentElement.parentNode; | ||
return null; | ||
} | ||
/** | ||
* Gets the scrollTop version of an element. If a window is provided, the 'pageYOffset' is used. | ||
* @param {HTMLElement | Window} element | ||
* @returns {number} | ||
*/ | ||
function getScrollTop(element) { | ||
if (element instanceof Element) | ||
return element.scrollTop; | ||
return element.pageYOffset; | ||
} | ||
/** | ||
* Wraps the scroll method | ||
* @param {HTMLElement} element | ||
*/ | ||
function wrapScroll(element) { | ||
var target = element instanceof HTMLHtmlElement || element instanceof HTMLBodyElement ? window : element; | ||
// Check if the target has already been wrapped, and if so, apply the original scroll function from it | ||
var wrapped = getWrapper(target); | ||
var originalScroll = wrapped == null ? target.scroll : wrapped.scroll.original; | ||
var originalScrollTo = wrapped == null ? target.scrollTo : wrapped.scrollTo.original; | ||
var originalScrollBy = wrapped == null ? target.scrollBy : wrapped.scrollBy.original; | ||
target.scroll = onScroll.bind(target, target, originalScroll, "scroll"); | ||
target.scrollTo = onScroll.bind(target, target, originalScrollTo, "scroll"); | ||
target.scrollBy = onScroll.bind(target, target, originalScrollBy, "scrollBy"); | ||
// Store it so we can retrieve the original handlers later on | ||
addWrapper(target, { | ||
scroll: { | ||
original: originalScroll, | ||
wrapped: target.scroll | ||
}, | ||
scrollTo: { | ||
original: originalScrollTo, | ||
wrapped: target.scrollTo | ||
}, | ||
scrollBy: { | ||
original: originalScrollBy, | ||
wrapped: target.scrollBy | ||
/** | ||
* Finds the nearest ancestor of an element that can scroll | ||
* @param {Element} target | ||
* @returns {Element|Window?} | ||
*/ | ||
function findNearestAncestorsWithScrollBehavior(target) { | ||
var currentElement = target; | ||
while (currentElement != null) { | ||
var behavior = getScrollBehavior(currentElement); | ||
if (behavior != null) | ||
return [currentElement, behavior]; | ||
var parent_1 = getParent(currentElement); | ||
// If the last Node is equal to the latest parentNode, break immediately | ||
if (parent_1 === currentElement) | ||
break; | ||
currentElement = parent_1; | ||
} | ||
}); | ||
} | ||
/** | ||
* Called when 'scroll()' is invoked on the element | ||
* @param {HTMLElement} element | ||
* @param {Function} original | ||
* @param {string} kind | ||
* @param {number | ScrollToOptions} x | ||
* @param {number} y | ||
*/ | ||
function onScroll(element, original, kind, x, y) { | ||
if (typeof x === "number") { | ||
onScrollPrimitive(x, y, element, original, kind); | ||
return undefined; | ||
} | ||
else { | ||
onScrollWithOptions(x, element, original, kind); | ||
} | ||
} | ||
/** | ||
* Invoked when a 'ScrollToOptions' dict is provided to 'scroll()' as the first argument | ||
* @param {ScrollToOptions} options | ||
* @param {HTMLElement} element | ||
* @param {Function} original | ||
* @param {string} kind | ||
*/ | ||
function onScrollWithOptions(options, element, original, kind) { | ||
// If scrolling is explicitly requested non-smooth, invoke the original scroll function | ||
if (options.behavior != null && options.behavior !== ScrollBehaviorKind.SMOOTH) { | ||
original(options); | ||
} | ||
else { | ||
// Otherwise, invoke the primitive scroll function | ||
var normalizedLeft = options.left == null ? 0 : options.left; | ||
var normalizedTop = options.top == null ? 0 : options.top; | ||
onScrollPrimitive(normalizedLeft, normalizedTop, element, original, kind); | ||
} | ||
} | ||
/** | ||
* Invoked when 'scroll()' is invoked with primitive x or y values | ||
* @param {number} x | ||
* @param {number} y | ||
* @param {HTMLElement} element | ||
* @param {Function} original | ||
* @param {string} kind | ||
*/ | ||
function onScrollPrimitive(x, y, element, original, kind) { | ||
var normalizedX = kind === "scroll" ? x : getScrollLeft(element) + x; | ||
var normalizedY = kind === "scroll" ? y : getScrollTop(element) + y; | ||
smoothScroll(getSmoothScrollOptions(element, normalizedX, normalizedY, original)); | ||
} | ||
/** | ||
* Registers all elements with a scroll-behavior CSS-property value of 'smooth' | ||
* @param {string[]} selectors | ||
*/ | ||
function registerElements(selectors) { | ||
getElementsForSelectors(selectors).forEach(function (element) { return registerElement(element); }); | ||
} | ||
/** | ||
* Registers an element with a scroll-behavior CSS-property value of 'smooth' | ||
* @param {HTMLElement} element | ||
*/ | ||
function registerElement(element) { | ||
wrapScroll(element); | ||
} | ||
/** | ||
* Get the current scroll behavior of an HTMLElement | ||
* @param {HTMLElement} element | ||
* @returns {ScrollBehaviorKind} | ||
*/ | ||
function getScrollBehavior(element) { | ||
var val = null; | ||
// First, check as an attribute | ||
var attribute = element.getAttribute("style"); | ||
if (attribute != null) { | ||
// Find the position within the string where 'scroll-behavior' is declare (if it is). | ||
var indexOfScrollBehavior = attribute.indexOf("scroll-behavior"); | ||
if (indexOfScrollBehavior >= 0) { | ||
// Check where it ends. If it never sees a ';', it is the last (or only) style property of the string | ||
var endIndexOfScrollBehavior = attribute.indexOf(";", indexOfScrollBehavior); | ||
// Slice the attribute value from after the ':' sign and up until the next ';' (or to the end if it is the last or only style property) | ||
val = attribute.slice(indexOfScrollBehavior + "scroll-behavior:".length, endIndexOfScrollBehavior < 0 ? undefined : endIndexOfScrollBehavior).trim(); | ||
// tslint:disable:no-any | ||
/** | ||
* Finds the nearest root from an element | ||
* @param {Element} target | ||
* @returns {Document|ShadowRoot} | ||
*/ | ||
function findNearestRoot(target) { | ||
var currentElement = target; | ||
while (currentElement != null) { | ||
if ("ShadowRoot" in window && (currentElement instanceof window.ShadowRoot)) { | ||
// Assume this is a ShadowRoot | ||
return currentElement; | ||
} | ||
var parent_1 = getParent(currentElement); | ||
if (parent_1 === currentElement) { | ||
return document; | ||
} | ||
currentElement = parent_1; | ||
} | ||
return document; | ||
} | ||
// If 'val' is still null, no match was found as an inline-style | ||
if (val == null) { | ||
/*tslint:disable*/ | ||
val = element.style.scrollBehavior; | ||
/*tslint:enable*/ | ||
/** | ||
* A Regular expression that matches id's of the form "#[digit]" | ||
* @type {RegExp} | ||
*/ | ||
var ID_WITH_LEADING_DIGIT_REGEXP = /^#\d/; | ||
/** | ||
* Catches anchor navigation to IDs within the same root and ensures that they can be smooth-scrolled | ||
* if the scroll behavior is smooth in the first rooter within that context | ||
*/ | ||
function catchNavigation() { | ||
// Listen for 'click' events globally | ||
window.addEventListener("click", function (e) { | ||
// Only work with trusted events on HTMLAnchorElements | ||
if (!e.isTrusted || !(e.target instanceof HTMLAnchorElement)) | ||
return; | ||
var hrefAttributeValue = e.target.getAttribute("href"); | ||
// Only work with HTMLAnchorElements that navigates to a specific ID | ||
if (hrefAttributeValue == null || !hrefAttributeValue.startsWith("#")) | ||
return; | ||
// Find the nearest ancestor that can be scrolled | ||
var ancestorWithScrollBehaviorResult = findNearestAncestorsWithScrollBehavior(e.target); | ||
// If there is none, don't proceed | ||
if (ancestorWithScrollBehaviorResult == null) | ||
return; | ||
// Take the scroll behavior for that ancestor | ||
var _a = __read(ancestorWithScrollBehaviorResult, 2), ancestorWithScrollBehavior = _a[0], behavior = _a[1]; | ||
// If the behavior isn't smooth, don't proceed | ||
if (behavior !== "smooth") | ||
return; | ||
// Find the nearest root, whether it be a ShadowRoot or the document itself | ||
var root = findNearestRoot(e.target); | ||
// Attempt to match the selector from that root. querySelector' doesn't support IDs that start with a digit, so work around that limitation | ||
var elementMatch = hrefAttributeValue.match(ID_WITH_LEADING_DIGIT_REGEXP) != null | ||
? root.getElementById(hrefAttributeValue.slice(1)) | ||
: root.querySelector(hrefAttributeValue); | ||
// If no selector could be found, don't proceed | ||
if (elementMatch == null) | ||
return; | ||
// Otherwise, first prevent the default action. | ||
e.preventDefault(); | ||
// Now, scroll to the element with that ID | ||
ancestorWithScrollBehavior.scrollTo({ | ||
behavior: behavior, | ||
top: elementMatch.offsetTop, | ||
left: elementMatch.offsetLeft | ||
}); | ||
}); | ||
} | ||
/*tslint:enable:no-any*/ | ||
return val === ScrollBehaviorKind.SMOOTH ? ScrollBehaviorKind.SMOOTH : ScrollBehaviorKind.AUTO; | ||
} | ||
/** | ||
* How often to check for elements with a 'scroll-behavior' CSS property | ||
* @type {number} | ||
*/ | ||
var PROPERTY_CHECK_INTERVAL = 3000; | ||
/** | ||
* How long to wait before tracking for the first time | ||
* @type {number} | ||
*/ | ||
var INIT_DELAY = 300; | ||
/** | ||
* The elements that are currently being tracked where 'scroll-behavior' is set as a style property | ||
* @type {Set<HTMLElement>} | ||
*/ | ||
var TRACKED_PROPERTY_ELEMENTS = new Set(); | ||
/** | ||
* Starts tracking elements with a 'scroll-behavior' CSS property | ||
*/ | ||
function startTracking() { | ||
track(); | ||
} | ||
/** | ||
* Tracks all elements with a 'scroll-behavior' CSS property. | ||
* Waits for the browser to become idle | ||
*/ | ||
function track() { | ||
setTimeout(trackElements, INIT_DELAY); | ||
} | ||
/** | ||
* Tracks all elements. Some of this is by selectors, some of this is by watching CSS property values set imperatively | ||
*/ | ||
function trackElements() { | ||
trackSelectors(); | ||
startTrackingInlineProperties(); | ||
trackInlineProperties(); | ||
} | ||
/** | ||
* Registers an interval to track inline properties | ||
*/ | ||
function startTrackingInlineProperties() { | ||
setInterval(trackInlineProperties, PROPERTY_CHECK_INTERVAL); | ||
} | ||
/** | ||
* Tracks inline properties | ||
*/ | ||
function trackInlineProperties() { | ||
var all = Array.from(document.querySelectorAll("*")); | ||
var filtered = new Set(all.filter(function (node) { return node instanceof HTMLElement && getScrollBehavior(node) === ScrollBehaviorKind.SMOOTH; })); | ||
// Check if a tracked element should be disposed (e.g. lost the property value in the meantime) | ||
TRACKED_PROPERTY_ELEMENTS.forEach(function (trackedElement) { | ||
if (!filtered.has(trackedElement)) { | ||
TRACKED_PROPERTY_ELEMENTS.delete(trackedElement); | ||
disposeElement(trackedElement); | ||
var ELEMENT_ORIGINAL_SCROLL_INTO_VIEW = Element.prototype.scrollIntoView; | ||
/** | ||
* The majority of this file is based on https://github.com/stipsan/compute-scroll-into-view (MIT license), | ||
* but has been rewritten to accept a scroller as an argument. | ||
*/ | ||
/** | ||
* Find out which edge to align against when logical scroll position is "nearest" | ||
* Interesting fact: "nearest" works similarly to "if-needed", if the element is fully visible it will not scroll it | ||
* | ||
* Legends: | ||
* ┌────────┐ ┏ ━ ━ ━ ┓ | ||
* │ target │ frame | ||
* └────────┘ ┗ ━ ━ ━ ┛ | ||
*/ | ||
function alignNearest(scrollingEdgeStart, scrollingEdgeEnd, scrollingSize, scrollingBorderStart, scrollingBorderEnd, elementEdgeStart, elementEdgeEnd, elementSize) { | ||
/** | ||
* If element edge A and element edge B are both outside scrolling box edge A and scrolling box edge B | ||
* | ||
* ┌──┐ | ||
* ┏━│━━│━┓ | ||
* │ │ | ||
* ┃ │ │ ┃ do nothing | ||
* │ │ | ||
* ┗━│━━│━┛ | ||
* └──┘ | ||
* | ||
* If element edge C and element edge D are both outside scrolling box edge C and scrolling box edge D | ||
* | ||
* ┏ ━ ━ ━ ━ ┓ | ||
* ┌───────────┐ | ||
* │┃ ┃│ do nothing | ||
* └───────────┘ | ||
* ┗ ━ ━ ━ ━ ┛ | ||
*/ | ||
if ((elementEdgeStart < scrollingEdgeStart && | ||
elementEdgeEnd > scrollingEdgeEnd) || | ||
(elementEdgeStart > scrollingEdgeStart && elementEdgeEnd < scrollingEdgeEnd)) { | ||
return 0; | ||
} | ||
}); | ||
// Add new tracked elements from the filtered ones | ||
filtered.forEach(function (filteredElement) { | ||
if (!TRACKED_PROPERTY_ELEMENTS.has(filteredElement)) { | ||
TRACKED_PROPERTY_ELEMENTS.add(filteredElement); | ||
registerElement(filteredElement); | ||
/** | ||
* If element edge A is outside scrolling box edge A and element height is less than scrolling box height | ||
* | ||
* ┌──┐ | ||
* ┏━│━━│━┓ ┏━┌━━┐━┓ | ||
* └──┘ │ │ | ||
* from ┃ ┃ to ┃ └──┘ ┃ | ||
* | ||
* ┗━ ━━ ━┛ ┗━ ━━ ━┛ | ||
* | ||
* If element edge B is outside scrolling box edge B and element height is greater than scrolling box height | ||
* | ||
* ┏━ ━━ ━┓ ┏━┌━━┐━┓ | ||
* │ │ | ||
* from ┃ ┌──┐ ┃ to ┃ │ │ ┃ | ||
* │ │ │ │ | ||
* ┗━│━━│━┛ ┗━│━━│━┛ | ||
* │ │ └──┘ | ||
* │ │ | ||
* └──┘ | ||
* | ||
* If element edge C is outside scrolling box edge C and element width is less than scrolling box width | ||
* | ||
* from to | ||
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓ | ||
* ┌───┐ ┌───┐ | ||
* │ ┃ │ ┃ ┃ │ ┃ | ||
* └───┘ └───┘ | ||
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛ | ||
* | ||
* If element edge D is outside scrolling box edge D and element width is greater than scrolling box width | ||
* | ||
* from to | ||
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓ | ||
* ┌───────────┐ ┌───────────┐ | ||
* ┃ │ ┃ │ ┃ ┃ │ | ||
* └───────────┘ └───────────┘ | ||
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛ | ||
*/ | ||
if ((elementEdgeStart <= scrollingEdgeStart && elementSize <= scrollingSize) || | ||
(elementEdgeEnd >= scrollingEdgeEnd && elementSize >= scrollingSize)) { | ||
return elementEdgeStart - scrollingEdgeStart - scrollingBorderStart; | ||
} | ||
}); | ||
} | ||
/** | ||
* Finds all elements with a 'scroll-behavior' CSS-property. Wraps all of them and unwraps all elements that | ||
* has been previously wrapped but since lost their 'scroll-behavior' CSS-property | ||
*/ | ||
function trackSelectors() { | ||
window.Polyfill({ | ||
keywords: { | ||
declarations: ["scroll-behavior: *"] | ||
/** | ||
* If element edge B is outside scrolling box edge B and element height is less than scrolling box height | ||
* | ||
* ┏━ ━━ ━┓ ┏━ ━━ ━┓ | ||
* | ||
* from ┃ ┃ to ┃ ┌──┐ ┃ | ||
* ┌──┐ │ │ | ||
* ┗━│━━│━┛ ┗━└━━┘━┛ | ||
* └──┘ | ||
* | ||
* If element edge A is outside scrolling box edge A and element height is greater than scrolling box height | ||
* | ||
* ┌──┐ | ||
* │ │ | ||
* │ │ ┌──┐ | ||
* ┏━│━━│━┓ ┏━│━━│━┓ | ||
* │ │ │ │ | ||
* from ┃ └──┘ ┃ to ┃ │ │ ┃ | ||
* │ │ | ||
* ┗━ ━━ ━┛ ┗━└━━┘━┛ | ||
* | ||
* If element edge C is outside scrolling box edge C and element width is greater than scrolling box width | ||
* | ||
* from to | ||
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓ | ||
* ┌───────────┐ ┌───────────┐ | ||
* │ ┃ │ ┃ │ ┃ ┃ | ||
* └───────────┘ └───────────┘ | ||
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛ | ||
* | ||
* If element edge D is outside scrolling box edge D and element width is less than scrolling box width | ||
* | ||
* from to | ||
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓ | ||
* ┌───┐ ┌───┐ | ||
* ┃ │ ┃ │ ┃ │ ┃ | ||
* └───┘ └───┘ | ||
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛ | ||
* | ||
*/ | ||
if ((elementEdgeEnd > scrollingEdgeEnd && elementSize < scrollingSize) || | ||
(elementEdgeStart < scrollingEdgeStart && elementSize > scrollingSize)) { | ||
return elementEdgeEnd - scrollingEdgeEnd + scrollingBorderEnd; | ||
} | ||
}) | ||
.doMatched(function (rules) { return registerElements(getSelectorsForCSSMatch(rules)); }) | ||
.undoUnmatched(function (rules) { return disposeElements(getSelectorsForCSSMatch(rules)); }); | ||
} | ||
/** | ||
* Gets all selectors for a CSS match | ||
* @param {PolyfillRuleSet} rules | ||
* @returns {string[]} | ||
*/ | ||
function getSelectorsForCSSMatch(rules) { | ||
var candidates = []; | ||
rules.each(function (rule) { | ||
var declaration = rule.getDeclaration(); | ||
if (declaration["scroll-behavior"] === ScrollBehaviorKind.SMOOTH) { | ||
candidates.push(rule.getSelectors()); | ||
return 0; | ||
} | ||
function computeScrollIntoView(target, scroller, options) { | ||
var block = options.block, inline = options.inline; | ||
// Used to handle the top most element that can be scrolled | ||
var scrollingElement = document.scrollingElement || document.documentElement; | ||
// Support pinch-zooming properly, making sure elements scroll into the visual viewport | ||
// Browsers that don't support visualViewport will report the layout viewport dimensions on document.documentElement.clientWidth/Height | ||
// and viewport dimensions on window.innerWidth/Height | ||
// https://www.quirksmode.org/mobile/viewports2.html | ||
// https://bokand.github.io/viewport/index.html | ||
var viewportWidth = window.visualViewport != null | ||
? visualViewport.width | ||
: innerWidth; | ||
var viewportHeight = window.visualViewport != null | ||
? visualViewport.height | ||
: innerHeight; | ||
var viewportX = window.scrollX != null ? window.scrollX : window.pageXOffset; | ||
var viewportY = window.scrollY != null ? window.scrollY : window.pageYOffset; | ||
var _a = target.getBoundingClientRect(), targetHeight = _a.height, targetWidth = _a.width, targetTop = _a.top, targetRight = _a.right, targetBottom = _a.bottom, targetLeft = _a.left; | ||
// These values mutate as we loop through and generate scroll coordinates | ||
var targetBlock = block === "start" || block === "nearest" | ||
? targetTop | ||
: block === "end" | ||
? targetBottom | ||
: targetTop + targetHeight / 2; // block === 'center | ||
var targetInline = inline === "center" | ||
? targetLeft + targetWidth / 2 | ||
: inline === "end" | ||
? targetRight | ||
: targetLeft; // inline === 'start || inline === 'nearest | ||
var _b = scroller.getBoundingClientRect(), height = _b.height, width = _b.width, top = _b.top, right = _b.right, bottom = _b.bottom, left = _b.left; | ||
var frameStyle = getComputedStyle(scroller); | ||
var borderLeft = parseInt(frameStyle.borderLeftWidth, 10); | ||
var borderTop = parseInt(frameStyle.borderTopWidth, 10); | ||
var borderRight = parseInt(frameStyle.borderRightWidth, 10); | ||
var borderBottom = parseInt(frameStyle.borderBottomWidth, 10); | ||
var blockScroll = 0; | ||
var inlineScroll = 0; | ||
// The property existance checks for offset[Width|Height] is because only HTMLElement objects have them, but any Element might pass by here | ||
// @TODO find out if the "as HTMLElement" overrides can be dropped | ||
var scrollbarWidth = "offsetWidth" in scroller | ||
? scroller.offsetWidth - | ||
scroller.clientWidth - | ||
borderLeft - | ||
borderRight | ||
: 0; | ||
var scrollbarHeight = "offsetHeight" in scroller | ||
? scroller.offsetHeight - | ||
scroller.clientHeight - | ||
borderTop - | ||
borderBottom | ||
: 0; | ||
if (scrollingElement === scroller) { | ||
// Handle viewport logic (document.documentElement or document.body) | ||
if (block === "start") { | ||
blockScroll = targetBlock; | ||
} | ||
else if (block === "end") { | ||
blockScroll = targetBlock - viewportHeight; | ||
} | ||
else if (block === "nearest") { | ||
blockScroll = alignNearest(viewportY, viewportY + viewportHeight, viewportHeight, borderTop, borderBottom, viewportY + targetBlock, viewportY + targetBlock + targetHeight, targetHeight); | ||
} | ||
else { | ||
// block === 'center' is the default | ||
blockScroll = targetBlock - viewportHeight / 2; | ||
} | ||
if (inline === "start") { | ||
inlineScroll = targetInline; | ||
} | ||
else if (inline === "center") { | ||
inlineScroll = targetInline - viewportWidth / 2; | ||
} | ||
else if (inline === "end") { | ||
inlineScroll = targetInline - viewportWidth; | ||
} | ||
else { | ||
// inline === 'nearest' is the default | ||
inlineScroll = alignNearest(viewportX, viewportX + viewportWidth, viewportWidth, borderLeft, borderRight, viewportX + targetInline, viewportX + targetInline + targetWidth, targetWidth); | ||
} | ||
// Apply scroll position offsets and ensure they are within bounds | ||
// @TODO add more test cases to cover this 100% | ||
blockScroll = Math.max(0, blockScroll + viewportY); | ||
inlineScroll = Math.max(0, inlineScroll + viewportX); | ||
} | ||
}); | ||
return candidates; | ||
} | ||
else { | ||
// Handle each scrolling frame that might exist between the target and the viewport | ||
if (block === "start") { | ||
blockScroll = targetBlock - top - borderTop; | ||
} | ||
else if (block === "end") { | ||
blockScroll = targetBlock - bottom + borderBottom + scrollbarHeight; | ||
} | ||
else if (block === "nearest") { | ||
blockScroll = alignNearest(top, bottom, height, borderTop, borderBottom + scrollbarHeight, targetBlock, targetBlock + targetHeight, targetHeight); | ||
} | ||
else { | ||
// block === 'center' is the default | ||
blockScroll = targetBlock - (top + height / 2) + scrollbarHeight / 2; | ||
} | ||
if (inline === "start") { | ||
inlineScroll = targetInline - left - borderLeft; | ||
} | ||
else if (inline === "center") { | ||
inlineScroll = targetInline - (left + width / 2) + scrollbarWidth / 2; | ||
} | ||
else if (inline === "end") { | ||
inlineScroll = targetInline - right + borderRight + scrollbarWidth; | ||
} | ||
else { | ||
// inline === 'nearest' is the default | ||
inlineScroll = alignNearest(left, right, width, borderLeft, borderRight + scrollbarWidth, targetInline, targetInline + targetWidth, targetWidth); | ||
} | ||
var scrollLeft = scroller.scrollLeft, scrollTop = scroller.scrollTop; | ||
// Ensure scroll coordinates are not out of bounds while applying scroll offsets | ||
blockScroll = Math.max(0, Math.min(scrollTop + blockScroll, scroller.scrollHeight - height + scrollbarHeight)); | ||
inlineScroll = Math.max(0, Math.min(scrollLeft + inlineScroll, scroller.scrollWidth - width + scrollbarWidth)); | ||
} | ||
return { | ||
top: blockScroll, | ||
left: inlineScroll | ||
}; | ||
} | ||
/** | ||
* Applies the polyfill | ||
*/ | ||
function apply() { | ||
startTracking(); | ||
} | ||
/** | ||
* Patches the 'scrollIntoView' method on the Element prototype | ||
*/ | ||
function patchElementScrollIntoView() { | ||
Element.prototype.scrollIntoView = function (arg) { | ||
var normalizedOptions = arg == null || arg === true | ||
? { | ||
block: "start", | ||
inline: "nearest" | ||
} | ||
: arg === false | ||
? { | ||
block: "end", | ||
inline: "nearest" | ||
} | ||
: arg; | ||
// Find the nearest ancestor that can be scrolled | ||
var ancestorWithScrollBehaviorResult = findNearestAncestorsWithScrollBehavior(this); | ||
// If there is none, opt-out by calling the original implementation | ||
if (ancestorWithScrollBehaviorResult == null) { | ||
ELEMENT_ORIGINAL_SCROLL_INTO_VIEW.call(this, normalizedOptions); | ||
return; | ||
} | ||
var _a = __read(ancestorWithScrollBehaviorResult, 2), ancestorWithScroll = _a[0], ancestorWithScrollBehavior = _a[1]; | ||
var behavior = normalizedOptions.behavior != null ? normalizedOptions.behavior : ancestorWithScrollBehavior; | ||
// If the behavior isn't smooth, simply invoke the original implementation and do no more | ||
if (behavior !== "smooth") { | ||
ELEMENT_ORIGINAL_SCROLL_INTO_VIEW.call(this, normalizedOptions); | ||
return; | ||
} | ||
ancestorWithScroll.scrollTo(__assign({ behavior: behavior }, computeScrollIntoView(this, ancestorWithScroll, normalizedOptions))); | ||
}; | ||
} | ||
/** | ||
* This polyfill makes any browser understand the CSS-property 'scroll-behavior'. | ||
* For any element with a 'scroll-behavior' CSS property value of 'smooth', any change | ||
* in its scroll position will render smoothly. | ||
* | ||
* DEPENDENCIES | ||
* - requestAnimationFrame | ||
* | ||
* CAVEATS | ||
* - You cannot set 'scrollLeft' or 'scrollTop'. There is no way to overwrite the property descriptors for those operations. Instead, use 'scroll()', 'scrollTo' or 'scrollBy' which does the exact same thing | ||
* - Element.scrollIntoView() is not polyfilled at the moment. | ||
* - Elements inside ShadowRoots won't be detected at the moment. | ||
*/ | ||
if (!SUPPORTS_SCROLL_BEHAVIOR) { | ||
apply(); | ||
} | ||
/** | ||
* Applies the polyfill | ||
*/ | ||
function patch() { | ||
patchElementScroll(); | ||
patchElementScrollBy(); | ||
patchElementScrollTo(); | ||
patchElementScrollIntoView(); | ||
patchWindowScroll(); | ||
patchWindowScrollBy(); | ||
patchWindowScrollTo(); | ||
catchNavigation(); | ||
} | ||
if (!SUPPORTS_SCROLL_BEHAVIOR) { | ||
patch(); | ||
} | ||
}()); | ||
//# sourceMappingURL=index.js.map |
140
package.json
{ | ||
"name": "scroll-behavior-polyfill", | ||
"version": "1.0.2", | ||
"description": "A polyfill for the 'scroll-behavior' CSS-property", | ||
"scripts": { | ||
"changelog:generate": "conventional-changelog --outfile CHANGELOG.md --release-count 0", | ||
"readme:badges": "node node_modules/@wessberg/ts-config/readme/badge/helper/add-badges.js", | ||
"readme:refresh": "npm run changelog:generate && npm run readme:badges", | ||
"commit:readme": "npm run readme:refresh && git commit -am \"Bumped version\" --no-verify || true", | ||
"clean:dist": "rm -r -f dist", | ||
"clean": "npm run clean:dist", | ||
"rollup": "rollup -c rollup.config.js", | ||
"rollup:watch": "rollup -c rollup.config.js --watch", | ||
"prebuild": "npm run clean", | ||
"build": "npm run rollup", | ||
"prewatch": "npm run clean", | ||
"watch": "npm run rollup:watch", | ||
"tslint": "tslint -c tslint.json -p tsconfig.json", | ||
"validate": "npm run tslint && npm run test", | ||
"test": "NODE_ENV=TEST echo \"skipping tests...\"", | ||
"prepublishOnly": "NODE_ENV=production npm run validate && npm run build", | ||
"precommit": "npm run tslint && exit 0", | ||
"prepush": "npm run validate && exit 0", | ||
"publish:major": "npm version major && npm run commit:readme && git push && npm publish", | ||
"publish:minor": "npm version minor && npm run commit:readme && git push && npm publish", | ||
"publish:patch": "npm version patch && npm run commit:readme && git push && npm publish" | ||
}, | ||
"keywords": [], | ||
"devDependencies": { | ||
"@wessberg/environment": "^1.0.1", | ||
"@wessberg/ts-config": "0.0.23", | ||
"conventional-changelog-cli": "^1.3.3", | ||
"husky": "latest", | ||
"rollup": "^0.49.2", | ||
"rollup-plugin-typescript2": "^0.5.2", | ||
"rollup-plugin-uglify": "^2.0.1", | ||
"rollup-watch": "^4.3.1", | ||
"tslint": "^5.7.0", | ||
"typescript": "2.5.2" | ||
}, | ||
"dependencies": {}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.js", | ||
"browser": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"typings": "./dist/index.d.ts", | ||
"es2015": "./dist/index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/wessberg/scroll-behavior-polyfill.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/wessberg/scroll-behavior-polyfill/issues" | ||
}, | ||
"author": { | ||
"name": "Frederik Wessberg", | ||
"email": "frederikwessberg@hotmail.com", | ||
"url": "https://github.com/wessberg" | ||
}, | ||
"engines": { | ||
"node": ">=7.4.0" | ||
}, | ||
"license": "MIT" | ||
"name": "scroll-behavior-polyfill", | ||
"version": "2.0.1", | ||
"description": "A polyfill for the 'scroll-behavior' CSS-property", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/wessberg/scroll-behavior-polyfill.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/wessberg/scroll-behavior-polyfill/issues" | ||
}, | ||
"scripts": { | ||
"generate:readme": "scaffold readme", | ||
"generate:license": "scaffold license", | ||
"generate:contributing": "scaffold contributing", | ||
"generate:coc": "scaffold coc", | ||
"generate:changelog": "standard-changelog --first-release", | ||
"generate:all": "npm run generate:license & npm run generate:contributing & npm run generate:coc & npm run generate:readme & npm run generate:changelog", | ||
"update": "ncu -ua && npm update && npm install", | ||
"lint": "tsc --noEmit && tslint -c tslint.json --project tsconfig.json", | ||
"prerollup": "rm -r -f dist", | ||
"rollup": "rollup -c rollup.config.js", | ||
"prepare": "npm run rollup", | ||
"publish:before": "NODE_ENV=production npm run lint && NODE_ENV=production npm run prepare && npm run generate:all && git add . && git commit -am \"Bumped version\" || true", | ||
"publish:after": "git push && npm publish", | ||
"publish:patch": "npm run publish:before && npm version patch && npm run publish:after", | ||
"publish:minor": "npm run publish:before && npm version minor && npm run publish:after", | ||
"publish:major": "npm run publish:before && npm version major && npm run publish:after" | ||
}, | ||
"files": [ | ||
"dist/**/*.*" | ||
], | ||
"keywords": [ | ||
"scroll-behavior", | ||
"polyfill", | ||
"css", | ||
"smooth", | ||
"scroll behavior" | ||
], | ||
"author": { | ||
"name": "Frederik Wessberg", | ||
"email": "frederikwessberg@hotmail.com", | ||
"url": "https://github.com/wessberg" | ||
}, | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@wessberg/rollup-plugin-ts": "1.1.17", | ||
"@wessberg/scaffold": "1.0.5", | ||
"@wessberg/ts-config": "^0.0.34", | ||
"npm-check-updates": "^2.15.0", | ||
"rollup": "^1.0.2", | ||
"rollup-plugin-node-resolve": "^4.0.0", | ||
"tslib": "^1.9.3", | ||
"tslint": "^5.12.0", | ||
"typescript": "^3.2.2", | ||
"standard-changelog": "^2.0.6" | ||
}, | ||
"dependencies": {}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.js", | ||
"browser": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"typings": "./dist/index.d.ts", | ||
"es2015": "./dist/index.js", | ||
"engines": { | ||
"node": ">=9.0.0" | ||
}, | ||
"scaffold": { | ||
"patreonUserId": "11315442", | ||
"contributorMeta": { | ||
"Frederik Wessberg": { | ||
"imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4", | ||
"role": "Maintainer", | ||
"twitterHandle": "FredWessberg", | ||
"isCocEnforcer": true | ||
} | ||
}, | ||
"backers": [] | ||
} | ||
} |
125
README.md
@@ -1,26 +0,32 @@ | ||
# `scroll-behavior` polyfill | ||
[![NPM version][npm-version-image]][npm-version-url] | ||
[![License-mit][license-mit-image]][license-mit-url] | ||
<a href="https://npmcharts.com/compare/scroll-behavior-polyfill?minimal=true"><img alt="Downloads per month" src="https://img.shields.io/npm/dm/scroll-behavior-polyfill.svg" height="20"></img></a> | ||
<a href="https://david-dm.org/scroll-behavior-polyfill"><img alt="Dependencies" src="https://img.shields.io/david/scroll-behavior-polyfill.svg" height="20"></img></a> | ||
<a href="https://www.npmjs.com/package/scroll-behavior-polyfill"><img alt="NPM Version" src="https://badge.fury.io/js/scroll-behavior-polyfill.svg" height="20"></img></a> | ||
<a href="https://github.com/wessberg/scroll-behavior-polyfill/graphs/contributors"><img alt="Contributors" src="https://img.shields.io/github/contributors/wessberg%2Fscroll-behavior-polyfill.svg" height="20"></img></a> | ||
<a href="https://opensource.org/licenses/MIT"><img alt="MIT License" src="https://img.shields.io/badge/License-MIT-yellow.svg" height="20"></img></a> | ||
<a href="https://www.patreon.com/bePatron?u=11315442"><img alt="Support on Patreon" src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" height="20"></img></a> | ||
[license-mit-url]: https://opensource.org/licenses/MIT | ||
# `scroll-behavior-polyfill` | ||
[license-mit-image]: https://img.shields.io/badge/License-MIT-yellow.svg | ||
> A polyfill for the [`scroll-behavior`](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior) CSS-property | ||
[npm-version-url]: https://www.npmjs.com/package/scroll-behavior-polyfill | ||
## Description | ||
[npm-version-image]: https://badge.fury.io/js/scroll-behavior-polyfill.svg | ||
The scroll-behavior CSS property sets the behavior for a scrolling box when scrolling is triggered by the navigation or CSSOM scrolling APIs. | ||
> A polyfill for the new CSS property: `scroll-behavior`. | ||
## Install | ||
## Installation | ||
### NPM | ||
You can `npm install` it like this: | ||
``` | ||
npm install scroll-behavior-polyfill | ||
$ npm install scroll-behavior-polyfill | ||
``` | ||
## Adding the polyfill | ||
### Yarn | ||
### Importing it | ||
``` | ||
$ yarn add scroll-behavior-polyfill | ||
``` | ||
## Applying the polyfill | ||
The polyfill will be feature detected and applied if and only if the browser doesn't support the property already. | ||
@@ -30,50 +36,97 @@ To include it, add this somewhere: | ||
```typescript | ||
import "scroll-behavior-polyfill" | ||
import "scroll-behavior-polyfill"; | ||
``` | ||
### Conditionally importing it | ||
However, it is strongly suggested that you only include the polyfill for browsers that doesn't already support `scroll-behavior`. | ||
One way to do so is with an async import: | ||
Preferably, you should feature detect before including the code since there is no need to include a polyfill that won't ever be applied. | ||
One way to do so is with async imports: | ||
```typescript | ||
if (!"scrollBehavior" in document.documentElement.style) { | ||
await import("scroll-behavior-polyfill"); | ||
await import("scroll-behavior-polyfill"); | ||
} | ||
``` | ||
Alternatively, you can use [Polyfill.app](https://github.com/wessberg/Polyfiller) which uses this polyfill and takes care of only loading the polyfill if needed as well as adding the language features that the polyfill depends on (See [dependencies](#dependencies--browser-support)). | ||
## Usage | ||
You can use scroll-behavior exactly how you expect to: | ||
### Declarative API | ||
### As a CSS-property | ||
You can define the `scroll-behavior` of Elements via one of the following approaches: | ||
```css | ||
#something { | ||
scroll-behavior: smooth | ||
} | ||
``` | ||
- A style attribute including a `scroll-behavior` property. | ||
- An element with a `scroll-behavior` attribute. | ||
- Or, an element with a `CSSStyleDeclaration` with a `scrollBehavior` property. | ||
### As an inline-style | ||
This means that either of the following approaches will work: | ||
```html | ||
<!-- Works just fine when given in the 'style' attribute --> | ||
<div style="scroll-behavior: smooth"></div> | ||
<!-- Works just fine when given as an attribute of the name 'scroll-behavior' --> | ||
<div scroll-behavior="smooth"></div> | ||
``` | ||
### As an imperative style property | ||
```typescript | ||
// Works jut fine when given as a style property | ||
element.style.scrollBehavior = "smooth"; | ||
``` | ||
## Dependencies | ||
See [this section](#are-there-any-known-quirks) for information about why `scroll-behavior` values provided in stylesheets won't be discovered by the polyfill. | ||
This polyfill expects `requestAnimationFrame` to be defined. | ||
Please polyfill it! | ||
### Imperative API | ||
## Caveats | ||
You can of course also use the imperative `scroll()`, `scrollTo`, `scrollBy`, and `scrollIntoView` APIs and provide `scroll-behavior` options. | ||
For example: | ||
```typescript | ||
// Works for the window object | ||
window.scroll({ | ||
behavior: "smooth", | ||
top: 100, | ||
left: 0 | ||
}); | ||
// Works for any element (and supports all options) | ||
myElement.scrollIntoView(); | ||
myElement.scrollBy({ | ||
behavior: "smooth", | ||
top: 50, | ||
left: 0 | ||
}); | ||
``` | ||
## Dependencies & Browser support | ||
This polyfill is distributed in ES3-compatible syntax, but is using some modern APIs and language features which must be available: | ||
- `requestAnimationFrame` | ||
- `Element.prototype.scrollIntoView` | ||
For by far the most browsers, these features will already be natively available. | ||
Generally, I would highly recommend using something like [Polyfill.app](https://github.com/wessberg/Polyfiller) which takes care of this stuff automatically. | ||
## Contributing | ||
Do you want to contribute? Awesome! Please follow [these recommendations](./CONTRIBUTING.md). | ||
## Maintainers | ||
- <a href="https://github.com/wessberg"><img alt="Frederik Wessberg" src="https://avatars2.githubusercontent.com/u/20454213?s=460&v=4" height="11"></img></a> [Frederik Wessberg](https://github.com/wessberg): _Maintainer_ | ||
## FAQ | ||
### Are there any known quirks? | ||
- You cannot set `scrollLeft` or `scrollTop`. There is no way to overwrite the property descriptors for those operations. Instead, use `scroll()`, `scrollTo` or `scrollBy` which does the exact same thing. | ||
- `Element.scrollIntoView()` is not polyfilled at the moment. | ||
- Elements inside ShadowRoots won't be detected at the moment. It probably will be soon. | ||
- `scroll-behavior` properties declared only in stylesheets won't be discovered. This is because [polyfilling CSS is hard and really bad for performance](https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/). | ||
## Backers 🏅 | ||
[Become a backer](https://www.patreon.com/bePatron?u=11315442) and get your name, logo, and link to your site listed here. | ||
## License 📄 | ||
MIT © [Frederik Wessberg](https://github.com/wessberg) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
132
107965
1
801
1