├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets ├── icon.svg ├── icon128.png ├── icon48.png ├── iconBasic.svg ├── iconContour128.png └── iconContour64.png ├── externalAssets └── tile.svg ├── lib ├── css-what.js └── underscore.js ├── package-lock.json ├── package.json ├── platform ├── chromium │ ├── js │ │ ├── vapiBackground.js │ │ └── vapiInjected.js │ └── manifest.json ├── firefox │ ├── js │ │ ├── vapiBackground.js │ │ └── vapiInjected.js │ └── manifest.json └── safari │ ├── Info.plist │ ├── Settings.plist │ ├── background.html │ └── js │ ├── vapiBackground.js │ └── vapiInjected.js ├── src ├── js │ ├── background.js │ ├── content.js │ ├── explorer.js │ ├── popup.js │ └── whitelist.js └── popup.html └── tests ├── animationOpacity.html ├── bigSticky.html ├── classification.html ├── delayedStyling.html ├── elementMoves.html ├── headerWrongSize.html ├── import.html ├── inlineLink.html ├── outsideBody.html ├── pseudoElement.html ├── stickyPosition.html ├── style.css ├── weirdScroll.html └── zeroHeight.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Packages 4 | *.egg 5 | *.eggs 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | 12 | # Installer logs 13 | pip-log.txt 14 | 15 | *.DS_store 16 | 17 | #Minified JS 18 | *-min.js 19 | 20 | # PyCharm 21 | .idea 22 | *.iml 23 | 24 | =* 25 | ._* 26 | 27 | .cache 28 | local*ini 29 | 30 | npm-debug.log 31 | node_modules/ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Boris Lykah 2 | Authors: Boris Lykah 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License version 3 6 | as published by the Free Software Foundation. 7 | 8 | This program is distributed in the hope that it will be useful, but 9 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 10 | or FITNESS FOR A PARTICULAR PURPOSE. 11 | See the GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program; if not, see http://www.gnu.org/licenses or write to 14 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 15 | Boston, MA, 02110-1301 USA. 16 | 17 | The interactive user interfaces in modified source and object code versions 18 | of this program must display Appropriate Legal Notices, as required under 19 | Section 5 of the GNU Affero General Public License. 20 | 21 | You can be released from the requirements of the license by purchasing 22 | a commercial license. Buying such a license is mandatory as soon as you 23 | develop commercial activities involving the StickyDucky software without 24 | disclosing the source code of your own applications. 25 | These activities include: shipping parts or whole of StickyDucky with a 26 | commercial product, such as browser extension, application, web proxy, 27 | and others; shipping StickyDucky with a closed source product. 28 | 29 | For more information, please contact Boris Lykah at this 30 | address: lykahb gmail.com -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIST := dist/ 2 | 3 | prepare: 4 | mkdir -p ${DIST} 5 | cp -R assets lib src/* LICENSE ${DIST} 6 | 7 | firefox: DIST := ${DIST}/firefox 8 | firefox: prepare 9 | cp -r platform/firefox/* ${DIST} 10 | cd ${DIST}; zip -r sticky-ducky@addons.mozilla.org.xpi * 11 | 12 | chromium: DIST := ${DIST}/chromium 13 | chromium: prepare 14 | cp -r platform/chromium/* ${DIST} 15 | cd ${DIST}; zip -r StickyDucky.zip * 16 | 17 | safari: DIST := ${DIST}/StickyDucky.safariextension 18 | safari: prepare 19 | cp -r platform/safari/* ${DIST} 20 | cp assets/icon128.png ${DIST}/Icon.png 21 | cp assets/icon48.png ${DIST}/Icon-48.png 22 | cp assets/icon128.png ${DIST}/Icon-128.png 23 | 24 | clean: 25 | rm -rf ${DIST} 26 | 27 | browserify: 28 | npm ci 29 | node_modules/browserify/bin/cmd.js --require css-what --standalone CSSWhat > lib/css-what.js 30 | node_modules/prettier/bin-prettier.js --write lib/css-what.js 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticky Ducky 2 | 3 | *A browser extension that cleans pages of the sticky elements and shows them when needed. Fast and simple.* 4 | 5 | Many websites have sticky headers, social buttons and other things which are always on the screen. Sometimes they are useful, 6 | but most of the time it's junk that simply occupies the screen space. Sticky Ducky automatically cleans the page of the sticky 7 | elements and shows them again when you need it. You can show them on hover or on scroll. 8 | 9 | Sticky Ducky examines the document and CSS to find the stickies. Then it injects a stylesheet with the rules that precisely target 10 | the stickies to clean them up. Keeping DOM unaltered keeps the extension well separated from the page DOM logic in Javascript. 11 | Even on the most dynamic websites using React or Vue this approach works reliably. 12 | 13 | There is experimental support for the mobile devices with Firefox. 14 | 15 | ## Inspiration 16 | As more websites I visit started putting sticky junk on the page, it got annoying. All solutions I found were doing similar things: 17 | removing fixed elements from DOM or setting display to none, searching them with the inline style, required user input to block an element. 18 | 19 | I wanted an extension that would never break website layout and do its job automatically. So I created a simple one that 20 | hid the headers on several news websites. 21 | Later that year after https://daringfireball.net/2017/06/medium_dickbars was published on Hacker News, I realized that a 22 | lot of people feel the same way and may want to use it too. So, after several rounds of rewrites and polishing it's ready for release. Enjoy! 23 | 24 | ## Settings 25 | 26 | ### Modes of hiding stickies 27 | Some sticky elements like navigation may be useful to display. Sticky Ducky lets you do it quickly. 28 | * When hovering over. This requires a pointer and is not recommended on the touch screen devices. 29 | * After scrolling up. This works on mobile devices too. 30 | * On top of the page. This settings is the least likely to show the stickies when not needed. 31 | * Always. This is similar to disabling extension. 32 | 33 | ### Whitelist 34 | You can whitelist the sticky elements on the websites or individual pages. 35 | The whitelist rules support a limited subset of [Adblock Plus filters format](https://adblockplus.org/filter-cheatsheet). 36 | Notably, the wildcards are currently not supported. 37 | 38 | * You can whitelist by an address part: `/checkout` or `.jira.` will whitelist the stickies on any domain if the URL has that pattern. 39 | * By domain name: `||corp.com` whitelists everything on `https://corp.com/index.html` and `https://mail.corp.com/index.html` 40 | * By the exact address `|https://bugtracker.corp.com/secure/|` 41 | 42 | All URL patterns can be combined with the individual selectors: 43 | For example, with the entry `||bugtracker.corp.com###header` Sticky Ducky would ignore the header on every page of the bugtracker.corp.com but treat the other stickies as usual. 44 | Selectors must be simple: `.class` or `#id` is okay but `div.class` is not. The selectors are passed inside of `:not` that has tight constraints. 45 | 46 | ## Installation 47 | 48 | [Chrome Store](https://chrome.google.com/webstore/detail/sticky-ducky/gklhneccajklhledmencldobkjjpnhkd) 49 | 50 | [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/sticky-ducky/) 51 | 52 | ## FAQ 53 | ### How does Sticky Ducky work? 54 | 1. It analyzes style sheets to discover rules that make elements fixed or sticky. 55 | 2. Then it uses selectors from them to find the sticky elements in the page DOM. 56 | 3. For each element that may be sticky, apply heuristics to classify its type (header, footer, etc.) and decide if it should be hidden and under what conditions. 57 | 58 | ### There are still some sticky elements on a page 59 | Likely, Sticky Ducky has detected that element but its heuristics decided not to hide it. You can use developer tools to see if it found it - the detected elements would have an extra attribute `sticky-ducky-type`. 60 | 61 | ### Why not hide all sticky elements? 62 | Cleaning up sticky elements too eagerly can break websites. Even worse, it would not be obvious that the extension has caused it and the site needs to be whitelisted. So the heuristics make cautious choices. 63 | Here are a few examples: 64 | - On Twitter (and many other sites) opening a picture full-screen brings up a gallery view and disables scroll on the page. The gallery itself is sticky. So hiding it would create an impression that the page froze and does not respond. 65 | - On Github on PR files view the bars with file names are sticky. Ideally, we want to show them when they are on the screen and hide when they are scrolled away. However, the browser API makes it difficult to distinguish those cases. So there is only a choice between hiding them in any position or doing nothing. 66 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lykahb/sticky-ducky/9dbce928738b219209907577fdd738c7dd98cb5a/assets/icon128.png -------------------------------------------------------------------------------- /assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lykahb/sticky-ducky/9dbce928738b219209907577fdd738c7dd98cb5a/assets/icon48.png -------------------------------------------------------------------------------- /assets/iconBasic.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /assets/iconContour128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lykahb/sticky-ducky/9dbce928738b219209907577fdd738c7dd98cb5a/assets/iconContour128.png -------------------------------------------------------------------------------- /assets/iconContour64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lykahb/sticky-ducky/9dbce928738b219209907577fdd738c7dd98cb5a/assets/iconContour64.png -------------------------------------------------------------------------------- /externalAssets/tile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Sticky 51 | Ducky 52 | 53 | 54 | 58 | 59 | Cleans page of sticky headers 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/css-what.js: -------------------------------------------------------------------------------- 1 | (function (f) { 2 | if (typeof exports === "object" && typeof module !== "undefined") { 3 | module.exports = f(); 4 | } else if (typeof define === "function" && define.amd) { 5 | define([], f); 6 | } else { 7 | var g; 8 | if (typeof window !== "undefined") { 9 | g = window; 10 | } else if (typeof global !== "undefined") { 11 | g = global; 12 | } else if (typeof self !== "undefined") { 13 | g = self; 14 | } else { 15 | g = this; 16 | } 17 | g.CSSWhat = f(); 18 | } 19 | })(function () { 20 | var define, module, exports; 21 | return (function () { 22 | function r(e, n, t) { 23 | function o(i, f) { 24 | if (!n[i]) { 25 | if (!e[i]) { 26 | var c = "function" == typeof require && require; 27 | if (!f && c) return c(i, !0); 28 | if (u) return u(i, !0); 29 | var a = new Error("Cannot find module '" + i + "'"); 30 | throw ((a.code = "MODULE_NOT_FOUND"), a); 31 | } 32 | var p = (n[i] = { exports: {} }); 33 | e[i][0].call( 34 | p.exports, 35 | function (r) { 36 | var n = e[i][1][r]; 37 | return o(n || r); 38 | }, 39 | p, 40 | p.exports, 41 | r, 42 | e, 43 | n, 44 | t 45 | ); 46 | } 47 | return n[i].exports; 48 | } 49 | for ( 50 | var u = "function" == typeof require && require, i = 0; 51 | i < t.length; 52 | i++ 53 | ) 54 | o(t[i]); 55 | return o; 56 | } 57 | return r; 58 | })()( 59 | { 60 | 1: [ 61 | function (require, module, exports) { 62 | "use strict"; 63 | var __spreadArray = 64 | (this && this.__spreadArray) || 65 | function (to, from, pack) { 66 | if (pack || arguments.length === 2) 67 | for (var i = 0, l = from.length, ar; i < l; i++) { 68 | if (ar || !(i in from)) { 69 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 70 | ar[i] = from[i]; 71 | } 72 | } 73 | return to.concat(ar || Array.prototype.slice.call(from)); 74 | }; 75 | Object.defineProperty(exports, "__esModule", { value: true }); 76 | exports.isTraversal = void 0; 77 | var reName = 78 | /^[^\\#]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\-\u00b0-\uFFFF])+/; 79 | var reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi; 80 | var actionTypes = new Map([ 81 | ["~", "element"], 82 | ["^", "start"], 83 | ["$", "end"], 84 | ["*", "any"], 85 | ["!", "not"], 86 | ["|", "hyphen"], 87 | ]); 88 | var Traversals = { 89 | ">": "child", 90 | "<": "parent", 91 | "~": "sibling", 92 | "+": "adjacent", 93 | }; 94 | var attribSelectors = { 95 | "#": ["id", "equals"], 96 | ".": ["class", "element"], 97 | }; 98 | // Pseudos, whose data property is parsed as well. 99 | var unpackPseudos = new Set([ 100 | "has", 101 | "not", 102 | "matches", 103 | "is", 104 | "where", 105 | "host", 106 | "host-context", 107 | ]); 108 | var traversalNames = new Set( 109 | __spreadArray( 110 | ["descendant"], 111 | Object.keys(Traversals).map(function (k) { 112 | return Traversals[k]; 113 | }), 114 | true 115 | ) 116 | ); 117 | /** 118 | * Attributes that are case-insensitive in HTML. 119 | * 120 | * @private 121 | * @see https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors 122 | */ 123 | var caseInsensitiveAttributes = new Set([ 124 | "accept", 125 | "accept-charset", 126 | "align", 127 | "alink", 128 | "axis", 129 | "bgcolor", 130 | "charset", 131 | "checked", 132 | "clear", 133 | "codetype", 134 | "color", 135 | "compact", 136 | "declare", 137 | "defer", 138 | "dir", 139 | "direction", 140 | "disabled", 141 | "enctype", 142 | "face", 143 | "frame", 144 | "hreflang", 145 | "http-equiv", 146 | "lang", 147 | "language", 148 | "link", 149 | "media", 150 | "method", 151 | "multiple", 152 | "nohref", 153 | "noresize", 154 | "noshade", 155 | "nowrap", 156 | "readonly", 157 | "rel", 158 | "rev", 159 | "rules", 160 | "scope", 161 | "scrolling", 162 | "selected", 163 | "shape", 164 | "target", 165 | "text", 166 | "type", 167 | "valign", 168 | "valuetype", 169 | "vlink", 170 | ]); 171 | /** 172 | * Checks whether a specific selector is a traversal. 173 | * This is useful eg. in swapping the order of elements that 174 | * are not traversals. 175 | * 176 | * @param selector Selector to check. 177 | */ 178 | function isTraversal(selector) { 179 | return traversalNames.has(selector.type); 180 | } 181 | exports.isTraversal = isTraversal; 182 | var stripQuotesFromPseudos = new Set(["contains", "icontains"]); 183 | var quotes = new Set(['"', "'"]); 184 | // Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152 185 | function funescape(_, escaped, escapedWhitespace) { 186 | var high = parseInt(escaped, 16) - 0x10000; 187 | // NaN means non-codepoint 188 | return high !== high || escapedWhitespace 189 | ? escaped 190 | : high < 0 191 | ? // BMP codepoint 192 | String.fromCharCode(high + 0x10000) 193 | : // Supplemental Plane codepoint (surrogate pair) 194 | String.fromCharCode( 195 | (high >> 10) | 0xd800, 196 | (high & 0x3ff) | 0xdc00 197 | ); 198 | } 199 | function unescapeCSS(str) { 200 | return str.replace(reEscape, funescape); 201 | } 202 | function isWhitespace(c) { 203 | return ( 204 | c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r" 205 | ); 206 | } 207 | /** 208 | * Parses `selector`, optionally with the passed `options`. 209 | * 210 | * @param selector Selector to parse. 211 | * @param options Options for parsing. 212 | * @returns Returns a two-dimensional array. 213 | * The first dimension represents selectors separated by commas (eg. `sub1, sub2`), 214 | * the second contains the relevant tokens for that selector. 215 | */ 216 | function parse(selector, options) { 217 | var subselects = []; 218 | var endIndex = parseSelector(subselects, "" + selector, options, 0); 219 | if (endIndex < selector.length) { 220 | throw new Error( 221 | "Unmatched selector: " + selector.slice(endIndex) 222 | ); 223 | } 224 | return subselects; 225 | } 226 | exports.default = parse; 227 | function parseSelector(subselects, selector, options, selectorIndex) { 228 | var _a, _b; 229 | if (options === void 0) { 230 | options = {}; 231 | } 232 | var tokens = []; 233 | var sawWS = false; 234 | function getName(offset) { 235 | var match = selector.slice(selectorIndex + offset).match(reName); 236 | if (!match) { 237 | throw new Error( 238 | "Expected name, found " + selector.slice(selectorIndex) 239 | ); 240 | } 241 | var name = match[0]; 242 | selectorIndex += offset + name.length; 243 | return unescapeCSS(name); 244 | } 245 | function stripWhitespace(offset) { 246 | while (isWhitespace(selector.charAt(selectorIndex + offset))) 247 | offset++; 248 | selectorIndex += offset; 249 | } 250 | function isEscaped(pos) { 251 | var slashCount = 0; 252 | while (selector.charAt(--pos) === "\\") slashCount++; 253 | return (slashCount & 1) === 1; 254 | } 255 | function ensureNotTraversal() { 256 | if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) { 257 | throw new Error("Did not expect successive traversals."); 258 | } 259 | } 260 | stripWhitespace(0); 261 | while (selector !== "") { 262 | var firstChar = selector.charAt(selectorIndex); 263 | if (isWhitespace(firstChar)) { 264 | sawWS = true; 265 | stripWhitespace(1); 266 | } else if (firstChar in Traversals) { 267 | ensureNotTraversal(); 268 | tokens.push({ type: Traversals[firstChar] }); 269 | sawWS = false; 270 | stripWhitespace(1); 271 | } else if (firstChar === ",") { 272 | if (tokens.length === 0) { 273 | throw new Error("Empty sub-selector"); 274 | } 275 | subselects.push(tokens); 276 | tokens = []; 277 | sawWS = false; 278 | stripWhitespace(1); 279 | } else if (selector.startsWith("/*", selectorIndex)) { 280 | var endIndex = selector.indexOf("*/", selectorIndex + 2); 281 | if (endIndex < 0) { 282 | throw new Error("Comment was not terminated"); 283 | } 284 | selectorIndex = endIndex + 2; 285 | } else { 286 | if (sawWS) { 287 | ensureNotTraversal(); 288 | tokens.push({ type: "descendant" }); 289 | sawWS = false; 290 | } 291 | if (firstChar in attribSelectors) { 292 | var _c = attribSelectors[firstChar], 293 | name_1 = _c[0], 294 | action = _c[1]; 295 | tokens.push({ 296 | type: "attribute", 297 | name: name_1, 298 | action: action, 299 | value: getName(1), 300 | namespace: null, 301 | // TODO: Add quirksMode option, which makes `ignoreCase` `true` for HTML. 302 | ignoreCase: options.xmlMode ? null : false, 303 | }); 304 | } else if (firstChar === "[") { 305 | stripWhitespace(1); 306 | // Determine attribute name and namespace 307 | var namespace = null; 308 | if (selector.charAt(selectorIndex) === "|") { 309 | namespace = ""; 310 | selectorIndex += 1; 311 | } 312 | if (selector.startsWith("*|", selectorIndex)) { 313 | namespace = "*"; 314 | selectorIndex += 2; 315 | } 316 | var name_2 = getName(0); 317 | if ( 318 | namespace === null && 319 | selector.charAt(selectorIndex) === "|" && 320 | selector.charAt(selectorIndex + 1) !== "=" 321 | ) { 322 | namespace = name_2; 323 | name_2 = getName(1); 324 | } 325 | if ( 326 | (_a = options.lowerCaseAttributeNames) !== null && 327 | _a !== void 0 328 | ? _a 329 | : !options.xmlMode 330 | ) { 331 | name_2 = name_2.toLowerCase(); 332 | } 333 | stripWhitespace(0); 334 | // Determine comparison operation 335 | var action = "exists"; 336 | var possibleAction = actionTypes.get( 337 | selector.charAt(selectorIndex) 338 | ); 339 | if (possibleAction) { 340 | action = possibleAction; 341 | if (selector.charAt(selectorIndex + 1) !== "=") { 342 | throw new Error("Expected `=`"); 343 | } 344 | stripWhitespace(2); 345 | } else if (selector.charAt(selectorIndex) === "=") { 346 | action = "equals"; 347 | stripWhitespace(1); 348 | } 349 | // Determine value 350 | var value = ""; 351 | var ignoreCase = null; 352 | if (action !== "exists") { 353 | if (quotes.has(selector.charAt(selectorIndex))) { 354 | var quote = selector.charAt(selectorIndex); 355 | var sectionEnd = selectorIndex + 1; 356 | while ( 357 | sectionEnd < selector.length && 358 | (selector.charAt(sectionEnd) !== quote || 359 | isEscaped(sectionEnd)) 360 | ) { 361 | sectionEnd += 1; 362 | } 363 | if (selector.charAt(sectionEnd) !== quote) { 364 | throw new Error("Attribute value didn't end"); 365 | } 366 | value = unescapeCSS( 367 | selector.slice(selectorIndex + 1, sectionEnd) 368 | ); 369 | selectorIndex = sectionEnd + 1; 370 | } else { 371 | var valueStart = selectorIndex; 372 | while ( 373 | selectorIndex < selector.length && 374 | ((!isWhitespace(selector.charAt(selectorIndex)) && 375 | selector.charAt(selectorIndex) !== "]") || 376 | isEscaped(selectorIndex)) 377 | ) { 378 | selectorIndex += 1; 379 | } 380 | value = unescapeCSS( 381 | selector.slice(valueStart, selectorIndex) 382 | ); 383 | } 384 | stripWhitespace(0); 385 | // See if we have a force ignore flag 386 | var forceIgnore = selector.charAt(selectorIndex); 387 | // If the forceIgnore flag is set (either `i` or `s`), use that value 388 | if (forceIgnore === "s" || forceIgnore === "S") { 389 | ignoreCase = false; 390 | stripWhitespace(1); 391 | } else if (forceIgnore === "i" || forceIgnore === "I") { 392 | ignoreCase = true; 393 | stripWhitespace(1); 394 | } 395 | } 396 | // If `xmlMode` is set, there are no rules; otherwise, use the `caseInsensitiveAttributes` list. 397 | if (!options.xmlMode) { 398 | // TODO: Skip this for `exists`, as there is no value to compare to. 399 | ignoreCase !== null && ignoreCase !== void 0 400 | ? ignoreCase 401 | : (ignoreCase = caseInsensitiveAttributes.has(name_2)); 402 | } 403 | if (selector.charAt(selectorIndex) !== "]") { 404 | throw new Error("Attribute selector didn't terminate"); 405 | } 406 | selectorIndex += 1; 407 | var attributeSelector = { 408 | type: "attribute", 409 | name: name_2, 410 | action: action, 411 | value: value, 412 | namespace: namespace, 413 | ignoreCase: ignoreCase, 414 | }; 415 | tokens.push(attributeSelector); 416 | } else if (firstChar === ":") { 417 | if (selector.charAt(selectorIndex + 1) === ":") { 418 | tokens.push({ 419 | type: "pseudo-element", 420 | name: getName(2).toLowerCase(), 421 | }); 422 | continue; 423 | } 424 | var name_3 = getName(1).toLowerCase(); 425 | var data = null; 426 | if (selector.charAt(selectorIndex) === "(") { 427 | if (unpackPseudos.has(name_3)) { 428 | if (quotes.has(selector.charAt(selectorIndex + 1))) { 429 | throw new Error( 430 | "Pseudo-selector " + name_3 + " cannot be quoted" 431 | ); 432 | } 433 | data = []; 434 | selectorIndex = parseSelector( 435 | data, 436 | selector, 437 | options, 438 | selectorIndex + 1 439 | ); 440 | if (selector.charAt(selectorIndex) !== ")") { 441 | throw new Error( 442 | "Missing closing parenthesis in :" + 443 | name_3 + 444 | " (" + 445 | selector + 446 | ")" 447 | ); 448 | } 449 | selectorIndex += 1; 450 | } else { 451 | selectorIndex += 1; 452 | var start = selectorIndex; 453 | var counter = 1; 454 | for ( 455 | ; 456 | counter > 0 && selectorIndex < selector.length; 457 | selectorIndex++ 458 | ) { 459 | if ( 460 | selector.charAt(selectorIndex) === "(" && 461 | !isEscaped(selectorIndex) 462 | ) { 463 | counter++; 464 | } else if ( 465 | selector.charAt(selectorIndex) === ")" && 466 | !isEscaped(selectorIndex) 467 | ) { 468 | counter--; 469 | } 470 | } 471 | if (counter) { 472 | throw new Error("Parenthesis not matched"); 473 | } 474 | data = selector.slice(start, selectorIndex - 1); 475 | if (stripQuotesFromPseudos.has(name_3)) { 476 | var quot = data.charAt(0); 477 | if (quot === data.slice(-1) && quotes.has(quot)) { 478 | data = data.slice(1, -1); 479 | } 480 | data = unescapeCSS(data); 481 | } 482 | } 483 | } 484 | tokens.push({ type: "pseudo", name: name_3, data: data }); 485 | } else { 486 | var namespace = null; 487 | var name_4 = void 0; 488 | if (firstChar === "*") { 489 | selectorIndex += 1; 490 | name_4 = "*"; 491 | } else if (reName.test(selector.slice(selectorIndex))) { 492 | if (selector.charAt(selectorIndex) === "|") { 493 | namespace = ""; 494 | selectorIndex += 1; 495 | } 496 | name_4 = getName(0); 497 | } else { 498 | /* 499 | * We have finished parsing the selector. 500 | * Remove descendant tokens at the end if they exist, 501 | * and return the last index, so that parsing can be 502 | * picked up from here. 503 | */ 504 | if ( 505 | tokens.length && 506 | tokens[tokens.length - 1].type === "descendant" 507 | ) { 508 | tokens.pop(); 509 | } 510 | addToken(subselects, tokens); 511 | return selectorIndex; 512 | } 513 | if (selector.charAt(selectorIndex) === "|") { 514 | namespace = name_4; 515 | if (selector.charAt(selectorIndex + 1) === "*") { 516 | name_4 = "*"; 517 | selectorIndex += 2; 518 | } else { 519 | name_4 = getName(1); 520 | } 521 | } 522 | if (name_4 === "*") { 523 | tokens.push({ type: "universal", namespace: namespace }); 524 | } else { 525 | if ( 526 | (_b = options.lowerCaseTags) !== null && _b !== void 0 527 | ? _b 528 | : !options.xmlMode 529 | ) { 530 | name_4 = name_4.toLowerCase(); 531 | } 532 | tokens.push({ 533 | type: "tag", 534 | name: name_4, 535 | namespace: namespace, 536 | }); 537 | } 538 | } 539 | } 540 | } 541 | addToken(subselects, tokens); 542 | return selectorIndex; 543 | } 544 | function addToken(subselects, tokens) { 545 | if (subselects.length > 0 && tokens.length === 0) { 546 | throw new Error("Empty sub-selector"); 547 | } 548 | subselects.push(tokens); 549 | } 550 | }, 551 | {}, 552 | ], 553 | 2: [ 554 | function (require, module, exports) { 555 | "use strict"; 556 | var __spreadArray = 557 | (this && this.__spreadArray) || 558 | function (to, from, pack) { 559 | if (pack || arguments.length === 2) 560 | for (var i = 0, l = from.length, ar; i < l; i++) { 561 | if (ar || !(i in from)) { 562 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 563 | ar[i] = from[i]; 564 | } 565 | } 566 | return to.concat(ar || Array.prototype.slice.call(from)); 567 | }; 568 | Object.defineProperty(exports, "__esModule", { value: true }); 569 | var actionTypes = { 570 | equals: "", 571 | element: "~", 572 | start: "^", 573 | end: "$", 574 | any: "*", 575 | not: "!", 576 | hyphen: "|", 577 | }; 578 | var charsToEscape = new Set( 579 | __spreadArray( 580 | __spreadArray( 581 | [], 582 | Object.keys(actionTypes) 583 | .map(function (typeKey) { 584 | return actionTypes[typeKey]; 585 | }) 586 | .filter(Boolean), 587 | true 588 | ), 589 | [":", "[", "]", " ", "\\", "(", ")", "'"], 590 | false 591 | ) 592 | ); 593 | /** 594 | * Turns `selector` back into a string. 595 | * 596 | * @param selector Selector to stringify. 597 | */ 598 | function stringify(selector) { 599 | return selector.map(stringifySubselector).join(", "); 600 | } 601 | exports.default = stringify; 602 | function stringifySubselector(token) { 603 | return token.map(stringifyToken).join(""); 604 | } 605 | function stringifyToken(token) { 606 | switch (token.type) { 607 | // Simple types 608 | case "child": 609 | return " > "; 610 | case "parent": 611 | return " < "; 612 | case "sibling": 613 | return " ~ "; 614 | case "adjacent": 615 | return " + "; 616 | case "descendant": 617 | return " "; 618 | case "universal": 619 | return getNamespace(token.namespace) + "*"; 620 | case "tag": 621 | return getNamespacedName(token); 622 | case "pseudo-element": 623 | return "::" + escapeName(token.name); 624 | case "pseudo": 625 | if (token.data === null) return ":" + escapeName(token.name); 626 | if (typeof token.data === "string") { 627 | return ( 628 | ":" + 629 | escapeName(token.name) + 630 | "(" + 631 | escapeName(token.data) + 632 | ")" 633 | ); 634 | } 635 | return ( 636 | ":" + 637 | escapeName(token.name) + 638 | "(" + 639 | stringify(token.data) + 640 | ")" 641 | ); 642 | case "attribute": { 643 | if ( 644 | token.name === "id" && 645 | token.action === "equals" && 646 | !token.ignoreCase && 647 | !token.namespace 648 | ) { 649 | return "#" + escapeName(token.value); 650 | } 651 | if ( 652 | token.name === "class" && 653 | token.action === "element" && 654 | !token.ignoreCase && 655 | !token.namespace 656 | ) { 657 | return "." + escapeName(token.value); 658 | } 659 | var name_1 = getNamespacedName(token); 660 | if (token.action === "exists") { 661 | return "[" + name_1 + "]"; 662 | } 663 | return ( 664 | "[" + 665 | name_1 + 666 | actionTypes[token.action] + 667 | "='" + 668 | escapeName(token.value) + 669 | "'" + 670 | (token.ignoreCase 671 | ? "i" 672 | : token.ignoreCase === false 673 | ? "s" 674 | : "") + 675 | "]" 676 | ); 677 | } 678 | } 679 | } 680 | function getNamespacedName(token) { 681 | return "" + getNamespace(token.namespace) + escapeName(token.name); 682 | } 683 | function getNamespace(namespace) { 684 | return namespace !== null 685 | ? (namespace === "*" ? "*" : escapeName(namespace)) + "|" 686 | : ""; 687 | } 688 | function escapeName(str) { 689 | return str 690 | .split("") 691 | .map(function (c) { 692 | return charsToEscape.has(c) ? "\\" + c : c; 693 | }) 694 | .join(""); 695 | } 696 | }, 697 | {}, 698 | ], 699 | "css-what": [ 700 | function (require, module, exports) { 701 | "use strict"; 702 | var __createBinding = 703 | (this && this.__createBinding) || 704 | (Object.create 705 | ? function (o, m, k, k2) { 706 | if (k2 === undefined) k2 = k; 707 | Object.defineProperty(o, k2, { 708 | enumerable: true, 709 | get: function () { 710 | return m[k]; 711 | }, 712 | }); 713 | } 714 | : function (o, m, k, k2) { 715 | if (k2 === undefined) k2 = k; 716 | o[k2] = m[k]; 717 | }); 718 | var __exportStar = 719 | (this && this.__exportStar) || 720 | function (m, exports) { 721 | for (var p in m) 722 | if ( 723 | p !== "default" && 724 | !Object.prototype.hasOwnProperty.call(exports, p) 725 | ) 726 | __createBinding(exports, m, p); 727 | }; 728 | var __importDefault = 729 | (this && this.__importDefault) || 730 | function (mod) { 731 | return mod && mod.__esModule ? mod : { default: mod }; 732 | }; 733 | Object.defineProperty(exports, "__esModule", { value: true }); 734 | exports.stringify = exports.parse = void 0; 735 | __exportStar(require("./parse"), exports); 736 | var parse_1 = require("./parse"); 737 | Object.defineProperty(exports, "parse", { 738 | enumerable: true, 739 | get: function () { 740 | return __importDefault(parse_1).default; 741 | }, 742 | }); 743 | var stringify_1 = require("./stringify"); 744 | Object.defineProperty(exports, "stringify", { 745 | enumerable: true, 746 | get: function () { 747 | return __importDefault(stringify_1).default; 748 | }, 749 | }); 750 | }, 751 | { "./parse": 1, "./stringify": 2 }, 752 | ], 753 | }, 754 | {}, 755 | [] 756 | )("css-what"); 757 | }); 758 | -------------------------------------------------------------------------------- /lib/underscore.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define('underscore', factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () { 5 | var current = global._; 6 | var exports = global._ = factory(); 7 | exports.noConflict = function () { global._ = current; return exports; }; 8 | }())); 9 | }(this, (function () { 10 | // Underscore.js 1.13.1 11 | // https://underscorejs.org 12 | // (c) 2009-2021 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors 13 | // Underscore may be freely distributed under the MIT license. 14 | 15 | // Current version. 16 | var VERSION = '1.13.1'; 17 | 18 | // Establish the root object, `window` (`self`) in the browser, `global` 19 | // on the server, or `this` in some virtual machines. We use `self` 20 | // instead of `window` for `WebWorker` support. 21 | var root = typeof self == 'object' && self.self === self && self || 22 | typeof global == 'object' && global.global === global && global || 23 | Function('return this')() || 24 | {}; 25 | 26 | // Save bytes in the minified (but not gzipped) version: 27 | var ArrayProto = Array.prototype, ObjProto = Object.prototype; 28 | var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; 29 | 30 | // Create quick reference variables for speed access to core prototypes. 31 | var push = ArrayProto.push, 32 | slice = ArrayProto.slice, 33 | toString = ObjProto.toString, 34 | hasOwnProperty = ObjProto.hasOwnProperty; 35 | 36 | // Modern feature detection. 37 | var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined', 38 | supportsDataView = typeof DataView !== 'undefined'; 39 | 40 | // All **ECMAScript 5+** native function implementations that we hope to use 41 | // are declared here. 42 | var nativeIsArray = Array.isArray, 43 | nativeKeys = Object.keys, 44 | nativeCreate = Object.create, 45 | nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; 46 | 47 | // Create references to these builtin functions because we override them. 48 | var _isNaN = isNaN, 49 | _isFinite = isFinite; 50 | 51 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. 52 | var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 53 | var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 54 | 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 55 | 56 | // The largest integer that can be represented exactly. 57 | var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; 58 | 59 | // Some functions take a variable number of arguments, or a few expected 60 | // arguments at the beginning and then a variable number of values to operate 61 | // on. This helper accumulates all remaining arguments past the function’s 62 | // argument length (or an explicit `startIndex`), into an array that becomes 63 | // the last argument. Similar to ES6’s "rest parameter". 64 | function restArguments(func, startIndex) { 65 | startIndex = startIndex == null ? func.length - 1 : +startIndex; 66 | return function() { 67 | var length = Math.max(arguments.length - startIndex, 0), 68 | rest = Array(length), 69 | index = 0; 70 | for (; index < length; index++) { 71 | rest[index] = arguments[index + startIndex]; 72 | } 73 | switch (startIndex) { 74 | case 0: return func.call(this, rest); 75 | case 1: return func.call(this, arguments[0], rest); 76 | case 2: return func.call(this, arguments[0], arguments[1], rest); 77 | } 78 | var args = Array(startIndex + 1); 79 | for (index = 0; index < startIndex; index++) { 80 | args[index] = arguments[index]; 81 | } 82 | args[startIndex] = rest; 83 | return func.apply(this, args); 84 | }; 85 | } 86 | 87 | // Is a given variable an object? 88 | function isObject(obj) { 89 | var type = typeof obj; 90 | return type === 'function' || type === 'object' && !!obj; 91 | } 92 | 93 | // Is a given value equal to null? 94 | function isNull(obj) { 95 | return obj === null; 96 | } 97 | 98 | // Is a given variable undefined? 99 | function isUndefined(obj) { 100 | return obj === void 0; 101 | } 102 | 103 | // Is a given value a boolean? 104 | function isBoolean(obj) { 105 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; 106 | } 107 | 108 | // Is a given value a DOM element? 109 | function isElement(obj) { 110 | return !!(obj && obj.nodeType === 1); 111 | } 112 | 113 | // Internal function for creating a `toString`-based type tester. 114 | function tagTester(name) { 115 | var tag = '[object ' + name + ']'; 116 | return function(obj) { 117 | return toString.call(obj) === tag; 118 | }; 119 | } 120 | 121 | var isString = tagTester('String'); 122 | 123 | var isNumber = tagTester('Number'); 124 | 125 | var isDate = tagTester('Date'); 126 | 127 | var isRegExp = tagTester('RegExp'); 128 | 129 | var isError = tagTester('Error'); 130 | 131 | var isSymbol = tagTester('Symbol'); 132 | 133 | var isArrayBuffer = tagTester('ArrayBuffer'); 134 | 135 | var isFunction = tagTester('Function'); 136 | 137 | // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old 138 | // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). 139 | var nodelist = root.document && root.document.childNodes; 140 | if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { 141 | isFunction = function(obj) { 142 | return typeof obj == 'function' || false; 143 | }; 144 | } 145 | 146 | var isFunction$1 = isFunction; 147 | 148 | var hasObjectTag = tagTester('Object'); 149 | 150 | // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`. 151 | // In IE 11, the most common among them, this problem also applies to 152 | // `Map`, `WeakMap` and `Set`. 153 | var hasStringTagBug = ( 154 | supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))) 155 | ), 156 | isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map)); 157 | 158 | var isDataView = tagTester('DataView'); 159 | 160 | // In IE 10 - Edge 13, we need a different heuristic 161 | // to determine whether an object is a `DataView`. 162 | function ie10IsDataView(obj) { 163 | return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer); 164 | } 165 | 166 | var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView); 167 | 168 | // Is a given value an array? 169 | // Delegates to ECMA5's native `Array.isArray`. 170 | var isArray = nativeIsArray || tagTester('Array'); 171 | 172 | // Internal function to check whether `key` is an own property name of `obj`. 173 | function has$1(obj, key) { 174 | return obj != null && hasOwnProperty.call(obj, key); 175 | } 176 | 177 | var isArguments = tagTester('Arguments'); 178 | 179 | // Define a fallback version of the method in browsers (ahem, IE < 9), where 180 | // there isn't any inspectable "Arguments" type. 181 | (function() { 182 | if (!isArguments(arguments)) { 183 | isArguments = function(obj) { 184 | return has$1(obj, 'callee'); 185 | }; 186 | } 187 | }()); 188 | 189 | var isArguments$1 = isArguments; 190 | 191 | // Is a given object a finite number? 192 | function isFinite$1(obj) { 193 | return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); 194 | } 195 | 196 | // Is the given value `NaN`? 197 | function isNaN$1(obj) { 198 | return isNumber(obj) && _isNaN(obj); 199 | } 200 | 201 | // Predicate-generating function. Often useful outside of Underscore. 202 | function constant(value) { 203 | return function() { 204 | return value; 205 | }; 206 | } 207 | 208 | // Common internal logic for `isArrayLike` and `isBufferLike`. 209 | function createSizePropertyCheck(getSizeProperty) { 210 | return function(collection) { 211 | var sizeProperty = getSizeProperty(collection); 212 | return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; 213 | } 214 | } 215 | 216 | // Internal helper to generate a function to obtain property `key` from `obj`. 217 | function shallowProperty(key) { 218 | return function(obj) { 219 | return obj == null ? void 0 : obj[key]; 220 | }; 221 | } 222 | 223 | // Internal helper to obtain the `byteLength` property of an object. 224 | var getByteLength = shallowProperty('byteLength'); 225 | 226 | // Internal helper to determine whether we should spend extensive checks against 227 | // `ArrayBuffer` et al. 228 | var isBufferLike = createSizePropertyCheck(getByteLength); 229 | 230 | // Is a given value a typed array? 231 | var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; 232 | function isTypedArray(obj) { 233 | // `ArrayBuffer.isView` is the most future-proof, so use it when available. 234 | // Otherwise, fall back on the above regular expression. 235 | return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) : 236 | isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); 237 | } 238 | 239 | var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); 240 | 241 | // Internal helper to obtain the `length` property of an object. 242 | var getLength = shallowProperty('length'); 243 | 244 | // Internal helper to create a simple lookup structure. 245 | // `collectNonEnumProps` used to depend on `_.contains`, but this led to 246 | // circular imports. `emulatedSet` is a one-off solution that only works for 247 | // arrays of strings. 248 | function emulatedSet(keys) { 249 | var hash = {}; 250 | for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; 251 | return { 252 | contains: function(key) { return hash[key]; }, 253 | push: function(key) { 254 | hash[key] = true; 255 | return keys.push(key); 256 | } 257 | }; 258 | } 259 | 260 | // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't 261 | // be iterated by `for key in ...` and thus missed. Extends `keys` in place if 262 | // needed. 263 | function collectNonEnumProps(obj, keys) { 264 | keys = emulatedSet(keys); 265 | var nonEnumIdx = nonEnumerableProps.length; 266 | var constructor = obj.constructor; 267 | var proto = isFunction$1(constructor) && constructor.prototype || ObjProto; 268 | 269 | // Constructor is a special case. 270 | var prop = 'constructor'; 271 | if (has$1(obj, prop) && !keys.contains(prop)) keys.push(prop); 272 | 273 | while (nonEnumIdx--) { 274 | prop = nonEnumerableProps[nonEnumIdx]; 275 | if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { 276 | keys.push(prop); 277 | } 278 | } 279 | } 280 | 281 | // Retrieve the names of an object's own properties. 282 | // Delegates to **ECMAScript 5**'s native `Object.keys`. 283 | function keys(obj) { 284 | if (!isObject(obj)) return []; 285 | if (nativeKeys) return nativeKeys(obj); 286 | var keys = []; 287 | for (var key in obj) if (has$1(obj, key)) keys.push(key); 288 | // Ahem, IE < 9. 289 | if (hasEnumBug) collectNonEnumProps(obj, keys); 290 | return keys; 291 | } 292 | 293 | // Is a given array, string, or object empty? 294 | // An "empty" object has no enumerable own-properties. 295 | function isEmpty(obj) { 296 | if (obj == null) return true; 297 | // Skip the more expensive `toString`-based type checks if `obj` has no 298 | // `.length`. 299 | var length = getLength(obj); 300 | if (typeof length == 'number' && ( 301 | isArray(obj) || isString(obj) || isArguments$1(obj) 302 | )) return length === 0; 303 | return getLength(keys(obj)) === 0; 304 | } 305 | 306 | // Returns whether an object has a given set of `key:value` pairs. 307 | function isMatch(object, attrs) { 308 | var _keys = keys(attrs), length = _keys.length; 309 | if (object == null) return !length; 310 | var obj = Object(object); 311 | for (var i = 0; i < length; i++) { 312 | var key = _keys[i]; 313 | if (attrs[key] !== obj[key] || !(key in obj)) return false; 314 | } 315 | return true; 316 | } 317 | 318 | // If Underscore is called as a function, it returns a wrapped object that can 319 | // be used OO-style. This wrapper holds altered versions of all functions added 320 | // through `_.mixin`. Wrapped objects may be chained. 321 | function _$1(obj) { 322 | if (obj instanceof _$1) return obj; 323 | if (!(this instanceof _$1)) return new _$1(obj); 324 | this._wrapped = obj; 325 | } 326 | 327 | _$1.VERSION = VERSION; 328 | 329 | // Extracts the result from a wrapped and chained object. 330 | _$1.prototype.value = function() { 331 | return this._wrapped; 332 | }; 333 | 334 | // Provide unwrapping proxies for some methods used in engine operations 335 | // such as arithmetic and JSON stringification. 336 | _$1.prototype.valueOf = _$1.prototype.toJSON = _$1.prototype.value; 337 | 338 | _$1.prototype.toString = function() { 339 | return String(this._wrapped); 340 | }; 341 | 342 | // Internal function to wrap or shallow-copy an ArrayBuffer, 343 | // typed array or DataView to a new view, reusing the buffer. 344 | function toBufferView(bufferSource) { 345 | return new Uint8Array( 346 | bufferSource.buffer || bufferSource, 347 | bufferSource.byteOffset || 0, 348 | getByteLength(bufferSource) 349 | ); 350 | } 351 | 352 | // We use this string twice, so give it a name for minification. 353 | var tagDataView = '[object DataView]'; 354 | 355 | // Internal recursive comparison function for `_.isEqual`. 356 | function eq(a, b, aStack, bStack) { 357 | // Identical objects are equal. `0 === -0`, but they aren't identical. 358 | // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). 359 | if (a === b) return a !== 0 || 1 / a === 1 / b; 360 | // `null` or `undefined` only equal to itself (strict comparison). 361 | if (a == null || b == null) return false; 362 | // `NaN`s are equivalent, but non-reflexive. 363 | if (a !== a) return b !== b; 364 | // Exhaust primitive checks 365 | var type = typeof a; 366 | if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; 367 | return deepEq(a, b, aStack, bStack); 368 | } 369 | 370 | // Internal recursive comparison function for `_.isEqual`. 371 | function deepEq(a, b, aStack, bStack) { 372 | // Unwrap any wrapped objects. 373 | if (a instanceof _$1) a = a._wrapped; 374 | if (b instanceof _$1) b = b._wrapped; 375 | // Compare `[[Class]]` names. 376 | var className = toString.call(a); 377 | if (className !== toString.call(b)) return false; 378 | // Work around a bug in IE 10 - Edge 13. 379 | if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) { 380 | if (!isDataView$1(b)) return false; 381 | className = tagDataView; 382 | } 383 | switch (className) { 384 | // These types are compared by value. 385 | case '[object RegExp]': 386 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') 387 | case '[object String]': 388 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 389 | // equivalent to `new String("5")`. 390 | return '' + a === '' + b; 391 | case '[object Number]': 392 | // `NaN`s are equivalent, but non-reflexive. 393 | // Object(NaN) is equivalent to NaN. 394 | if (+a !== +a) return +b !== +b; 395 | // An `egal` comparison is performed for other numeric values. 396 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 397 | case '[object Date]': 398 | case '[object Boolean]': 399 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 400 | // millisecond representations. Note that invalid dates with millisecond representations 401 | // of `NaN` are not equivalent. 402 | return +a === +b; 403 | case '[object Symbol]': 404 | return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); 405 | case '[object ArrayBuffer]': 406 | case tagDataView: 407 | // Coerce to typed array so we can fall through. 408 | return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); 409 | } 410 | 411 | var areArrays = className === '[object Array]'; 412 | if (!areArrays && isTypedArray$1(a)) { 413 | var byteLength = getByteLength(a); 414 | if (byteLength !== getByteLength(b)) return false; 415 | if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; 416 | areArrays = true; 417 | } 418 | if (!areArrays) { 419 | if (typeof a != 'object' || typeof b != 'object') return false; 420 | 421 | // Objects with different constructors are not equivalent, but `Object`s or `Array`s 422 | // from different frames are. 423 | var aCtor = a.constructor, bCtor = b.constructor; 424 | if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && 425 | isFunction$1(bCtor) && bCtor instanceof bCtor) 426 | && ('constructor' in a && 'constructor' in b)) { 427 | return false; 428 | } 429 | } 430 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 431 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 432 | 433 | // Initializing stack of traversed objects. 434 | // It's done here since we only need them for objects and arrays comparison. 435 | aStack = aStack || []; 436 | bStack = bStack || []; 437 | var length = aStack.length; 438 | while (length--) { 439 | // Linear search. Performance is inversely proportional to the number of 440 | // unique nested structures. 441 | if (aStack[length] === a) return bStack[length] === b; 442 | } 443 | 444 | // Add the first object to the stack of traversed objects. 445 | aStack.push(a); 446 | bStack.push(b); 447 | 448 | // Recursively compare objects and arrays. 449 | if (areArrays) { 450 | // Compare array lengths to determine if a deep comparison is necessary. 451 | length = a.length; 452 | if (length !== b.length) return false; 453 | // Deep compare the contents, ignoring non-numeric properties. 454 | while (length--) { 455 | if (!eq(a[length], b[length], aStack, bStack)) return false; 456 | } 457 | } else { 458 | // Deep compare objects. 459 | var _keys = keys(a), key; 460 | length = _keys.length; 461 | // Ensure that both objects contain the same number of properties before comparing deep equality. 462 | if (keys(b).length !== length) return false; 463 | while (length--) { 464 | // Deep compare each member 465 | key = _keys[length]; 466 | if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; 467 | } 468 | } 469 | // Remove the first object from the stack of traversed objects. 470 | aStack.pop(); 471 | bStack.pop(); 472 | return true; 473 | } 474 | 475 | // Perform a deep comparison to check if two objects are equal. 476 | function isEqual(a, b) { 477 | return eq(a, b); 478 | } 479 | 480 | // Retrieve all the enumerable property names of an object. 481 | function allKeys(obj) { 482 | if (!isObject(obj)) return []; 483 | var keys = []; 484 | for (var key in obj) keys.push(key); 485 | // Ahem, IE < 9. 486 | if (hasEnumBug) collectNonEnumProps(obj, keys); 487 | return keys; 488 | } 489 | 490 | // Since the regular `Object.prototype.toString` type tests don't work for 491 | // some types in IE 11, we use a fingerprinting heuristic instead, based 492 | // on the methods. It's not great, but it's the best we got. 493 | // The fingerprint method lists are defined below. 494 | function ie11fingerprint(methods) { 495 | var length = getLength(methods); 496 | return function(obj) { 497 | if (obj == null) return false; 498 | // `Map`, `WeakMap` and `Set` have no enumerable keys. 499 | var keys = allKeys(obj); 500 | if (getLength(keys)) return false; 501 | for (var i = 0; i < length; i++) { 502 | if (!isFunction$1(obj[methods[i]])) return false; 503 | } 504 | // If we are testing against `WeakMap`, we need to ensure that 505 | // `obj` doesn't have a `forEach` method in order to distinguish 506 | // it from a regular `Map`. 507 | return methods !== weakMapMethods || !isFunction$1(obj[forEachName]); 508 | }; 509 | } 510 | 511 | // In the interest of compact minification, we write 512 | // each string in the fingerprints only once. 513 | var forEachName = 'forEach', 514 | hasName = 'has', 515 | commonInit = ['clear', 'delete'], 516 | mapTail = ['get', hasName, 'set']; 517 | 518 | // `Map`, `WeakMap` and `Set` each have slightly different 519 | // combinations of the above sublists. 520 | var mapMethods = commonInit.concat(forEachName, mapTail), 521 | weakMapMethods = commonInit.concat(mapTail), 522 | setMethods = ['add'].concat(commonInit, forEachName, hasName); 523 | 524 | var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map'); 525 | 526 | var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap'); 527 | 528 | var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set'); 529 | 530 | var isWeakSet = tagTester('WeakSet'); 531 | 532 | // Retrieve the values of an object's properties. 533 | function values(obj) { 534 | var _keys = keys(obj); 535 | var length = _keys.length; 536 | var values = Array(length); 537 | for (var i = 0; i < length; i++) { 538 | values[i] = obj[_keys[i]]; 539 | } 540 | return values; 541 | } 542 | 543 | // Convert an object into a list of `[key, value]` pairs. 544 | // The opposite of `_.object` with one argument. 545 | function pairs(obj) { 546 | var _keys = keys(obj); 547 | var length = _keys.length; 548 | var pairs = Array(length); 549 | for (var i = 0; i < length; i++) { 550 | pairs[i] = [_keys[i], obj[_keys[i]]]; 551 | } 552 | return pairs; 553 | } 554 | 555 | // Invert the keys and values of an object. The values must be serializable. 556 | function invert(obj) { 557 | var result = {}; 558 | var _keys = keys(obj); 559 | for (var i = 0, length = _keys.length; i < length; i++) { 560 | result[obj[_keys[i]]] = _keys[i]; 561 | } 562 | return result; 563 | } 564 | 565 | // Return a sorted list of the function names available on the object. 566 | function functions(obj) { 567 | var names = []; 568 | for (var key in obj) { 569 | if (isFunction$1(obj[key])) names.push(key); 570 | } 571 | return names.sort(); 572 | } 573 | 574 | // An internal function for creating assigner functions. 575 | function createAssigner(keysFunc, defaults) { 576 | return function(obj) { 577 | var length = arguments.length; 578 | if (defaults) obj = Object(obj); 579 | if (length < 2 || obj == null) return obj; 580 | for (var index = 1; index < length; index++) { 581 | var source = arguments[index], 582 | keys = keysFunc(source), 583 | l = keys.length; 584 | for (var i = 0; i < l; i++) { 585 | var key = keys[i]; 586 | if (!defaults || obj[key] === void 0) obj[key] = source[key]; 587 | } 588 | } 589 | return obj; 590 | }; 591 | } 592 | 593 | // Extend a given object with all the properties in passed-in object(s). 594 | var extend = createAssigner(allKeys); 595 | 596 | // Assigns a given object with all the own properties in the passed-in 597 | // object(s). 598 | // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 599 | var extendOwn = createAssigner(keys); 600 | 601 | // Fill in a given object with default properties. 602 | var defaults = createAssigner(allKeys, true); 603 | 604 | // Create a naked function reference for surrogate-prototype-swapping. 605 | function ctor() { 606 | return function(){}; 607 | } 608 | 609 | // An internal function for creating a new object that inherits from another. 610 | function baseCreate(prototype) { 611 | if (!isObject(prototype)) return {}; 612 | if (nativeCreate) return nativeCreate(prototype); 613 | var Ctor = ctor(); 614 | Ctor.prototype = prototype; 615 | var result = new Ctor; 616 | Ctor.prototype = null; 617 | return result; 618 | } 619 | 620 | // Creates an object that inherits from the given prototype object. 621 | // If additional properties are provided then they will be added to the 622 | // created object. 623 | function create(prototype, props) { 624 | var result = baseCreate(prototype); 625 | if (props) extendOwn(result, props); 626 | return result; 627 | } 628 | 629 | // Create a (shallow-cloned) duplicate of an object. 630 | function clone(obj) { 631 | if (!isObject(obj)) return obj; 632 | return isArray(obj) ? obj.slice() : extend({}, obj); 633 | } 634 | 635 | // Invokes `interceptor` with the `obj` and then returns `obj`. 636 | // The primary purpose of this method is to "tap into" a method chain, in 637 | // order to perform operations on intermediate results within the chain. 638 | function tap(obj, interceptor) { 639 | interceptor(obj); 640 | return obj; 641 | } 642 | 643 | // Normalize a (deep) property `path` to array. 644 | // Like `_.iteratee`, this function can be customized. 645 | function toPath$1(path) { 646 | return isArray(path) ? path : [path]; 647 | } 648 | _$1.toPath = toPath$1; 649 | 650 | // Internal wrapper for `_.toPath` to enable minification. 651 | // Similar to `cb` for `_.iteratee`. 652 | function toPath(path) { 653 | return _$1.toPath(path); 654 | } 655 | 656 | // Internal function to obtain a nested property in `obj` along `path`. 657 | function deepGet(obj, path) { 658 | var length = path.length; 659 | for (var i = 0; i < length; i++) { 660 | if (obj == null) return void 0; 661 | obj = obj[path[i]]; 662 | } 663 | return length ? obj : void 0; 664 | } 665 | 666 | // Get the value of the (deep) property on `path` from `object`. 667 | // If any property in `path` does not exist or if the value is 668 | // `undefined`, return `defaultValue` instead. 669 | // The `path` is normalized through `_.toPath`. 670 | function get(object, path, defaultValue) { 671 | var value = deepGet(object, toPath(path)); 672 | return isUndefined(value) ? defaultValue : value; 673 | } 674 | 675 | // Shortcut function for checking if an object has a given property directly on 676 | // itself (in other words, not on a prototype). Unlike the internal `has` 677 | // function, this public version can also traverse nested properties. 678 | function has(obj, path) { 679 | path = toPath(path); 680 | var length = path.length; 681 | for (var i = 0; i < length; i++) { 682 | var key = path[i]; 683 | if (!has$1(obj, key)) return false; 684 | obj = obj[key]; 685 | } 686 | return !!length; 687 | } 688 | 689 | // Keep the identity function around for default iteratees. 690 | function identity(value) { 691 | return value; 692 | } 693 | 694 | // Returns a predicate for checking whether an object has a given set of 695 | // `key:value` pairs. 696 | function matcher(attrs) { 697 | attrs = extendOwn({}, attrs); 698 | return function(obj) { 699 | return isMatch(obj, attrs); 700 | }; 701 | } 702 | 703 | // Creates a function that, when passed an object, will traverse that object’s 704 | // properties down the given `path`, specified as an array of keys or indices. 705 | function property(path) { 706 | path = toPath(path); 707 | return function(obj) { 708 | return deepGet(obj, path); 709 | }; 710 | } 711 | 712 | // Internal function that returns an efficient (for current engines) version 713 | // of the passed-in callback, to be repeatedly applied in other Underscore 714 | // functions. 715 | function optimizeCb(func, context, argCount) { 716 | if (context === void 0) return func; 717 | switch (argCount == null ? 3 : argCount) { 718 | case 1: return function(value) { 719 | return func.call(context, value); 720 | }; 721 | // The 2-argument case is omitted because we’re not using it. 722 | case 3: return function(value, index, collection) { 723 | return func.call(context, value, index, collection); 724 | }; 725 | case 4: return function(accumulator, value, index, collection) { 726 | return func.call(context, accumulator, value, index, collection); 727 | }; 728 | } 729 | return function() { 730 | return func.apply(context, arguments); 731 | }; 732 | } 733 | 734 | // An internal function to generate callbacks that can be applied to each 735 | // element in a collection, returning the desired result — either `_.identity`, 736 | // an arbitrary callback, a property matcher, or a property accessor. 737 | function baseIteratee(value, context, argCount) { 738 | if (value == null) return identity; 739 | if (isFunction$1(value)) return optimizeCb(value, context, argCount); 740 | if (isObject(value) && !isArray(value)) return matcher(value); 741 | return property(value); 742 | } 743 | 744 | // External wrapper for our callback generator. Users may customize 745 | // `_.iteratee` if they want additional predicate/iteratee shorthand styles. 746 | // This abstraction hides the internal-only `argCount` argument. 747 | function iteratee(value, context) { 748 | return baseIteratee(value, context, Infinity); 749 | } 750 | _$1.iteratee = iteratee; 751 | 752 | // The function we call internally to generate a callback. It invokes 753 | // `_.iteratee` if overridden, otherwise `baseIteratee`. 754 | function cb(value, context, argCount) { 755 | if (_$1.iteratee !== iteratee) return _$1.iteratee(value, context); 756 | return baseIteratee(value, context, argCount); 757 | } 758 | 759 | // Returns the results of applying the `iteratee` to each element of `obj`. 760 | // In contrast to `_.map` it returns an object. 761 | function mapObject(obj, iteratee, context) { 762 | iteratee = cb(iteratee, context); 763 | var _keys = keys(obj), 764 | length = _keys.length, 765 | results = {}; 766 | for (var index = 0; index < length; index++) { 767 | var currentKey = _keys[index]; 768 | results[currentKey] = iteratee(obj[currentKey], currentKey, obj); 769 | } 770 | return results; 771 | } 772 | 773 | // Predicate-generating function. Often useful outside of Underscore. 774 | function noop(){} 775 | 776 | // Generates a function for a given object that returns a given property. 777 | function propertyOf(obj) { 778 | if (obj == null) return noop; 779 | return function(path) { 780 | return get(obj, path); 781 | }; 782 | } 783 | 784 | // Run a function **n** times. 785 | function times(n, iteratee, context) { 786 | var accum = Array(Math.max(0, n)); 787 | iteratee = optimizeCb(iteratee, context, 1); 788 | for (var i = 0; i < n; i++) accum[i] = iteratee(i); 789 | return accum; 790 | } 791 | 792 | // Return a random integer between `min` and `max` (inclusive). 793 | function random(min, max) { 794 | if (max == null) { 795 | max = min; 796 | min = 0; 797 | } 798 | return min + Math.floor(Math.random() * (max - min + 1)); 799 | } 800 | 801 | // A (possibly faster) way to get the current timestamp as an integer. 802 | var now = Date.now || function() { 803 | return new Date().getTime(); 804 | }; 805 | 806 | // Internal helper to generate functions for escaping and unescaping strings 807 | // to/from HTML interpolation. 808 | function createEscaper(map) { 809 | var escaper = function(match) { 810 | return map[match]; 811 | }; 812 | // Regexes for identifying a key that needs to be escaped. 813 | var source = '(?:' + keys(map).join('|') + ')'; 814 | var testRegexp = RegExp(source); 815 | var replaceRegexp = RegExp(source, 'g'); 816 | return function(string) { 817 | string = string == null ? '' : '' + string; 818 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 819 | }; 820 | } 821 | 822 | // Internal list of HTML entities for escaping. 823 | var escapeMap = { 824 | '&': '&', 825 | '<': '<', 826 | '>': '>', 827 | '"': '"', 828 | "'": ''', 829 | '`': '`' 830 | }; 831 | 832 | // Function for escaping strings to HTML interpolation. 833 | var _escape = createEscaper(escapeMap); 834 | 835 | // Internal list of HTML entities for unescaping. 836 | var unescapeMap = invert(escapeMap); 837 | 838 | // Function for unescaping strings from HTML interpolation. 839 | var _unescape = createEscaper(unescapeMap); 840 | 841 | // By default, Underscore uses ERB-style template delimiters. Change the 842 | // following template settings to use alternative delimiters. 843 | var templateSettings = _$1.templateSettings = { 844 | evaluate: /<%([\s\S]+?)%>/g, 845 | interpolate: /<%=([\s\S]+?)%>/g, 846 | escape: /<%-([\s\S]+?)%>/g 847 | }; 848 | 849 | // When customizing `_.templateSettings`, if you don't want to define an 850 | // interpolation, evaluation or escaping regex, we need one that is 851 | // guaranteed not to match. 852 | var noMatch = /(.)^/; 853 | 854 | // Certain characters need to be escaped so that they can be put into a 855 | // string literal. 856 | var escapes = { 857 | "'": "'", 858 | '\\': '\\', 859 | '\r': 'r', 860 | '\n': 'n', 861 | '\u2028': 'u2028', 862 | '\u2029': 'u2029' 863 | }; 864 | 865 | var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; 866 | 867 | function escapeChar(match) { 868 | return '\\' + escapes[match]; 869 | } 870 | 871 | // In order to prevent third-party code injection through 872 | // `_.templateSettings.variable`, we test it against the following regular 873 | // expression. It is intentionally a bit more liberal than just matching valid 874 | // identifiers, but still prevents possible loopholes through defaults or 875 | // destructuring assignment. 876 | var bareIdentifier = /^\s*(\w|\$)+\s*$/; 877 | 878 | // JavaScript micro-templating, similar to John Resig's implementation. 879 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 880 | // and correctly escapes quotes within interpolated code. 881 | // NB: `oldSettings` only exists for backwards compatibility. 882 | function template(text, settings, oldSettings) { 883 | if (!settings && oldSettings) settings = oldSettings; 884 | settings = defaults({}, settings, _$1.templateSettings); 885 | 886 | // Combine delimiters into one regular expression via alternation. 887 | var matcher = RegExp([ 888 | (settings.escape || noMatch).source, 889 | (settings.interpolate || noMatch).source, 890 | (settings.evaluate || noMatch).source 891 | ].join('|') + '|$', 'g'); 892 | 893 | // Compile the template source, escaping string literals appropriately. 894 | var index = 0; 895 | var source = "__p+='"; 896 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 897 | source += text.slice(index, offset).replace(escapeRegExp, escapeChar); 898 | index = offset + match.length; 899 | 900 | if (escape) { 901 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 902 | } else if (interpolate) { 903 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 904 | } else if (evaluate) { 905 | source += "';\n" + evaluate + "\n__p+='"; 906 | } 907 | 908 | // Adobe VMs need the match returned to produce the correct offset. 909 | return match; 910 | }); 911 | source += "';\n"; 912 | 913 | var argument = settings.variable; 914 | if (argument) { 915 | // Insure against third-party code injection. (CVE-2021-23358) 916 | if (!bareIdentifier.test(argument)) throw new Error( 917 | 'variable is not a bare identifier: ' + argument 918 | ); 919 | } else { 920 | // If a variable is not specified, place data values in local scope. 921 | source = 'with(obj||{}){\n' + source + '}\n'; 922 | argument = 'obj'; 923 | } 924 | 925 | source = "var __t,__p='',__j=Array.prototype.join," + 926 | "print=function(){__p+=__j.call(arguments,'');};\n" + 927 | source + 'return __p;\n'; 928 | 929 | var render; 930 | try { 931 | render = new Function(argument, '_', source); 932 | } catch (e) { 933 | e.source = source; 934 | throw e; 935 | } 936 | 937 | var template = function(data) { 938 | return render.call(this, data, _$1); 939 | }; 940 | 941 | // Provide the compiled source as a convenience for precompilation. 942 | template.source = 'function(' + argument + '){\n' + source + '}'; 943 | 944 | return template; 945 | } 946 | 947 | // Traverses the children of `obj` along `path`. If a child is a function, it 948 | // is invoked with its parent as context. Returns the value of the final 949 | // child, or `fallback` if any child is undefined. 950 | function result(obj, path, fallback) { 951 | path = toPath(path); 952 | var length = path.length; 953 | if (!length) { 954 | return isFunction$1(fallback) ? fallback.call(obj) : fallback; 955 | } 956 | for (var i = 0; i < length; i++) { 957 | var prop = obj == null ? void 0 : obj[path[i]]; 958 | if (prop === void 0) { 959 | prop = fallback; 960 | i = length; // Ensure we don't continue iterating. 961 | } 962 | obj = isFunction$1(prop) ? prop.call(obj) : prop; 963 | } 964 | return obj; 965 | } 966 | 967 | // Generate a unique integer id (unique within the entire client session). 968 | // Useful for temporary DOM ids. 969 | var idCounter = 0; 970 | function uniqueId(prefix) { 971 | var id = ++idCounter + ''; 972 | return prefix ? prefix + id : id; 973 | } 974 | 975 | // Start chaining a wrapped Underscore object. 976 | function chain(obj) { 977 | var instance = _$1(obj); 978 | instance._chain = true; 979 | return instance; 980 | } 981 | 982 | // Internal function to execute `sourceFunc` bound to `context` with optional 983 | // `args`. Determines whether to execute a function as a constructor or as a 984 | // normal function. 985 | function executeBound(sourceFunc, boundFunc, context, callingContext, args) { 986 | if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 987 | var self = baseCreate(sourceFunc.prototype); 988 | var result = sourceFunc.apply(self, args); 989 | if (isObject(result)) return result; 990 | return self; 991 | } 992 | 993 | // Partially apply a function by creating a version that has had some of its 994 | // arguments pre-filled, without changing its dynamic `this` context. `_` acts 995 | // as a placeholder by default, allowing any combination of arguments to be 996 | // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. 997 | var partial = restArguments(function(func, boundArgs) { 998 | var placeholder = partial.placeholder; 999 | var bound = function() { 1000 | var position = 0, length = boundArgs.length; 1001 | var args = Array(length); 1002 | for (var i = 0; i < length; i++) { 1003 | args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; 1004 | } 1005 | while (position < arguments.length) args.push(arguments[position++]); 1006 | return executeBound(func, bound, this, this, args); 1007 | }; 1008 | return bound; 1009 | }); 1010 | 1011 | partial.placeholder = _$1; 1012 | 1013 | // Create a function bound to a given object (assigning `this`, and arguments, 1014 | // optionally). 1015 | var bind = restArguments(function(func, context, args) { 1016 | if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); 1017 | var bound = restArguments(function(callArgs) { 1018 | return executeBound(func, bound, context, this, args.concat(callArgs)); 1019 | }); 1020 | return bound; 1021 | }); 1022 | 1023 | // Internal helper for collection methods to determine whether a collection 1024 | // should be iterated as an array or as an object. 1025 | // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength 1026 | // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 1027 | var isArrayLike = createSizePropertyCheck(getLength); 1028 | 1029 | // Internal implementation of a recursive `flatten` function. 1030 | function flatten$1(input, depth, strict, output) { 1031 | output = output || []; 1032 | if (!depth && depth !== 0) { 1033 | depth = Infinity; 1034 | } else if (depth <= 0) { 1035 | return output.concat(input); 1036 | } 1037 | var idx = output.length; 1038 | for (var i = 0, length = getLength(input); i < length; i++) { 1039 | var value = input[i]; 1040 | if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { 1041 | // Flatten current level of array or arguments object. 1042 | if (depth > 1) { 1043 | flatten$1(value, depth - 1, strict, output); 1044 | idx = output.length; 1045 | } else { 1046 | var j = 0, len = value.length; 1047 | while (j < len) output[idx++] = value[j++]; 1048 | } 1049 | } else if (!strict) { 1050 | output[idx++] = value; 1051 | } 1052 | } 1053 | return output; 1054 | } 1055 | 1056 | // Bind a number of an object's methods to that object. Remaining arguments 1057 | // are the method names to be bound. Useful for ensuring that all callbacks 1058 | // defined on an object belong to it. 1059 | var bindAll = restArguments(function(obj, keys) { 1060 | keys = flatten$1(keys, false, false); 1061 | var index = keys.length; 1062 | if (index < 1) throw new Error('bindAll must be passed function names'); 1063 | while (index--) { 1064 | var key = keys[index]; 1065 | obj[key] = bind(obj[key], obj); 1066 | } 1067 | return obj; 1068 | }); 1069 | 1070 | // Memoize an expensive function by storing its results. 1071 | function memoize(func, hasher) { 1072 | var memoize = function(key) { 1073 | var cache = memoize.cache; 1074 | var address = '' + (hasher ? hasher.apply(this, arguments) : key); 1075 | if (!has$1(cache, address)) cache[address] = func.apply(this, arguments); 1076 | return cache[address]; 1077 | }; 1078 | memoize.cache = {}; 1079 | return memoize; 1080 | } 1081 | 1082 | // Delays a function for the given number of milliseconds, and then calls 1083 | // it with the arguments supplied. 1084 | var delay = restArguments(function(func, wait, args) { 1085 | return setTimeout(function() { 1086 | return func.apply(null, args); 1087 | }, wait); 1088 | }); 1089 | 1090 | // Defers a function, scheduling it to run after the current call stack has 1091 | // cleared. 1092 | var defer = partial(delay, _$1, 1); 1093 | 1094 | // Returns a function, that, when invoked, will only be triggered at most once 1095 | // during a given window of time. Normally, the throttled function will run 1096 | // as much as it can, without ever going more than once per `wait` duration; 1097 | // but if you'd like to disable the execution on the leading edge, pass 1098 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 1099 | function throttle(func, wait, options) { 1100 | var timeout, context, args, result; 1101 | var previous = 0; 1102 | if (!options) options = {}; 1103 | 1104 | var later = function() { 1105 | previous = options.leading === false ? 0 : now(); 1106 | timeout = null; 1107 | result = func.apply(context, args); 1108 | if (!timeout) context = args = null; 1109 | }; 1110 | 1111 | var throttled = function() { 1112 | var _now = now(); 1113 | if (!previous && options.leading === false) previous = _now; 1114 | var remaining = wait - (_now - previous); 1115 | context = this; 1116 | args = arguments; 1117 | if (remaining <= 0 || remaining > wait) { 1118 | if (timeout) { 1119 | clearTimeout(timeout); 1120 | timeout = null; 1121 | } 1122 | previous = _now; 1123 | result = func.apply(context, args); 1124 | if (!timeout) context = args = null; 1125 | } else if (!timeout && options.trailing !== false) { 1126 | timeout = setTimeout(later, remaining); 1127 | } 1128 | return result; 1129 | }; 1130 | 1131 | throttled.cancel = function() { 1132 | clearTimeout(timeout); 1133 | previous = 0; 1134 | timeout = context = args = null; 1135 | }; 1136 | 1137 | return throttled; 1138 | } 1139 | 1140 | // When a sequence of calls of the returned function ends, the argument 1141 | // function is triggered. The end of a sequence is defined by the `wait` 1142 | // parameter. If `immediate` is passed, the argument function will be 1143 | // triggered at the beginning of the sequence instead of at the end. 1144 | function debounce(func, wait, immediate) { 1145 | var timeout, previous, args, result, context; 1146 | 1147 | var later = function() { 1148 | var passed = now() - previous; 1149 | if (wait > passed) { 1150 | timeout = setTimeout(later, wait - passed); 1151 | } else { 1152 | timeout = null; 1153 | if (!immediate) result = func.apply(context, args); 1154 | // This check is needed because `func` can recursively invoke `debounced`. 1155 | if (!timeout) args = context = null; 1156 | } 1157 | }; 1158 | 1159 | var debounced = restArguments(function(_args) { 1160 | context = this; 1161 | args = _args; 1162 | previous = now(); 1163 | if (!timeout) { 1164 | timeout = setTimeout(later, wait); 1165 | if (immediate) result = func.apply(context, args); 1166 | } 1167 | return result; 1168 | }); 1169 | 1170 | debounced.cancel = function() { 1171 | clearTimeout(timeout); 1172 | timeout = args = context = null; 1173 | }; 1174 | 1175 | return debounced; 1176 | } 1177 | 1178 | // Returns the first function passed as an argument to the second, 1179 | // allowing you to adjust arguments, run code before and after, and 1180 | // conditionally execute the original function. 1181 | function wrap(func, wrapper) { 1182 | return partial(wrapper, func); 1183 | } 1184 | 1185 | // Returns a negated version of the passed-in predicate. 1186 | function negate(predicate) { 1187 | return function() { 1188 | return !predicate.apply(this, arguments); 1189 | }; 1190 | } 1191 | 1192 | // Returns a function that is the composition of a list of functions, each 1193 | // consuming the return value of the function that follows. 1194 | function compose() { 1195 | var args = arguments; 1196 | var start = args.length - 1; 1197 | return function() { 1198 | var i = start; 1199 | var result = args[start].apply(this, arguments); 1200 | while (i--) result = args[i].call(this, result); 1201 | return result; 1202 | }; 1203 | } 1204 | 1205 | // Returns a function that will only be executed on and after the Nth call. 1206 | function after(times, func) { 1207 | return function() { 1208 | if (--times < 1) { 1209 | return func.apply(this, arguments); 1210 | } 1211 | }; 1212 | } 1213 | 1214 | // Returns a function that will only be executed up to (but not including) the 1215 | // Nth call. 1216 | function before(times, func) { 1217 | var memo; 1218 | return function() { 1219 | if (--times > 0) { 1220 | memo = func.apply(this, arguments); 1221 | } 1222 | if (times <= 1) func = null; 1223 | return memo; 1224 | }; 1225 | } 1226 | 1227 | // Returns a function that will be executed at most one time, no matter how 1228 | // often you call it. Useful for lazy initialization. 1229 | var once = partial(before, 2); 1230 | 1231 | // Returns the first key on an object that passes a truth test. 1232 | function findKey(obj, predicate, context) { 1233 | predicate = cb(predicate, context); 1234 | var _keys = keys(obj), key; 1235 | for (var i = 0, length = _keys.length; i < length; i++) { 1236 | key = _keys[i]; 1237 | if (predicate(obj[key], key, obj)) return key; 1238 | } 1239 | } 1240 | 1241 | // Internal function to generate `_.findIndex` and `_.findLastIndex`. 1242 | function createPredicateIndexFinder(dir) { 1243 | return function(array, predicate, context) { 1244 | predicate = cb(predicate, context); 1245 | var length = getLength(array); 1246 | var index = dir > 0 ? 0 : length - 1; 1247 | for (; index >= 0 && index < length; index += dir) { 1248 | if (predicate(array[index], index, array)) return index; 1249 | } 1250 | return -1; 1251 | }; 1252 | } 1253 | 1254 | // Returns the first index on an array-like that passes a truth test. 1255 | var findIndex = createPredicateIndexFinder(1); 1256 | 1257 | // Returns the last index on an array-like that passes a truth test. 1258 | var findLastIndex = createPredicateIndexFinder(-1); 1259 | 1260 | // Use a comparator function to figure out the smallest index at which 1261 | // an object should be inserted so as to maintain order. Uses binary search. 1262 | function sortedIndex(array, obj, iteratee, context) { 1263 | iteratee = cb(iteratee, context, 1); 1264 | var value = iteratee(obj); 1265 | var low = 0, high = getLength(array); 1266 | while (low < high) { 1267 | var mid = Math.floor((low + high) / 2); 1268 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; 1269 | } 1270 | return low; 1271 | } 1272 | 1273 | // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. 1274 | function createIndexFinder(dir, predicateFind, sortedIndex) { 1275 | return function(array, item, idx) { 1276 | var i = 0, length = getLength(array); 1277 | if (typeof idx == 'number') { 1278 | if (dir > 0) { 1279 | i = idx >= 0 ? idx : Math.max(idx + length, i); 1280 | } else { 1281 | length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; 1282 | } 1283 | } else if (sortedIndex && idx && length) { 1284 | idx = sortedIndex(array, item); 1285 | return array[idx] === item ? idx : -1; 1286 | } 1287 | if (item !== item) { 1288 | idx = predicateFind(slice.call(array, i, length), isNaN$1); 1289 | return idx >= 0 ? idx + i : -1; 1290 | } 1291 | for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { 1292 | if (array[idx] === item) return idx; 1293 | } 1294 | return -1; 1295 | }; 1296 | } 1297 | 1298 | // Return the position of the first occurrence of an item in an array, 1299 | // or -1 if the item is not included in the array. 1300 | // If the array is large and already in sort order, pass `true` 1301 | // for **isSorted** to use binary search. 1302 | var indexOf = createIndexFinder(1, findIndex, sortedIndex); 1303 | 1304 | // Return the position of the last occurrence of an item in an array, 1305 | // or -1 if the item is not included in the array. 1306 | var lastIndexOf = createIndexFinder(-1, findLastIndex); 1307 | 1308 | // Return the first value which passes a truth test. 1309 | function find(obj, predicate, context) { 1310 | var keyFinder = isArrayLike(obj) ? findIndex : findKey; 1311 | var key = keyFinder(obj, predicate, context); 1312 | if (key !== void 0 && key !== -1) return obj[key]; 1313 | } 1314 | 1315 | // Convenience version of a common use case of `_.find`: getting the first 1316 | // object containing specific `key:value` pairs. 1317 | function findWhere(obj, attrs) { 1318 | return find(obj, matcher(attrs)); 1319 | } 1320 | 1321 | // The cornerstone for collection functions, an `each` 1322 | // implementation, aka `forEach`. 1323 | // Handles raw objects in addition to array-likes. Treats all 1324 | // sparse array-likes as if they were dense. 1325 | function each(obj, iteratee, context) { 1326 | iteratee = optimizeCb(iteratee, context); 1327 | var i, length; 1328 | if (isArrayLike(obj)) { 1329 | for (i = 0, length = obj.length; i < length; i++) { 1330 | iteratee(obj[i], i, obj); 1331 | } 1332 | } else { 1333 | var _keys = keys(obj); 1334 | for (i = 0, length = _keys.length; i < length; i++) { 1335 | iteratee(obj[_keys[i]], _keys[i], obj); 1336 | } 1337 | } 1338 | return obj; 1339 | } 1340 | 1341 | // Return the results of applying the iteratee to each element. 1342 | function map(obj, iteratee, context) { 1343 | iteratee = cb(iteratee, context); 1344 | var _keys = !isArrayLike(obj) && keys(obj), 1345 | length = (_keys || obj).length, 1346 | results = Array(length); 1347 | for (var index = 0; index < length; index++) { 1348 | var currentKey = _keys ? _keys[index] : index; 1349 | results[index] = iteratee(obj[currentKey], currentKey, obj); 1350 | } 1351 | return results; 1352 | } 1353 | 1354 | // Internal helper to create a reducing function, iterating left or right. 1355 | function createReduce(dir) { 1356 | // Wrap code that reassigns argument variables in a separate function than 1357 | // the one that accesses `arguments.length` to avoid a perf hit. (#1991) 1358 | var reducer = function(obj, iteratee, memo, initial) { 1359 | var _keys = !isArrayLike(obj) && keys(obj), 1360 | length = (_keys || obj).length, 1361 | index = dir > 0 ? 0 : length - 1; 1362 | if (!initial) { 1363 | memo = obj[_keys ? _keys[index] : index]; 1364 | index += dir; 1365 | } 1366 | for (; index >= 0 && index < length; index += dir) { 1367 | var currentKey = _keys ? _keys[index] : index; 1368 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 1369 | } 1370 | return memo; 1371 | }; 1372 | 1373 | return function(obj, iteratee, memo, context) { 1374 | var initial = arguments.length >= 3; 1375 | return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); 1376 | }; 1377 | } 1378 | 1379 | // **Reduce** builds up a single result from a list of values, aka `inject`, 1380 | // or `foldl`. 1381 | var reduce = createReduce(1); 1382 | 1383 | // The right-associative version of reduce, also known as `foldr`. 1384 | var reduceRight = createReduce(-1); 1385 | 1386 | // Return all the elements that pass a truth test. 1387 | function filter(obj, predicate, context) { 1388 | var results = []; 1389 | predicate = cb(predicate, context); 1390 | each(obj, function(value, index, list) { 1391 | if (predicate(value, index, list)) results.push(value); 1392 | }); 1393 | return results; 1394 | } 1395 | 1396 | // Return all the elements for which a truth test fails. 1397 | function reject(obj, predicate, context) { 1398 | return filter(obj, negate(cb(predicate)), context); 1399 | } 1400 | 1401 | // Determine whether all of the elements pass a truth test. 1402 | function every(obj, predicate, context) { 1403 | predicate = cb(predicate, context); 1404 | var _keys = !isArrayLike(obj) && keys(obj), 1405 | length = (_keys || obj).length; 1406 | for (var index = 0; index < length; index++) { 1407 | var currentKey = _keys ? _keys[index] : index; 1408 | if (!predicate(obj[currentKey], currentKey, obj)) return false; 1409 | } 1410 | return true; 1411 | } 1412 | 1413 | // Determine if at least one element in the object passes a truth test. 1414 | function some(obj, predicate, context) { 1415 | predicate = cb(predicate, context); 1416 | var _keys = !isArrayLike(obj) && keys(obj), 1417 | length = (_keys || obj).length; 1418 | for (var index = 0; index < length; index++) { 1419 | var currentKey = _keys ? _keys[index] : index; 1420 | if (predicate(obj[currentKey], currentKey, obj)) return true; 1421 | } 1422 | return false; 1423 | } 1424 | 1425 | // Determine if the array or object contains a given item (using `===`). 1426 | function contains(obj, item, fromIndex, guard) { 1427 | if (!isArrayLike(obj)) obj = values(obj); 1428 | if (typeof fromIndex != 'number' || guard) fromIndex = 0; 1429 | return indexOf(obj, item, fromIndex) >= 0; 1430 | } 1431 | 1432 | // Invoke a method (with arguments) on every item in a collection. 1433 | var invoke = restArguments(function(obj, path, args) { 1434 | var contextPath, func; 1435 | if (isFunction$1(path)) { 1436 | func = path; 1437 | } else { 1438 | path = toPath(path); 1439 | contextPath = path.slice(0, -1); 1440 | path = path[path.length - 1]; 1441 | } 1442 | return map(obj, function(context) { 1443 | var method = func; 1444 | if (!method) { 1445 | if (contextPath && contextPath.length) { 1446 | context = deepGet(context, contextPath); 1447 | } 1448 | if (context == null) return void 0; 1449 | method = context[path]; 1450 | } 1451 | return method == null ? method : method.apply(context, args); 1452 | }); 1453 | }); 1454 | 1455 | // Convenience version of a common use case of `_.map`: fetching a property. 1456 | function pluck(obj, key) { 1457 | return map(obj, property(key)); 1458 | } 1459 | 1460 | // Convenience version of a common use case of `_.filter`: selecting only 1461 | // objects containing specific `key:value` pairs. 1462 | function where(obj, attrs) { 1463 | return filter(obj, matcher(attrs)); 1464 | } 1465 | 1466 | // Return the maximum element (or element-based computation). 1467 | function max(obj, iteratee, context) { 1468 | var result = -Infinity, lastComputed = -Infinity, 1469 | value, computed; 1470 | if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { 1471 | obj = isArrayLike(obj) ? obj : values(obj); 1472 | for (var i = 0, length = obj.length; i < length; i++) { 1473 | value = obj[i]; 1474 | if (value != null && value > result) { 1475 | result = value; 1476 | } 1477 | } 1478 | } else { 1479 | iteratee = cb(iteratee, context); 1480 | each(obj, function(v, index, list) { 1481 | computed = iteratee(v, index, list); 1482 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) { 1483 | result = v; 1484 | lastComputed = computed; 1485 | } 1486 | }); 1487 | } 1488 | return result; 1489 | } 1490 | 1491 | // Return the minimum element (or element-based computation). 1492 | function min(obj, iteratee, context) { 1493 | var result = Infinity, lastComputed = Infinity, 1494 | value, computed; 1495 | if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { 1496 | obj = isArrayLike(obj) ? obj : values(obj); 1497 | for (var i = 0, length = obj.length; i < length; i++) { 1498 | value = obj[i]; 1499 | if (value != null && value < result) { 1500 | result = value; 1501 | } 1502 | } 1503 | } else { 1504 | iteratee = cb(iteratee, context); 1505 | each(obj, function(v, index, list) { 1506 | computed = iteratee(v, index, list); 1507 | if (computed < lastComputed || computed === Infinity && result === Infinity) { 1508 | result = v; 1509 | lastComputed = computed; 1510 | } 1511 | }); 1512 | } 1513 | return result; 1514 | } 1515 | 1516 | // Sample **n** random values from a collection using the modern version of the 1517 | // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 1518 | // If **n** is not specified, returns a single random element. 1519 | // The internal `guard` argument allows it to work with `_.map`. 1520 | function sample(obj, n, guard) { 1521 | if (n == null || guard) { 1522 | if (!isArrayLike(obj)) obj = values(obj); 1523 | return obj[random(obj.length - 1)]; 1524 | } 1525 | var sample = isArrayLike(obj) ? clone(obj) : values(obj); 1526 | var length = getLength(sample); 1527 | n = Math.max(Math.min(n, length), 0); 1528 | var last = length - 1; 1529 | for (var index = 0; index < n; index++) { 1530 | var rand = random(index, last); 1531 | var temp = sample[index]; 1532 | sample[index] = sample[rand]; 1533 | sample[rand] = temp; 1534 | } 1535 | return sample.slice(0, n); 1536 | } 1537 | 1538 | // Shuffle a collection. 1539 | function shuffle(obj) { 1540 | return sample(obj, Infinity); 1541 | } 1542 | 1543 | // Sort the object's values by a criterion produced by an iteratee. 1544 | function sortBy(obj, iteratee, context) { 1545 | var index = 0; 1546 | iteratee = cb(iteratee, context); 1547 | return pluck(map(obj, function(value, key, list) { 1548 | return { 1549 | value: value, 1550 | index: index++, 1551 | criteria: iteratee(value, key, list) 1552 | }; 1553 | }).sort(function(left, right) { 1554 | var a = left.criteria; 1555 | var b = right.criteria; 1556 | if (a !== b) { 1557 | if (a > b || a === void 0) return 1; 1558 | if (a < b || b === void 0) return -1; 1559 | } 1560 | return left.index - right.index; 1561 | }), 'value'); 1562 | } 1563 | 1564 | // An internal function used for aggregate "group by" operations. 1565 | function group(behavior, partition) { 1566 | return function(obj, iteratee, context) { 1567 | var result = partition ? [[], []] : {}; 1568 | iteratee = cb(iteratee, context); 1569 | each(obj, function(value, index) { 1570 | var key = iteratee(value, index, obj); 1571 | behavior(result, value, key); 1572 | }); 1573 | return result; 1574 | }; 1575 | } 1576 | 1577 | // Groups the object's values by a criterion. Pass either a string attribute 1578 | // to group by, or a function that returns the criterion. 1579 | var groupBy = group(function(result, value, key) { 1580 | if (has$1(result, key)) result[key].push(value); else result[key] = [value]; 1581 | }); 1582 | 1583 | // Indexes the object's values by a criterion, similar to `_.groupBy`, but for 1584 | // when you know that your index values will be unique. 1585 | var indexBy = group(function(result, value, key) { 1586 | result[key] = value; 1587 | }); 1588 | 1589 | // Counts instances of an object that group by a certain criterion. Pass 1590 | // either a string attribute to count by, or a function that returns the 1591 | // criterion. 1592 | var countBy = group(function(result, value, key) { 1593 | if (has$1(result, key)) result[key]++; else result[key] = 1; 1594 | }); 1595 | 1596 | // Split a collection into two arrays: one whose elements all pass the given 1597 | // truth test, and one whose elements all do not pass the truth test. 1598 | var partition = group(function(result, value, pass) { 1599 | result[pass ? 0 : 1].push(value); 1600 | }, true); 1601 | 1602 | // Safely create a real, live array from anything iterable. 1603 | var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; 1604 | function toArray(obj) { 1605 | if (!obj) return []; 1606 | if (isArray(obj)) return slice.call(obj); 1607 | if (isString(obj)) { 1608 | // Keep surrogate pair characters together. 1609 | return obj.match(reStrSymbol); 1610 | } 1611 | if (isArrayLike(obj)) return map(obj, identity); 1612 | return values(obj); 1613 | } 1614 | 1615 | // Return the number of elements in a collection. 1616 | function size(obj) { 1617 | if (obj == null) return 0; 1618 | return isArrayLike(obj) ? obj.length : keys(obj).length; 1619 | } 1620 | 1621 | // Internal `_.pick` helper function to determine whether `key` is an enumerable 1622 | // property name of `obj`. 1623 | function keyInObj(value, key, obj) { 1624 | return key in obj; 1625 | } 1626 | 1627 | // Return a copy of the object only containing the allowed properties. 1628 | var pick = restArguments(function(obj, keys) { 1629 | var result = {}, iteratee = keys[0]; 1630 | if (obj == null) return result; 1631 | if (isFunction$1(iteratee)) { 1632 | if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); 1633 | keys = allKeys(obj); 1634 | } else { 1635 | iteratee = keyInObj; 1636 | keys = flatten$1(keys, false, false); 1637 | obj = Object(obj); 1638 | } 1639 | for (var i = 0, length = keys.length; i < length; i++) { 1640 | var key = keys[i]; 1641 | var value = obj[key]; 1642 | if (iteratee(value, key, obj)) result[key] = value; 1643 | } 1644 | return result; 1645 | }); 1646 | 1647 | // Return a copy of the object without the disallowed properties. 1648 | var omit = restArguments(function(obj, keys) { 1649 | var iteratee = keys[0], context; 1650 | if (isFunction$1(iteratee)) { 1651 | iteratee = negate(iteratee); 1652 | if (keys.length > 1) context = keys[1]; 1653 | } else { 1654 | keys = map(flatten$1(keys, false, false), String); 1655 | iteratee = function(value, key) { 1656 | return !contains(keys, key); 1657 | }; 1658 | } 1659 | return pick(obj, iteratee, context); 1660 | }); 1661 | 1662 | // Returns everything but the last entry of the array. Especially useful on 1663 | // the arguments object. Passing **n** will return all the values in 1664 | // the array, excluding the last N. 1665 | function initial(array, n, guard) { 1666 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); 1667 | } 1668 | 1669 | // Get the first element of an array. Passing **n** will return the first N 1670 | // values in the array. The **guard** check allows it to work with `_.map`. 1671 | function first(array, n, guard) { 1672 | if (array == null || array.length < 1) return n == null || guard ? void 0 : []; 1673 | if (n == null || guard) return array[0]; 1674 | return initial(array, array.length - n); 1675 | } 1676 | 1677 | // Returns everything but the first entry of the `array`. Especially useful on 1678 | // the `arguments` object. Passing an **n** will return the rest N values in the 1679 | // `array`. 1680 | function rest(array, n, guard) { 1681 | return slice.call(array, n == null || guard ? 1 : n); 1682 | } 1683 | 1684 | // Get the last element of an array. Passing **n** will return the last N 1685 | // values in the array. 1686 | function last(array, n, guard) { 1687 | if (array == null || array.length < 1) return n == null || guard ? void 0 : []; 1688 | if (n == null || guard) return array[array.length - 1]; 1689 | return rest(array, Math.max(0, array.length - n)); 1690 | } 1691 | 1692 | // Trim out all falsy values from an array. 1693 | function compact(array) { 1694 | return filter(array, Boolean); 1695 | } 1696 | 1697 | // Flatten out an array, either recursively (by default), or up to `depth`. 1698 | // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. 1699 | function flatten(array, depth) { 1700 | return flatten$1(array, depth, false); 1701 | } 1702 | 1703 | // Take the difference between one array and a number of other arrays. 1704 | // Only the elements present in just the first array will remain. 1705 | var difference = restArguments(function(array, rest) { 1706 | rest = flatten$1(rest, true, true); 1707 | return filter(array, function(value){ 1708 | return !contains(rest, value); 1709 | }); 1710 | }); 1711 | 1712 | // Return a version of the array that does not contain the specified value(s). 1713 | var without = restArguments(function(array, otherArrays) { 1714 | return difference(array, otherArrays); 1715 | }); 1716 | 1717 | // Produce a duplicate-free version of the array. If the array has already 1718 | // been sorted, you have the option of using a faster algorithm. 1719 | // The faster algorithm will not work with an iteratee if the iteratee 1720 | // is not a one-to-one function, so providing an iteratee will disable 1721 | // the faster algorithm. 1722 | function uniq(array, isSorted, iteratee, context) { 1723 | if (!isBoolean(isSorted)) { 1724 | context = iteratee; 1725 | iteratee = isSorted; 1726 | isSorted = false; 1727 | } 1728 | if (iteratee != null) iteratee = cb(iteratee, context); 1729 | var result = []; 1730 | var seen = []; 1731 | for (var i = 0, length = getLength(array); i < length; i++) { 1732 | var value = array[i], 1733 | computed = iteratee ? iteratee(value, i, array) : value; 1734 | if (isSorted && !iteratee) { 1735 | if (!i || seen !== computed) result.push(value); 1736 | seen = computed; 1737 | } else if (iteratee) { 1738 | if (!contains(seen, computed)) { 1739 | seen.push(computed); 1740 | result.push(value); 1741 | } 1742 | } else if (!contains(result, value)) { 1743 | result.push(value); 1744 | } 1745 | } 1746 | return result; 1747 | } 1748 | 1749 | // Produce an array that contains the union: each distinct element from all of 1750 | // the passed-in arrays. 1751 | var union = restArguments(function(arrays) { 1752 | return uniq(flatten$1(arrays, true, true)); 1753 | }); 1754 | 1755 | // Produce an array that contains every item shared between all the 1756 | // passed-in arrays. 1757 | function intersection(array) { 1758 | var result = []; 1759 | var argsLength = arguments.length; 1760 | for (var i = 0, length = getLength(array); i < length; i++) { 1761 | var item = array[i]; 1762 | if (contains(result, item)) continue; 1763 | var j; 1764 | for (j = 1; j < argsLength; j++) { 1765 | if (!contains(arguments[j], item)) break; 1766 | } 1767 | if (j === argsLength) result.push(item); 1768 | } 1769 | return result; 1770 | } 1771 | 1772 | // Complement of zip. Unzip accepts an array of arrays and groups 1773 | // each array's elements on shared indices. 1774 | function unzip(array) { 1775 | var length = array && max(array, getLength).length || 0; 1776 | var result = Array(length); 1777 | 1778 | for (var index = 0; index < length; index++) { 1779 | result[index] = pluck(array, index); 1780 | } 1781 | return result; 1782 | } 1783 | 1784 | // Zip together multiple lists into a single array -- elements that share 1785 | // an index go together. 1786 | var zip = restArguments(unzip); 1787 | 1788 | // Converts lists into objects. Pass either a single array of `[key, value]` 1789 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 1790 | // the corresponding values. Passing by pairs is the reverse of `_.pairs`. 1791 | function object(list, values) { 1792 | var result = {}; 1793 | for (var i = 0, length = getLength(list); i < length; i++) { 1794 | if (values) { 1795 | result[list[i]] = values[i]; 1796 | } else { 1797 | result[list[i][0]] = list[i][1]; 1798 | } 1799 | } 1800 | return result; 1801 | } 1802 | 1803 | // Generate an integer Array containing an arithmetic progression. A port of 1804 | // the native Python `range()` function. See 1805 | // [the Python documentation](https://docs.python.org/library/functions.html#range). 1806 | function range(start, stop, step) { 1807 | if (stop == null) { 1808 | stop = start || 0; 1809 | start = 0; 1810 | } 1811 | if (!step) { 1812 | step = stop < start ? -1 : 1; 1813 | } 1814 | 1815 | var length = Math.max(Math.ceil((stop - start) / step), 0); 1816 | var range = Array(length); 1817 | 1818 | for (var idx = 0; idx < length; idx++, start += step) { 1819 | range[idx] = start; 1820 | } 1821 | 1822 | return range; 1823 | } 1824 | 1825 | // Chunk a single array into multiple arrays, each containing `count` or fewer 1826 | // items. 1827 | function chunk(array, count) { 1828 | if (count == null || count < 1) return []; 1829 | var result = []; 1830 | var i = 0, length = array.length; 1831 | while (i < length) { 1832 | result.push(slice.call(array, i, i += count)); 1833 | } 1834 | return result; 1835 | } 1836 | 1837 | // Helper function to continue chaining intermediate results. 1838 | function chainResult(instance, obj) { 1839 | return instance._chain ? _$1(obj).chain() : obj; 1840 | } 1841 | 1842 | // Add your own custom functions to the Underscore object. 1843 | function mixin(obj) { 1844 | each(functions(obj), function(name) { 1845 | var func = _$1[name] = obj[name]; 1846 | _$1.prototype[name] = function() { 1847 | var args = [this._wrapped]; 1848 | push.apply(args, arguments); 1849 | return chainResult(this, func.apply(_$1, args)); 1850 | }; 1851 | }); 1852 | return _$1; 1853 | } 1854 | 1855 | // Add all mutator `Array` functions to the wrapper. 1856 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1857 | var method = ArrayProto[name]; 1858 | _$1.prototype[name] = function() { 1859 | var obj = this._wrapped; 1860 | if (obj != null) { 1861 | method.apply(obj, arguments); 1862 | if ((name === 'shift' || name === 'splice') && obj.length === 0) { 1863 | delete obj[0]; 1864 | } 1865 | } 1866 | return chainResult(this, obj); 1867 | }; 1868 | }); 1869 | 1870 | // Add all accessor `Array` functions to the wrapper. 1871 | each(['concat', 'join', 'slice'], function(name) { 1872 | var method = ArrayProto[name]; 1873 | _$1.prototype[name] = function() { 1874 | var obj = this._wrapped; 1875 | if (obj != null) obj = method.apply(obj, arguments); 1876 | return chainResult(this, obj); 1877 | }; 1878 | }); 1879 | 1880 | // Named Exports 1881 | 1882 | var allExports = { 1883 | __proto__: null, 1884 | VERSION: VERSION, 1885 | restArguments: restArguments, 1886 | isObject: isObject, 1887 | isNull: isNull, 1888 | isUndefined: isUndefined, 1889 | isBoolean: isBoolean, 1890 | isElement: isElement, 1891 | isString: isString, 1892 | isNumber: isNumber, 1893 | isDate: isDate, 1894 | isRegExp: isRegExp, 1895 | isError: isError, 1896 | isSymbol: isSymbol, 1897 | isArrayBuffer: isArrayBuffer, 1898 | isDataView: isDataView$1, 1899 | isArray: isArray, 1900 | isFunction: isFunction$1, 1901 | isArguments: isArguments$1, 1902 | isFinite: isFinite$1, 1903 | isNaN: isNaN$1, 1904 | isTypedArray: isTypedArray$1, 1905 | isEmpty: isEmpty, 1906 | isMatch: isMatch, 1907 | isEqual: isEqual, 1908 | isMap: isMap, 1909 | isWeakMap: isWeakMap, 1910 | isSet: isSet, 1911 | isWeakSet: isWeakSet, 1912 | keys: keys, 1913 | allKeys: allKeys, 1914 | values: values, 1915 | pairs: pairs, 1916 | invert: invert, 1917 | functions: functions, 1918 | methods: functions, 1919 | extend: extend, 1920 | extendOwn: extendOwn, 1921 | assign: extendOwn, 1922 | defaults: defaults, 1923 | create: create, 1924 | clone: clone, 1925 | tap: tap, 1926 | get: get, 1927 | has: has, 1928 | mapObject: mapObject, 1929 | identity: identity, 1930 | constant: constant, 1931 | noop: noop, 1932 | toPath: toPath$1, 1933 | property: property, 1934 | propertyOf: propertyOf, 1935 | matcher: matcher, 1936 | matches: matcher, 1937 | times: times, 1938 | random: random, 1939 | now: now, 1940 | escape: _escape, 1941 | unescape: _unescape, 1942 | templateSettings: templateSettings, 1943 | template: template, 1944 | result: result, 1945 | uniqueId: uniqueId, 1946 | chain: chain, 1947 | iteratee: iteratee, 1948 | partial: partial, 1949 | bind: bind, 1950 | bindAll: bindAll, 1951 | memoize: memoize, 1952 | delay: delay, 1953 | defer: defer, 1954 | throttle: throttle, 1955 | debounce: debounce, 1956 | wrap: wrap, 1957 | negate: negate, 1958 | compose: compose, 1959 | after: after, 1960 | before: before, 1961 | once: once, 1962 | findKey: findKey, 1963 | findIndex: findIndex, 1964 | findLastIndex: findLastIndex, 1965 | sortedIndex: sortedIndex, 1966 | indexOf: indexOf, 1967 | lastIndexOf: lastIndexOf, 1968 | find: find, 1969 | detect: find, 1970 | findWhere: findWhere, 1971 | each: each, 1972 | forEach: each, 1973 | map: map, 1974 | collect: map, 1975 | reduce: reduce, 1976 | foldl: reduce, 1977 | inject: reduce, 1978 | reduceRight: reduceRight, 1979 | foldr: reduceRight, 1980 | filter: filter, 1981 | select: filter, 1982 | reject: reject, 1983 | every: every, 1984 | all: every, 1985 | some: some, 1986 | any: some, 1987 | contains: contains, 1988 | includes: contains, 1989 | include: contains, 1990 | invoke: invoke, 1991 | pluck: pluck, 1992 | where: where, 1993 | max: max, 1994 | min: min, 1995 | shuffle: shuffle, 1996 | sample: sample, 1997 | sortBy: sortBy, 1998 | groupBy: groupBy, 1999 | indexBy: indexBy, 2000 | countBy: countBy, 2001 | partition: partition, 2002 | toArray: toArray, 2003 | size: size, 2004 | pick: pick, 2005 | omit: omit, 2006 | first: first, 2007 | head: first, 2008 | take: first, 2009 | initial: initial, 2010 | last: last, 2011 | rest: rest, 2012 | tail: rest, 2013 | drop: rest, 2014 | compact: compact, 2015 | flatten: flatten, 2016 | without: without, 2017 | uniq: uniq, 2018 | unique: uniq, 2019 | union: union, 2020 | intersection: intersection, 2021 | difference: difference, 2022 | unzip: unzip, 2023 | transpose: unzip, 2024 | zip: zip, 2025 | object: object, 2026 | range: range, 2027 | chunk: chunk, 2028 | mixin: mixin, 2029 | 'default': _$1 2030 | }; 2031 | 2032 | // Default Export 2033 | 2034 | // Add all of the Underscore functions to the wrapper object. 2035 | var _ = mixin(allExports); 2036 | // Legacy Node.js API. 2037 | _._ = _; 2038 | 2039 | return _; 2040 | 2041 | }))); 2042 | //# sourceMappingURL=underscore-umd.js.map 2043 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sticky-ducky", 3 | "dependencies": { 4 | "css-what": "^5.0.1", 5 | "underscore": "1.13.1" 6 | }, 7 | "devDependencies": { 8 | "browserify": "^16.5.2", 9 | "prettier": "^2.1.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /platform/chromium/js/vapiBackground.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | let vAPI = self.vAPI = self.vAPI || {}; 4 | let listeners = {}; 5 | let getListeners = name => listeners[name] || (listeners[name] = []); 6 | 7 | vAPI.getSettings = (keys, callback) => chrome.storage.local.get(keys, callback); 8 | vAPI.updateSettings = settings => chrome.storage.local.set(settings); 9 | vAPI.onPopupOpen = callback => callback(); 10 | vAPI.sendSettings = () => null; // Client gets the message from the Storage onChange 11 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 12 | vAPI.sendToBackground = (name, message) => chrome.runtime.sendMessage({name: name, message: message}); 13 | vAPI.sendToTabs = (tabs, name, message) => { 14 | tabs.forEach(tab => { 15 | try { 16 | chrome.tabs.sendMessage(tab.id, {name: name, message: message}); 17 | } catch (e) { 18 | console.error(e); 19 | } 20 | }); 21 | }; 22 | vAPI.getCurrentTabs = () => new Promise ((resolve) => { 23 | chrome.tabs.query({currentWindow: true, active: true}, resolve) 24 | }); 25 | 26 | chrome.runtime.onMessage.addListener((request, sender) => { 27 | // The Chromium sendResponse calls the sendMessage callback instead of sending a message 28 | let sendResponse = (name, message) => { 29 | if (sender.tab && sender.tab.id) { 30 | chrome.tabs.sendMessage(sender.tab.id, {name: name, message: message}); 31 | } else { 32 | // Respond to popup 33 | chrome.runtime.sendMessage({name: name, message: message}); 34 | } 35 | }; 36 | getListeners(request.name).map(handler => handler(request.message, sendResponse)); 37 | }); 38 | })(this); 39 | -------------------------------------------------------------------------------- /platform/chromium/js/vapiInjected.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | "use strict"; 3 | let vAPI = (self.vAPI = self.vAPI || {}); 4 | let listeners = {}; 5 | let isRegistered = false; 6 | 7 | let getListeners = name => listeners[name] || (listeners[name] = []); 8 | 9 | vAPI.sendToBackground = (name, message) => 10 | chrome.runtime.sendMessage({ name: name, message: message }); 11 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 12 | 13 | chrome.runtime.onMessage.addListener(request => { 14 | getListeners(request.name).map(handler => handler(request.message)); 15 | }); 16 | chrome.storage.onChanged.addListener(changes => { 17 | // Retrieve settings again 18 | let getSettings = () => { 19 | vAPI.sendToBackground('getSettings', {location: _.omit(window.location, _.isFunction)}); 20 | isRegistered = false; 21 | }; 22 | if (!document.hidden) { 23 | getSettings(); 24 | } else if (!isRegistered) { 25 | isRegistered = true; 26 | document.addEventListener("visibilitychange", getSettings, {once: true}); 27 | } 28 | }); 29 | })(this); 30 | -------------------------------------------------------------------------------- /platform/chromium/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Sticky Ducky", 4 | "author": "Boris Lykah", 5 | "description": "Automatically cleans pages of the sticky headers and other fixed elements", 6 | "version": "1.7.2", 7 | "minimum_chrome_version": "51", 8 | "permissions": [ 9 | "tabs", 10 | "storage", 11 | "" 12 | ], 13 | "browser_action": { 14 | "default_popup": "popup.html", 15 | "default_icon": "assets/icon48.png" 16 | }, 17 | "icons": { 18 | "48": "assets/icon48.png", 19 | "128": "assets/icon128.png" 20 | }, 21 | "background": { 22 | "scripts": [ 23 | "lib/underscore.js", 24 | "lib/css-what.js", 25 | "js/vapiBackground.js", 26 | "js/explorer.js", 27 | "js/whitelist.js", 28 | "js/background.js" 29 | ] 30 | }, 31 | "content_scripts": [ 32 | { 33 | "matches": [ 34 | "http://*/*", 35 | "https://*/*" 36 | ], 37 | "js": [ 38 | "lib/underscore.js", 39 | "lib/css-what.js", 40 | "js/vapiInjected.js", 41 | "js/explorer.js", 42 | "js/content.js" 43 | ], 44 | "run_at": "document_start" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /platform/firefox/js/vapiBackground.js: -------------------------------------------------------------------------------- 1 | (function (self) { 2 | 'use strict'; 3 | let vAPI = self.vAPI = self.vAPI || {}; 4 | let listeners = {}; 5 | let getListeners = name => listeners[name] || (listeners[name] = []); 6 | 7 | vAPI.getSettings = (keys, callback) => browser.storage.local.get(keys, callback); 8 | vAPI.updateSettings = settings => browser.storage.local.set(settings); 9 | vAPI.onPopupOpen = callback => callback(); 10 | vAPI.sendSettings = () => null; // Client gets the message from the Storage onChange 11 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 12 | vAPI.sendToBackground = (name, message) => browser.runtime.sendMessage({name: name, message: message}); 13 | vAPI.sendToTabs = (tabs, name, message) => { 14 | tabs.forEach(tab => { 15 | try { 16 | browser.tabs.sendMessage(tab.id, {name: name, message: message}); 17 | } catch (e) { 18 | console.error(e); 19 | } 20 | }); 21 | }; 22 | vAPI.getCurrentTabs = () => browser.tabs.query({currentWindow: true, active: true}); 23 | 24 | browser.runtime.onMessage.addListener((request, sender) => { 25 | let sendResponse = (name, message) => { 26 | if (sender.tab && sender.tab.id) { 27 | browser.tabs.sendMessage(sender.tab.id, {name: name, message: message}); 28 | } else { 29 | // Respond to popup 30 | browser.runtime.sendMessage({name: name, message: message}); 31 | } 32 | }; 33 | getListeners(request.name).map(handler => handler(request.message, sendResponse)); 34 | }); 35 | })(this); 36 | -------------------------------------------------------------------------------- /platform/firefox/js/vapiInjected.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | let vAPI = self.vAPI = self.vAPI || {}; 4 | let listeners = {}; 5 | let getListeners = name => listeners[name] || (listeners[name] = []); 6 | let isRegistered = false; 7 | 8 | vAPI.sendToBackground = (name, message) => browser.runtime.sendMessage({name: name, message: message}); 9 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 10 | 11 | browser.runtime.onMessage.addListener(request => { 12 | getListeners(request.name).map(handler => handler(request.message)); 13 | }); 14 | chrome.storage.onChanged.addListener(changes => { 15 | // Retrieve settings again 16 | let getSettings = () => { 17 | vAPI.sendToBackground('getSettings', {location: _.omit(window.location, _.isFunction)}); 18 | isRegistered = false; 19 | }; 20 | if (!document.hidden) { 21 | getSettings(); 22 | } else if (!isRegistered) { 23 | isRegistered = true; 24 | document.addEventListener("visibilitychange", getSettings, {once: true}); 25 | } 26 | }); 27 | })(this); 28 | -------------------------------------------------------------------------------- /platform/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Sticky Ducky", 4 | "author": "Boris Lykah", 5 | "description": "Automatically cleans pages of the sticky headers and other fixed elements", 6 | "version": "1.7.2", 7 | "applications": { 8 | "gecko": { 9 | "id": "sticky-ducky@addons.mozilla.org", 10 | "strict_min_version": "52.0" 11 | } 12 | }, 13 | "permissions": [ 14 | "tabs", 15 | "storage", 16 | "" 17 | ], 18 | "browser_action": { 19 | "default_popup": "popup.html", 20 | "default_icon": "assets/icon48.png" 21 | }, 22 | "icons": { 23 | "48": "assets/icon48.png", 24 | "128": "assets/icon128.png" 25 | }, 26 | "background": { 27 | "scripts": [ 28 | "lib/underscore.js", 29 | "lib/css-what.js", 30 | "js/vapiBackground.js", 31 | "js/explorer.js", 32 | "js/whitelist.js", 33 | "js/background.js" 34 | ] 35 | }, 36 | "content_scripts": [ 37 | { 38 | "matches": [ 39 | "http://*/*", 40 | "https://*/*" 41 | ], 42 | "js": [ 43 | "lib/underscore.js", 44 | "lib/css-what.js", 45 | "js/vapiInjected.js", 46 | "js/explorer.js", 47 | "js/content.js" 48 | ], 49 | "run_at": "document_start" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /platform/safari/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Boris Lykah 7 | Builder Version 8 | 13605.1.33.1.2 9 | CFBundleDisplayName 10 | StickyDucky 11 | CFBundleIdentifier 12 | com.stickyducky 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 1.6 17 | CFBundleVersion 18 | 1.6 19 | Chrome 20 | 21 | Database Quota 22 | 0 23 | Global Page 24 | background.html 25 | Popovers 26 | 27 | 28 | Filename 29 | popup.html 30 | Height 31 | 400 32 | Identifier 33 | popup 34 | Width 35 | 224 36 | 37 | 38 | Toolbar Items 39 | 40 | 41 | Identifier 42 | stickyducky 43 | Image 44 | assets/iconContour64.png 45 | Include By Default 46 | 47 | Label 48 | Sticky Ducky 49 | Popover 50 | popup 51 | 52 | 53 | 54 | Content 55 | 56 | Scripts 57 | 58 | Start 59 | 60 | lib/underscore-1.13.1.js 61 | lib/css-what-5.0.1.js 62 | vapiInjected.js 63 | explorer.js 64 | content.js 65 | 66 | 67 | 68 | Description 69 | Automatically cleans pages of the sticky headers and other fixed elements 70 | DeveloperIdentifier 71 | 0000000000 72 | ExtensionInfoDictionaryVersion 73 | 1.0 74 | Permissions 75 | 76 | Website Access 77 | 78 | Include Secure Pages 79 | 80 | Level 81 | All 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /platform/safari/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Key 7 | behavior 8 | Title 9 | When to show sticky elements 10 | Titles 11 | 12 | When hovering over 13 | After scrolling up 14 | On top of the page 15 | Always (disable extension) 16 | 17 | Type 18 | ListBox 19 | Values 20 | 21 | hover 22 | scroll 23 | top 24 | always 25 | 26 | 27 | 28 | Key 29 | isDevelopment 30 | 31 | 32 | Key 33 | whitelist 34 | Password 35 | 36 | Title 37 | Whitelist 38 | Type 39 | TextField 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /platform/safari/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sticky Ducky 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /platform/safari/js/vapiBackground.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | let vAPI = self.vAPI = self.vAPI || {}; 4 | let listeners = {}; 5 | let getListeners = name => listeners[name] || (listeners[name] = []); 6 | 7 | vAPI.getSettings = (keys, callback) => callback(_.pick(safari.extension.settings, keys)); 8 | vAPI.updateSettings = settings => _.extend(safari.extension.settings, settings); 9 | vAPI.onPopupOpen = callback => safari.application.addEventListener("popover", callback, true); 10 | vAPI.sendSettings = function(message) { 11 | safari.application.browserWindows.map(window => window.tabs.map(tab => { 12 | tab.page.dispatchMessage('settingsChanged', message); 13 | })); 14 | }; 15 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 16 | vAPI.sendToBackground = function(name, message) { 17 | let gw = safari.extension.globalPage.contentWindow; 18 | gw.postMessage({name: name, message: message}, window.location.origin); 19 | }; 20 | 21 | safari.application.addEventListener('message', e => { 22 | let sendResponse = (name, message) => e.target.page.dispatchMessage(name, message); 23 | getListeners(e.name).map(handler => handler(e.message, sendResponse)); 24 | }, false); 25 | 26 | // For communication with popover 27 | window.addEventListener('message', function (msg) { 28 | if (msg.origin === window.location.origin) { 29 | let sendResponse = (name, message) => 30 | msg.source.postMessage({name: name, message: message}, window.location.origin); 31 | getListeners(msg.data.name).map(handler => handler(msg.data.message, sendResponse)); 32 | } 33 | }, false); 34 | })(this); 35 | -------------------------------------------------------------------------------- /platform/safari/js/vapiInjected.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | let vAPI = self.vAPI = self.vAPI || {}; 4 | let listeners = {}; 5 | let getListeners = name => listeners[name] || (listeners[name] = []); 6 | 7 | vAPI.sendToBackground = (name, message) => safari.self.tab.dispatchMessage(name, message); 8 | vAPI.listen = (name, listener) => getListeners(name).push(listener); 9 | 10 | safari.self.addEventListener("message", msg => 11 | getListeners(msg.name).map(handler => handler(msg.message)), 12 | false); 13 | })(this); 14 | -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let settings = {}; 3 | 4 | // An optimization to cache the parsed rules. 5 | vAPI.getSettings(['whitelist', 'behavior', 'isDevelopment'], settingsResponse => { 6 | settings = settingsResponse; 7 | 8 | settings.parsedWhitelist = []; 9 | if (settings.whitelist) { 10 | try { 11 | settings.parsedWhitelist = parseRules(settings.whitelist); 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | } 16 | if (!settings.behavior) { 17 | // Assume that devices without touch have a mouse 18 | const hasTouch = 'ontouchstart' in window; 19 | settings.behavior = hasTouch ? 'scroll' : 'hover'; 20 | vAPI.updateSettings({behavior: settings.behavior}); 21 | } 22 | 23 | vAPI.listen('getSettings', (message, sendResponse) => { 24 | let response = _.pick(settings, 'behavior', 'isDevelopment'); 25 | if (settings.parsedWhitelist) { 26 | response.whitelist = matchWhitelist(settings.parsedWhitelist, message.location); 27 | } 28 | sendResponse('settings', response); 29 | }); 30 | }); 31 | 32 | vAPI.listen('updateSettings', (message, sendResponse) => { 33 | // Apply settings to the settings object 34 | if (message.whitelist !== undefined) { 35 | try { 36 | settings.parsedWhitelist = parseRules(message.whitelist); 37 | settings.whitelist = message.whitelist; 38 | } catch (e) { 39 | // TODO: replace with promise 40 | sendResponse('invalidSettings', e.message); 41 | return; 42 | } 43 | } 44 | if (message.behavior) { 45 | settings.behavior = message.behavior; 46 | } 47 | 48 | vAPI.updateSettings(message); 49 | 50 | // Update all tabs only if the behavior changed. 51 | if (message.behavior) { 52 | vAPI.sendSettings({behavior: message.behavior}); 53 | } 54 | // TODO: replace with promise 55 | sendResponse('acceptedSettings'); 56 | }); 57 | vAPI.listen('exploreSheet', (message, sendResponse) => { 58 | let explorer = new Explorer(result => { 59 | sendResponse('sheetExplored', result); 60 | }); 61 | explorer.fetchStylesheet(message.href, message.baseURI); 62 | }); 63 | vAPI.listen('addToWhitelist', (message, sendResponse) => { 64 | let url = null; 65 | try { 66 | url = new URL(message.url); 67 | } catch (e) { 68 | } 69 | if (!url || !url.hostname) { 70 | sendResponse('addToWhitelistError', {error: 'Invalid URL'}); 71 | return; 72 | } 73 | let existingRule = settings.parsedWhitelist.find(rule => rule.domain === url.hostname); 74 | if (existingRule) { 75 | sendResponse('addToWhitelistError', {error: 'The URL already exists in the whitelist'}); 76 | return; 77 | } 78 | 79 | // This really should be encapsulated in the whitelist module. 80 | if (!settings.whitelist) { 81 | settings.whitelist = '||' + url.hostname; 82 | } else { 83 | settings.whitelist = settings.whitelist + '\n||' + url.hostname; 84 | } 85 | settings.parsedWhitelist = parseRules(settings.whitelist); 86 | vAPI.updateSettings({whitelist: settings.whitelist}); 87 | sendResponse('addToWhitelistSuccess'); 88 | }); -------------------------------------------------------------------------------- /src/js/content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let exploration = { 3 | limit: 2, // Limit for exploration on shorter scroll distance 4 | lastScrollY: 0, // Keeps track of the scroll position during the last exploration 5 | // Storing the DOM nodes rather than stylesheet objects reduces memory consumption. 6 | internalSheets: [], // Internal top level stylesheets along with metadata 7 | externalSheets: {}, // A map where href is key and metadata is value 8 | sheetNodeSet: new Set(), // Owner nodes of all top level stylesheets 9 | selectors: { 10 | fixed: ['*[style*="fixed" i]'], 11 | sticky: ['*[style*="sticky" i]'], 12 | pseudoElements: [] 13 | } 14 | }; 15 | let settings = { 16 | // This a reference for the settings structure. The values will be updated. 17 | isDevelopment: false, 18 | behavior: 'scroll', 19 | whitelist: { 20 | type: 'none', // ['none', 'page', 'selectors'] 21 | selectors: [] // optional, if the type is 'selectors' 22 | }, 23 | transitionDuration: 0.2, // Duration of show/hide animation 24 | typesToShow: ['sidebar', 'splash', 'hidden'] // Hidden is here for caution - dimensions of a hidden element are unknown, and it cannot be classified 25 | }; 26 | let lastKnownScrollY = undefined; 27 | let stickyFixer = null; 28 | let scrollListener = _.debounce(_.throttle(ev => doAll(false, false, ev), 300), 50); // Debounce delay makes it run after the page scroll listeners 29 | 30 | class StickyFixer { 31 | constructor(stylesheet, state, getNewState, makeSelectorForHidden, hiddenStyle) { 32 | this.stylesheet = stylesheet; 33 | this.state = state; // hide, show, showFooters 34 | this.getNewState = getNewState; 35 | this.makeSelectorForHidden = makeSelectorForHidden; 36 | this.hiddenStyle = hiddenStyle; 37 | this.ruleCache = {}; 38 | } 39 | 40 | onChange(scrollInfo, forceUpdate) { 41 | let state = this.state; 42 | if (scrollInfo) { 43 | let input = { 44 | scrollY: scrollInfo.scrollY, 45 | oldState: this.state, 46 | isOnTop: scrollInfo.scrollY / window.innerHeight < 0.1, 47 | isOnBottom: (scrollInfo.scrollHeight - scrollInfo.scrollY) / window.innerHeight < 1.3 // close to 1/3 of the last screen 48 | }; 49 | let defaultState = input.isOnTop && 'show' || input.isOnBottom && 'showFooters' || 'hide'; 50 | state = this.getNewState(defaultState, input); 51 | } 52 | if (forceUpdate || state !== this.state) { 53 | this.updateStylesheet(this.getRules(state)); 54 | this.state = state; 55 | } 56 | } 57 | 58 | getRules(state) { 59 | // Opacity is the best way to fix the headers. Removing the fixed position breaks some layouts. 60 | // Select and hide them by the sticky-ducky-* attributes. 61 | // For better precision it's better to have `:moz-any(${exploration.selectors.sticky.join('')})` 62 | // instead of '*[sticky-ducky-position="sticky"]' but :moz-any and :is don't support compound selectors. 63 | // The :not(#sticky-ducky-boost-specificity) increases the specificity of the selectors. 64 | const rules = []; 65 | const typesToShow = state === 'showFooters' ? settings.typesToShow.concat('footer') : settings.typesToShow; 66 | const notWhitelistedSelector = settings.whitelist.type === 'selectors' ? settings.whitelist.selectors.map(s => `:not(${s})`).join('') : ''; 67 | 68 | const ignoreTypesToShowSelector = typesToShow.map(type => `:not([sticky-ducky-type="${type}"])`).join(''); 69 | const hiddenSelector = `[sticky-ducky-type]:not(#sticky-ducky-boost-specificity):not(:focus-within)${notWhitelistedSelector}`; 70 | 71 | // Apply the fix ignoring state. Otherwise, the layout will jump on scroll when shown after scrolling up. 72 | // Ignore cases that have top set to a non-zero value. For example, file headers in GitHub PRs. 73 | // If it is set to !important, the element would look shifted. 74 | const stickySelector = `*[sticky-ducky-position="sticky"]:not([style*="top:"]:not([style*="top:0"], [style*="top: 0"]))${hiddenSelector}`; 75 | 76 | // The static position doesn't work - see tests/stickyPosition.html 77 | // Relative position shifts when the element has a style for top, like GitHub does. 78 | // Hiding them makes little sense if they aren't out of viewport. 79 | const stickyFixStyle = this.getCachedStyle('stickyFixStyle', {position: 'relative', top: "0"}); 80 | rules.push(`${stickySelector} ${stickyFixStyle}`); 81 | 82 | const hideElsStyle = this.getCachedStyle('hideElsStyle', this.hiddenStyle); 83 | const showStyle = this.getCachedStyle('showStyle', {transition: `opacity ${settings.transitionDuration}s ease-in-out;`}); 84 | if (exploration.selectors.pseudoElements.length) { 85 | const allSelectors = exploration.selectors.pseudoElements.map(s => `${s.selector}::${s.pseudoElement}`).join(','); 86 | rules.push(`${allSelectors} ${showStyle}`); 87 | // Hide all fixed pseudo-elements. They cannot be classified, as you can't get their bounding rect 88 | // So a pseudo-element that looks like a footer would still be hidden when page is scrolled to the bottom. 89 | if (state !== 'show') { 90 | const hidePseudoElsSelector = exploration.selectors.pseudoElements.map(s => `${this.makeSelectorForHidden(s.selector)}::${s.pseudoElement}`).join(','); 91 | rules.push(`${hidePseudoElsSelector} ${hideElsStyle}`); 92 | } 93 | } 94 | 95 | const fixedSelector = `*[sticky-ducky-position="fixed"]${hiddenSelector}`; 96 | // To keep the opacity transitions animated, the show rule is included for all states. 97 | rules.push(`${fixedSelector} ${showStyle}`); 98 | if (state !== 'show') { 99 | const hideFixedElsSelector = this.makeSelectorForHidden(fixedSelector); 100 | rules.push(`${hideFixedElsSelector}${ignoreTypesToShowSelector} ${hideElsStyle}`); 101 | } 102 | 103 | return rules; 104 | } 105 | 106 | updateStylesheet(rules) { 107 | log('Updating stylesheet rules', rules); 108 | if (!this.stylesheet || !document.contains(this.stylesheet.ownerNode)) { 109 | let style = document.head.appendChild(document.createElement('style')); 110 | this.stylesheet = style.sheet; 111 | } 112 | // TODO: compare cssText against the rule and replace only the mismatching rules 113 | _.map(this.stylesheet.cssRules, () => this.stylesheet.deleteRule(0)); 114 | rules.forEach(rule => this.stylesheet.insertRule(rule, this.stylesheet.cssRules.length)); 115 | } 116 | 117 | getCachedStyle(name, style) { 118 | if (!this.ruleCache[name]) { 119 | this.ruleCache[name] = makeStyle(style); 120 | } 121 | return this.ruleCache[name]; 122 | } 123 | } 124 | 125 | let fixers = { 126 | 'hover': { 127 | getNewState: defaultState => defaultState, 128 | makeSelectorForHidden: selector => selector + ':not(:hover)', 129 | // In case the element has animation keyframes involving opacity, set animation to none 130 | // Opacity in a keyframe overrides even an !important rule. 131 | hiddenStyle: {opacity: 0, animation: 'none'} 132 | }, 133 | 'scroll': { 134 | getNewState: (defaultState, {scrollY, oldState}) => { 135 | log('scroll decision', defaultState, scrollY, lastKnownScrollY, oldState); 136 | return scrollY === lastKnownScrollY && oldState 137 | || scrollY < lastKnownScrollY && 'show' 138 | || defaultState 139 | }, 140 | makeSelectorForHidden: selector => selector, 141 | hiddenStyle: { 142 | opacity: 0, 143 | // Display: none cannot be used in a transition. 144 | // So visibility hides a sticky, and pointer-events makes it non-interactive. 145 | visibility: 'hidden', 146 | transition: `opacity ${settings.transitionDuration}s ease-in-out, visibility 0s ${settings.transitionDuration}s`, 147 | animation: 'none', 148 | 'pointer-events': 'none' 149 | } 150 | }, 151 | 'top': { 152 | getNewState: defaultState => defaultState, 153 | makeSelectorForHidden: selector => selector, 154 | hiddenStyle: { 155 | opacity: 0, 156 | visibility: 'hidden', 157 | transition: `opacity ${settings.transitionDuration}s ease-in-out, visibility 0s ${settings.transitionDuration}s`, 158 | animation: 'none', 159 | 'pointer-events': 'none' 160 | } 161 | }, 162 | 'absolute': { 163 | getNewState: defaultState => defaultState, 164 | makeSelectorForHidden: selector => selector, 165 | hiddenStyle: {position: 'absolute'} 166 | } 167 | }; 168 | 169 | function makeStyle(styles) { 170 | const stylesText = Object.keys(styles).map(name => `${name}: ${styles[name]} !important;`); 171 | return `{ ${stylesText.join('')} }`; 172 | } 173 | 174 | function getDocumentHeight() { 175 | // http://james.padolsey.com/javascript/get-document-height-cross-browser/ 176 | const body = document.body, html = document.documentElement; 177 | return Math.max( 178 | body.scrollHeight, body.offsetHeight, body.clientHeight, 179 | html.scrollHeight, html.offsetHeight, html.clientHeight); 180 | } 181 | 182 | function log(...args) { 183 | if (settings.isDevelopment) { 184 | console.log('Sticky Ducky: ', ...args); 185 | } 186 | } 187 | 188 | function measure(label, f) { 189 | if (!settings.isDevelopment) return f(); 190 | const before = window.performance.now(); 191 | const result = f(); 192 | const after = window.performance.now(); 193 | log(`Call to ${label} took ${after - before}ms`); 194 | return result; 195 | } 196 | 197 | function classify(el) { 198 | // Optimize for hidden elements 199 | if (window.getComputedStyle(el).display === 'none') { 200 | return 'hidden'; 201 | } 202 | const viewportWidth = window.innerWidth, 203 | viewportHeight = window.innerHeight, 204 | rect = el.getBoundingClientRect(), 205 | clip = (val, low, high, max) => Math.max(0, val + Math.min(low, 0) + Math.min(max - high, 0)), 206 | width = clip(rect.width || el.scrollWidth, rect.left, rect.right, viewportWidth), 207 | height = clip(rect.height || el.scrollHeight, rect.top, rect.bottom, viewportHeight), 208 | isWide = width / viewportWidth > 0.35, 209 | isThin = height / viewportHeight < 0.25, 210 | isTall = height / viewportHeight > 0.5, 211 | isOnTop = rect.top / viewportHeight < 0.1, 212 | isOnBottom = rect.bottom / viewportHeight > 0.9, 213 | isOnSide = rect.left / viewportWidth < 0.1 || rect.right / viewportWidth > 0.9; 214 | const type = isWide && isThin && isOnTop && 'header' 215 | || isWide && isThin && isOnBottom && 'footer' 216 | || isWide && isTall && 'splash' 217 | || isTall && isOnSide && 'sidebar' 218 | || width === 0 && height === 0 && 'hidden' 219 | || 'widget'; 220 | log(`Classified as ${type}`, el); 221 | return type; 222 | } 223 | 224 | function onNewSettings(newSettings) { 225 | // The new settings may contain only the updated properties 226 | _.extend(settings, newSettings); 227 | if (document.readyState === 'loading') { 228 | document.addEventListener('DOMContentLoaded', activateSettings); 229 | } else { 230 | activateSettings(); 231 | } 232 | } 233 | 234 | function activateSettings() { 235 | log(`Activating behavior ${settings.behavior}`); 236 | const isActive = !!stickyFixer; // Presence of stickyFixer indicates that the scroll listener is set 237 | const shouldBeActive = settings.behavior !== 'always' && settings.whitelist.type !== 'page'; 238 | if (shouldBeActive) { 239 | // Detecting passive events on Firefox and setting the listener immediately is buggy. Manifest supports only browsers that have it. 240 | if (!isActive) { 241 | document.addEventListener('scroll', scrollListener, {passive: true, capture: true}); 242 | } 243 | const newFixer = fixers[settings.behavior]; 244 | stickyFixer = new StickyFixer(stickyFixer && stickyFixer.stylesheet, stickyFixer && stickyFixer.state, newFixer.getNewState, newFixer.makeSelectorForHidden, newFixer.hiddenStyle); 245 | doAll(true, true); 246 | } else if (isActive && !shouldBeActive) { 247 | document.removeEventListener('scroll', scrollListener); 248 | if (stickyFixer.stylesheet) stickyFixer.stylesheet.ownerNode.remove(); 249 | stickyFixer = null; 250 | } 251 | } 252 | 253 | let exploreStickies = () => { 254 | let selectors = exploration.selectors.fixed.concat(exploration.selectors.sticky); 255 | let els = document.querySelectorAll(selectors.join(',')); 256 | els.forEach(el => { 257 | // Attributes are less likely to interfere with the page than dataset data-*. 258 | let type = el.getAttribute('sticky-ducky-type') 259 | if (!type || type === 'hidden') { 260 | el.setAttribute('sticky-ducky-type', classify(el)); 261 | } 262 | 263 | let position = el.getAttribute('sticky-ducky-position'); 264 | if (!position || position === 'other') { 265 | // Think of a header that only gets fixed once you scroll. That's why "other" has to be checked regularly. 266 | el.setAttribute('sticky-ducky-position', getPosition(el)); 267 | } 268 | }); 269 | log('explored stickies', els); 270 | }; 271 | 272 | let getPosition = el => { 273 | // This handles "FiXeD !important" or "-webkit-sticky" positions 274 | const position = window.getComputedStyle(el).position.toLowerCase(); 275 | return position.includes('fixed') && 'fixed' 276 | || position.includes('sticky') && 'sticky' 277 | || 'other'; 278 | }; 279 | 280 | function exploreStylesheets() { 281 | let anyRemoved = false; 282 | let explorer = new Explorer(result => onSheetExplored(result)); 283 | // We detect dynamic updates for the internal stylesheets by comparing rules size. 284 | // All internal (declared with 83 | 84 | 85 |
Sticky Ducky
86 |
87 |
When to show sticky elements:
88 |
89 | 90 | 91 | 92 | 93 |
94 | 95 | 96 |
97 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /tests/animationOpacity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 29 | 30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /tests/bigSticky.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /tests/classification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 | 39 | -------------------------------------------------------------------------------- /tests/delayedStyling.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /tests/elementMoves.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 12 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/headerWrongSize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /tests/import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/inlineLink.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /tests/outsideBody.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /tests/pseudoElement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/stickyPosition.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | 24 | 25 |
26 |
27 |
header 1
28 |
29 |

30 | Setting position to initial on the sticky elements affects the dimensions of its absolutely positioned children. 31 |

32 |
33 |
34 |
35 |
header 2
36 |
37 |

Second card

38 |
39 |
40 | 41 |
42 |
header 3
43 |
44 |

Third card

45 |
46 | 47 | -------------------------------------------------------------------------------- /tests/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: yellowgreen; 3 | height: 800em; 4 | margin: 0; 5 | } 6 | 7 | .stickyContent { 8 | width: 100%; 9 | height: 3em; 10 | background-color: black; 11 | opacity: 0.7; 12 | color: white; 13 | } -------------------------------------------------------------------------------- /tests/weirdScroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /tests/zeroHeight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | --------------------------------------------------------------------------------