├── .gitignore ├── package.json ├── README.md ├── LICENSE.md ├── style.css ├── index.html ├── gosh-hang-it.js └── lib └── polyfill.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gosh-hang-it", 3 | "version": "1.0.0", 4 | "description": "Polyfill for hanging-punctuation CSS property", 5 | "main": "gosh-hang-it.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/liamdanger/gosh-hang-it.git" 12 | }, 13 | "keywords": [ 14 | "punctuation", 15 | "typography", 16 | "polyfill" 17 | ], 18 | "author": "Liam Campbell", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/liamdanger/gosh-hang-it/issues" 22 | }, 23 | "homepage": "https://github.com/liamdanger/gosh-hang-it#readme" 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gosh Hang It! 2 | 3 | Gosh Hang It is a simple polyfill for the [`hanging-punctuation`](https://css-tricks.com/almanac/properties/h/hanging-punctuation/) CSS property, which exists in the spec but is not supported by any major browser. 4 | 5 | Here is an [example](http://liamdanger.github.io/gosh-hang-it)! 6 | 7 | ## Usage 8 | Gosh Hang It has one dependency, and that's `polyfill.js`. It's included in the repo! All you have to do is: 9 | 10 | 1. Add `polyfill.js` and `gosh-hang-it.js` to your HTML 11 | 2. Apply the CSS property `hanging-punctuation: first;` to your stylesheets liberally 12 | 3. Rejoice :balloon: 13 | 14 | --- 15 | 16 | ![Hang in there](http://ephemera.muledesign.com/hang-in-there.jpg) 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Liam Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | /** 3 | So first off we've gotta make sure we're in a nice space, plenty of room 4 | to maneuver for the trick we're about to pull. 5 | */ 6 | margin: 0 auto; 7 | max-width: 33em; 8 | padding: 20px; 9 | 10 | /** 11 | And this whole thing doesn't get pulled off without text, so we better 12 | make sure we've got some fine, nice-looking text to read. 13 | */ 14 | color: #222; 15 | font-family: "Droid Serif", "Georgia", "Times New Roman", serif; 16 | font-size: 20px; 17 | line-height: 1.5; 18 | } 19 | 20 | /** 21 | But I mean, we don't want the text to take away from the main attraction! 22 | Let's put a couple of little rules next to the text, so you can tell what's 23 | gonna happen. In the business, we call this "telegraphing". 24 | */ 25 | p { 26 | border-left: 1px solid #e0e0ff; 27 | } 28 | 29 | /** 30 | Don't forget, we're pulling off several versions of this little show, so we 31 | need to keep 'em separated. 32 | */ 33 | p + p { 34 | margin-top: 28px; 35 | } 36 | 37 | /** 38 | At some point we might want to display some code samples. They should look nice! 39 | */ 40 | code { 41 | color: #666; 42 | font-size: 16px; 43 | 44 | padding: 2px 2px; 45 | 46 | background-color: #eee; 47 | border-radius: 2px; 48 | } 49 | 50 | /** 51 | Let's also make sure links look okay 52 | */ 53 | a { 54 | color: #222; 55 | text-decoration: none; 56 | box-shadow: 0 5px 0 #D3E8F6; 57 | } 58 | a:hover { 59 | color: #268BD2; 60 | } 61 | 62 | /** 63 | Finally, here's the main attraction. It might look like crapnasty unsupported 64 | CSS to you now, but just wait and see if you don't change your mind. 65 | */ 66 | 67 | .hang-none { hanging-punctuation: none; } 68 | .hang-first { hanging-punctuation: first; } 69 | .hang-last { hanging-punctuation: last; } 70 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | "Gosh Hang It" 12 | 13 | 14 | 15 |

Gosh Hang It!

16 | 17 |

Gosh Hang It is a simple polyfill for the hanging-punctuation CSS property, which exists in the spec but is not supported by any major browser. The R. Buckminster Fuller quotes below are inside a container that has the hanging-punctuation: first rule applied to them.

18 | 19 |
20 |

21 | “I am enthusiastic over humanity’s extraordinary and sometimes ‘very’ timely ingenuities. If 22 | you are in a shipwreck and all the boats are gone, a piano top buoyant enough to keep you 23 | afloat that comes along makes a fortuitous life preserver. But this is not to say that the best 24 | way to design a life preserver is in the form of a piano top. I think that we are clinging to a 25 | great many piano tops in accepting yesterday’s fortuitous contrivings as constituting the only 26 | means for solving a given problem. Our brains deal exclusively with special-case experiences. 27 | Only our minds are able to discover the “generalized” principles operating without exception in 28 | each and every special-experience case which if detected and mastered will give knowledgeable 29 | advantage in all instances.” 30 |

31 | 32 |

33 | “Because our spontaneous initiative has been frustrated, too often inadvertently, in earliest 34 | childhood we do not tend, customarily, to dare to think competently regarding our potentials. 35 | We find it socially easier to go on with our narrow, shortsighted specializations and leave it to 36 | others—primarily to the politicians—to find some way of resolving our common dilemmas. 37 | Countering that spontaneous grownup trend to narrowness I will do my, hopefully ‘childish,’ 38 | best to confront as many of our problems as possible by employing the longest-distance 39 | thinking of which I am capable—though that may not take us very far into the future.” 40 |

41 | 42 | 43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /gosh-hang-it.js: -------------------------------------------------------------------------------- 1 | (function(window, document, undefined) { 2 | 3 | // Enable strict mode; no shirking or loafing permitted 4 | "use strict"; 5 | 6 | // Array of hangable punctuation characters 7 | var hangables = ['\'', '"', '‘', '’', '“', '”', ',', '.', '،', '۔', '、', '。', ',', '.', '﹐', '﹑', '﹒', '。', '、', '«', '»']; 8 | 9 | // Don't wrap characters in tags that contain non-display text 10 | var disallowedNodes = ['title', 'head', 'script', 'style']; 11 | 12 | // Monolithic objecty thing 13 | var gosh = {}; 14 | 15 | // Array of Hangable objects will live here 16 | gosh.chars = []; 17 | 18 | gosh.wrapHangables = function(el) { 19 | // Wrap hangable characters in an easily queryable and styleable span 20 | 21 | // but first make sure that we're not after a non-hangable node 22 | if (disallowedNodes.indexOf(el.nodeName.toLowerCase()) != -1) { 23 | return false; 24 | } 25 | 26 | var nodes = el.childNodes; 27 | 28 | for (var i = 0; i < nodes.length; ++i) { 29 | var node = nodes[i]; 30 | 31 | if(node.nodeType == 1) { 32 | // Recurse elements: gotta get to the juicy text nodes inside the HTML 33 | gosh.wrapHangables(node); 34 | } else if(node.nodeType == 3) { 35 | gosh.wrapCharactersInTextNode(node); 36 | } 37 | } 38 | 39 | gosh.trimEmptyWrappers(); 40 | } 41 | 42 | gosh.wrapCharactersInTextNode = function(node) { 43 | // For text nodes, wrap the hangable characters in spans and then wrap 44 | // that whole mess in another span so we can operate upon it in HTML 45 | 46 | var text = node.textContent, 47 | temp = document.createElement('span'), 48 | matchChars = new RegExp('[' + hangables.join('|') + ']', 'g'); 49 | 50 | text = text.replace(matchChars, function(match) { 51 | return '' + match + ''; 52 | }); 53 | 54 | temp.setAttribute('data-hang-wrapper', 'true'); 55 | temp.innerHTML = text; 56 | 57 | node.parentNode.insertBefore(temp, node); 58 | node.parentNode.removeChild(node); 59 | } 60 | 61 | gosh.trimEmptyWrappers = function() { 62 | var empties = document.querySelectorAll( '[data-hang-wrapper]' ); 63 | 64 | for (var i = 0; i < empties.length; ++i) { 65 | var empty = empties[i]; 66 | 67 | if (empty.innerHTML.match(/^\s*$/)) { 68 | empty.remove(); 69 | } 70 | } 71 | } 72 | 73 | gosh.unwrapHangables = function(el) { 74 | // Unwrap hangable characters to get the DOM mostly back to normal 75 | 76 | var nodes = el.childNodes; 77 | console.log(nodes); 78 | } 79 | 80 | gosh.instantiateHangables = function() { 81 | // Make a new Hangable object to manage each hangable character 82 | 83 | var chars = document.querySelectorAll('[data-hang]'); 84 | 85 | for (var i = 0; i < chars.length; ++i) { 86 | var char = chars[i]; 87 | 88 | var hangable = new Hangable(char); 89 | } 90 | } 91 | 92 | gosh.doMatched = function(rules) { 93 | rules.each(function(rule) { 94 | var matchedEl = document.querySelectorAll( rule.getSelectors() )[0]; 95 | 96 | if(matchedEl) { 97 | gosh.wrapHangables(matchedEl); 98 | gosh.instantiateHangables(); 99 | } 100 | }); 101 | } 102 | 103 | gosh.unhangAll = function(rules) { 104 | gosh.chars.forEach(function(char) { 105 | char.unhang().destroy(); 106 | }); 107 | } 108 | 109 | // Hangable object 110 | function Hangable(el) { 111 | this.el = el; 112 | this.container = this.getFirstBlockParent(); 113 | this.wrapper = this.container.querySelector('[data-hang-wrapper]'); 114 | 115 | gosh.chars.push(this); 116 | 117 | this.hang(); 118 | } 119 | 120 | Hangable.prototype.getFirstBlockParent = function() { 121 | var el = this.el; 122 | 123 | // Loop through parent nodes until we find a block, or close enough 124 | while (el.parentNode) { 125 | el = el.parentNode; 126 | var display = getComputedStyle(el).display; 127 | 128 | if (display == "block" || display == "flex" || display == "grid") { 129 | return el; 130 | } 131 | } 132 | } 133 | 134 | Hangable.prototype.getRelativePosition = function() { 135 | var containerOffset, elOffset; 136 | 137 | containerOffset = this.getContainerTrueOffset(); 138 | elOffset = this.el.offsetLeft; 139 | 140 | this.el.setAttribute('data-hang-position', elOffset - containerOffset); 141 | return elOffset - containerOffset; 142 | } 143 | 144 | Hangable.prototype.getRelativeWidth = function() { 145 | return (this.el.offsetWidth / this.getContainerWidth()); 146 | } 147 | 148 | Hangable.prototype.hang = function() { 149 | this.el.style.marginLeft = ''; 150 | 151 | if (this.getRelativePosition() == 0 && this.el === this.wrapper.firstElementChild) { 152 | this.el.style.marginLeft = (-100 * this.getRelativeWidth()) + '%'; 153 | 154 | return true; 155 | } 156 | 157 | return false; 158 | } 159 | 160 | Hangable.prototype.unhang = function() { 161 | // get the element's parent node 162 | var parent = this.el.parentNode; 163 | 164 | // move all children out of the element 165 | while (this.el.firstChild) parent.insertBefore(this.el.firstChild, this.el); 166 | 167 | // remove the empty element 168 | parent.removeChild(this.el); 169 | 170 | return this; 171 | } 172 | 173 | Hangable.prototype.destroy = function() { 174 | delete this; 175 | } 176 | 177 | Hangable.prototype.getContainerTrueOffset = function() { 178 | // Get the container's offset, minus borders or any other interloping spaces 179 | 180 | var containerOffset = this.container.offsetLeft + this.container.clientLeft; 181 | return containerOffset; 182 | } 183 | 184 | Hangable.prototype.getContainerWidth = function() { 185 | return this.container.clientWidth; 186 | } 187 | 188 | // Set up the polyfill 189 | window.onload = function() { 190 | Polyfill({ 191 | declarations: ["hanging-punctuation:first"] 192 | }) 193 | .doMatched(gosh.doMatched) 194 | .undoUnmatched(gosh.unhangAll); 195 | } 196 | 197 | // Make it Responsive(tm) 198 | window.onresize = function() { 199 | gosh.chars.forEach(function(char) { 200 | char.hang(); 201 | }); 202 | } 203 | 204 | // Export this whole thing at the end 205 | window.goshHangIt = gosh; 206 | 207 | })(window, document); 208 | -------------------------------------------------------------------------------- /lib/polyfill.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Polyfill.js - v0.1.0 3 | * 4 | * Copyright (c) 2015 Philip Walton 5 | * Released under the MIT license 6 | * 7 | * Date: 2015-06-21 8 | */ 9 | ;(function(window, document, undefined){ 10 | 11 | 'use strict'; 12 | 13 | var reNative = RegExp('^' + 14 | String({}.valueOf) 15 | .replace(/[.*+?\^${}()|\[\]\\]/g, '\\$&') 16 | .replace(/valueOf|for [^\]]+/g, '.+?') + '$' 17 | ) 18 | 19 | 20 | /** 21 | * Trim any leading or trailing whitespace 22 | */ 23 | function trim(s) { 24 | return s.replace(/^\s+|\s+$/g,'') 25 | } 26 | 27 | 28 | /** 29 | * Detects the presence of an item in an array 30 | */ 31 | function inArray(target, items) { 32 | var item 33 | , i = 0 34 | if (!target || !items) return false 35 | while(item = items[i++]) { 36 | if (target === item) return true 37 | } 38 | return false 39 | } 40 | 41 | 42 | /** 43 | * Determine if a method is support natively by the browser 44 | */ 45 | function isNative(fn) { 46 | return reNative.test(fn) 47 | } 48 | 49 | /** 50 | * Determine if a URL is local to the document origin 51 | * Inspired form Respond.js 52 | * https://github.com/scottjehl/Respond/blob/master/respond.src.js#L90-L91 53 | */ 54 | var isLocalURL = (function() { 55 | var base = document.getElementsByTagName("base")[0] 56 | , reProtocol = /^([a-zA-Z:]*\/\/)/ 57 | return function(url) { 58 | var isLocal = (!reProtocol.test(url) && !base) 59 | || url.replace(RegExp.$1, "").split("/")[0] === location.host 60 | return isLocal 61 | } 62 | }()) 63 | 64 | var supports = { 65 | // true with either native support or a polyfil, we don't care which 66 | matchMedia: window.matchMedia && window.matchMedia( "only all" ).matches, 67 | // true only if the browser supports window.matchMeida natively 68 | nativeMatchMedia: isNative(window.matchMedia) 69 | } 70 | 71 | var DownloadManager = (function() { 72 | 73 | var cache = {} 74 | , queue = [] 75 | , callbacks = [] 76 | , requestCount = 0 77 | , xhr = (function() { 78 | var method 79 | try { method = new window.XMLHttpRequest() } 80 | catch (e) { method = new window.ActiveXObject( "Microsoft.XMLHTTP" ) } 81 | return method 82 | }()) 83 | 84 | // return function(urls, callback) { 85 | 86 | function addURLsToQueue(urls) { 87 | var url 88 | , i = 0 89 | while (url = urls[i++]) { 90 | if (!cache[url] && !inArray(url, queue)) { 91 | queue.push(url) 92 | } 93 | } 94 | } 95 | 96 | function processQueue() { 97 | // don't process the next one if we're in the middle of a download 98 | if (!(xhr.readyState === 0 || xhr.readyState === 4)) return 99 | 100 | var url 101 | if (url = queue[0]) { 102 | downloadStylesheet(url) 103 | } 104 | if (!url) { 105 | invokeCallbacks() 106 | } 107 | } 108 | 109 | /** 110 | * Make the requests 111 | * 112 | * TODO: Get simultaneous downloads working, it can't be that hard 113 | */ 114 | function downloadStylesheet(url) { 115 | requestCount++ 116 | xhr.open("GET", url, true) 117 | xhr.onreadystatechange = function () { 118 | if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) { 119 | cache[url] = xhr.responseText 120 | queue.shift() 121 | processQueue() 122 | } 123 | } 124 | xhr.send(null) 125 | } 126 | 127 | /** 128 | * Check the cache to make sure all requests are complete 129 | */ 130 | function downloadsFinished(urls) { 131 | var url 132 | , i = 0 133 | , len = 0 134 | while (url = urls[i++]) { 135 | if (cache[url]) len++ 136 | } 137 | return (len === urls.length) 138 | } 139 | 140 | /** 141 | * Invoke each callback and remove it from the list 142 | */ 143 | function invokeCallbacks() { 144 | var callback 145 | while (callback = callbacks.shift()) { 146 | invokeCallback(callback.urls, callback.fn) 147 | } 148 | } 149 | 150 | /** 151 | * Put the stylesheets in the proper order and invoke the callback 152 | */ 153 | function invokeCallback(urls, callback) { 154 | var stylesheets = [] 155 | , url 156 | , i = 0 157 | while (url = urls[i++]) { 158 | stylesheets.push(cache[url]) 159 | } 160 | callback.call(null, stylesheets) 161 | } 162 | 163 | return { 164 | request: function(urls, callback) { 165 | // Add the callback to the list 166 | callbacks.push({urls: urls, fn: callback}) 167 | 168 | if (downloadsFinished(urls)) { 169 | invokeCallbacks() 170 | } else { 171 | addURLsToQueue(urls) 172 | processQueue() 173 | } 174 | }, 175 | clearCache: function() { 176 | cache = {} 177 | }, 178 | _getRequestCount: function() { 179 | return requestCount 180 | } 181 | } 182 | 183 | }()) 184 | 185 | var StyleManager = { 186 | 187 | _cache: {}, 188 | 189 | clearCache: function() { 190 | StyleManager._cache = {} 191 | }, 192 | 193 | /** 194 | * Parse a string of CSS 195 | * optionaly pass an identifier for caching 196 | * 197 | * Adopted from TJ Holowaychuk's 198 | * https://github.com/visionmedia/css-parse 199 | * 200 | * Minor changes include removing the "stylesheet" root and 201 | * using String.charAt(i) instead of String[i] for IE7 compatibility 202 | */ 203 | parse: function(css, identifier) { 204 | 205 | /** 206 | * Opening brace. 207 | */ 208 | function open() { 209 | return match(/^\{\s*/) 210 | } 211 | 212 | /** 213 | * Closing brace. 214 | */ 215 | function close() { 216 | return match(/^\}\s*/) 217 | } 218 | 219 | /** 220 | * Parse ruleset. 221 | */ 222 | function rules() { 223 | var node 224 | var rules = [] 225 | whitespace() 226 | comments(rules) 227 | while (css.charAt(0) != '}' && (node = atrule() || rule())) { 228 | rules.push(node) 229 | comments(rules) 230 | } 231 | return rules 232 | } 233 | 234 | /** 235 | * Match `re` and return captures. 236 | */ 237 | function match(re) { 238 | var m = re.exec(css) 239 | if (!m) return 240 | css = css.slice(m[0].length) 241 | return m 242 | } 243 | 244 | /** 245 | * Parse whitespace. 246 | */ 247 | function whitespace() { 248 | match(/^\s*/) 249 | } 250 | 251 | /** 252 | * Parse comments 253 | */ 254 | function comments(rules) { 255 | rules = rules || [] 256 | var c 257 | while (c = comment()) rules.push(c) 258 | return rules 259 | } 260 | 261 | /** 262 | * Parse comment. 263 | */ 264 | function comment() { 265 | if ('/' == css[0] && '*' == css[1]) { 266 | var i = 2 267 | while ('*' != css[i] || '/' != css[i + 1]) ++i 268 | i += 2 269 | var comment = css.slice(2, i - 2) 270 | css = css.slice(i) 271 | whitespace() 272 | return { comment: comment } 273 | } 274 | } 275 | 276 | /** 277 | * Parse selector. 278 | */ 279 | function selector() { 280 | var m = match(/^([^{]+)/) 281 | if (!m) return 282 | return trim(m[0]).split(/\s*,\s*/) 283 | } 284 | 285 | /** 286 | * Parse declaration. 287 | */ 288 | function declaration() { 289 | // prop 290 | var prop = match(/^(\*?[\-\w]+)\s*/) 291 | if (!prop) return 292 | prop = prop[0] 293 | 294 | // : 295 | if (!match(/^:\s*/)) return 296 | 297 | // val 298 | var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)\s*/) 299 | if (!val) return 300 | val = trim(val[0]) 301 | 302 | // 303 | match(/^[;\s]*/) 304 | 305 | return { property: prop, value: val } 306 | } 307 | 308 | /** 309 | * Parse keyframe. 310 | */ 311 | function keyframe() { 312 | var m 313 | var vals = [] 314 | 315 | while (m = match(/^(from|to|\d+%|\.\d+%|\d+\.\d+%)\s*/)) { 316 | vals.push(m[1]) 317 | match(/^,\s*/) 318 | } 319 | 320 | if (!vals.length) return 321 | 322 | return { 323 | values: vals, 324 | declarations: declarations() 325 | } 326 | } 327 | 328 | /** 329 | * Parse keyframes. 330 | */ 331 | function keyframes() { 332 | var m = match(/^@([\-\w]+)?keyframes */) 333 | if (!m) return 334 | var vendor = m[1] 335 | 336 | // identifier 337 | var m = match(/^([\-\w]+)\s*/) 338 | if (!m) return 339 | var name = m[1] 340 | 341 | if (!open()) return 342 | comments() 343 | 344 | var frame 345 | var frames = [] 346 | while (frame = keyframe()) { 347 | frames.push(frame) 348 | comments() 349 | } 350 | 351 | if (!close()) return 352 | 353 | var obj = { 354 | name: name, 355 | keyframes: frames 356 | } 357 | // don't include vendor unles there's a match 358 | if (vendor) obj.vendor = vendor 359 | 360 | return obj 361 | } 362 | 363 | /** 364 | * Parse supports. 365 | */ 366 | function supports() { 367 | var m = match(/^@supports *([^{]+)/) 368 | if (!m) return 369 | var supports = trim(m[1]) 370 | 371 | if (!open()) return 372 | comments() 373 | 374 | var style = rules() 375 | 376 | if (!close()) return 377 | 378 | return { supports: supports, rules: style } 379 | } 380 | 381 | /** 382 | * Parse media. 383 | */ 384 | function media() { 385 | var m = match(/^@media *([^{]+)/) 386 | if (!m) return 387 | 388 | var media = trim(m[1]) 389 | 390 | if (!open()) return 391 | comments() 392 | 393 | var style = rules() 394 | 395 | if (!close()) return 396 | 397 | return { media: media, rules: style } 398 | } 399 | 400 | 401 | /** 402 | * Parse paged media. 403 | */ 404 | function atpage() { 405 | var m = match(/^@page */) 406 | if (!m) return 407 | 408 | var sel = selector() || [] 409 | var decls = [] 410 | 411 | if (!open()) return 412 | comments() 413 | 414 | // declarations 415 | var decl 416 | while (decl = declaration() || atmargin()) { 417 | decls.push(decl) 418 | comments() 419 | } 420 | 421 | if (!close()) return 422 | 423 | return { 424 | type: "page", 425 | selectors: sel, 426 | declarations: decls 427 | } 428 | } 429 | 430 | /** 431 | * Parse margin at-rules 432 | */ 433 | function atmargin() { 434 | var m = match(/^@([a-z\-]+) */) 435 | if (!m) return 436 | var type = m[1] 437 | 438 | return { 439 | type: type, 440 | declarations: declarations() 441 | } 442 | } 443 | 444 | /** 445 | * Parse import 446 | */ 447 | function atimport() { 448 | return _atrule('import') 449 | } 450 | 451 | /** 452 | * Parse charset 453 | */ 454 | function atcharset() { 455 | return _atrule('charset') 456 | } 457 | 458 | /** 459 | * Parse namespace 460 | */ 461 | function atnamespace() { 462 | return _atrule('namespace') 463 | } 464 | 465 | /** 466 | * Parse non-block at-rules 467 | */ 468 | function _atrule(name) { 469 | var m = match(new RegExp('^@' + name + ' *([^;\\n]+);\\s*')) 470 | if (!m) return 471 | var ret = {} 472 | ret[name] = trim(m[1]) 473 | return ret 474 | } 475 | 476 | /** 477 | * Parse declarations. 478 | */ 479 | function declarations() { 480 | var decls = [] 481 | 482 | if (!open()) return 483 | comments() 484 | 485 | // declarations 486 | var decl 487 | while (decl = declaration()) { 488 | decls.push(decl) 489 | comments() 490 | } 491 | 492 | if (!close()) return 493 | return decls 494 | } 495 | 496 | /** 497 | * Parse at rule. 498 | */ 499 | function atrule() { 500 | return keyframes() 501 | || media() 502 | || supports() 503 | || atimport() 504 | || atcharset() 505 | || atnamespace() 506 | || atpage() 507 | } 508 | 509 | /** 510 | * Parse rule. 511 | */ 512 | function rule() { 513 | var sel = selector() 514 | if (!sel) return 515 | comments() 516 | return { selectors: sel, declarations: declarations() } 517 | } 518 | 519 | /** 520 | * Check the cache first, otherwise parse the CSS 521 | */ 522 | if (identifier && StyleManager._cache[identifier]) { 523 | return StyleManager._cache[identifier] 524 | } else { 525 | // strip comments before parsing 526 | css = css.replace(/\/\*[\s\S]*?\*\//g, "") 527 | return StyleManager._cache[identifier] = rules() 528 | } 529 | 530 | }, 531 | 532 | /** 533 | * Filter a ruleset by the passed keywords 534 | * Keywords may be either selector or property/value patterns 535 | */ 536 | filter: function(rules, keywords) { 537 | 538 | var filteredRules = [] 539 | 540 | /** 541 | * Concat a2 onto a1 even if a1 is undefined 542 | */ 543 | function safeConcat(a1, a2) { 544 | if (!a1 && !a2) return 545 | if (!a1) return [a2] 546 | return a1.concat(a2) 547 | } 548 | 549 | /** 550 | * Add a rule to the filtered ruleset, 551 | * but don't add empty media or supports values 552 | */ 553 | function addRule(rule) { 554 | if (rule.media == null) delete rule.media 555 | if (rule.supports == null) delete rule.supports 556 | filteredRules.push(rule) 557 | } 558 | 559 | function containsKeyword(string, keywordList) { 560 | if (!keywordList) return 561 | var i = keywordList.length 562 | while (i--) { 563 | if (string.indexOf(keywordList[i]) >= 0) return true 564 | } 565 | } 566 | 567 | function matchesKeywordPattern(declaration, patternList) { 568 | var wildcard = /\*/ 569 | , pattern 570 | , parts 571 | , reProp 572 | , reValue 573 | , i = 0 574 | while (pattern = patternList[i++]) { 575 | parts = pattern.split(":") 576 | reProp = new RegExp("^" + trim(parts[0]).replace(wildcard, ".*") + "$") 577 | reValue = new RegExp("^" + trim(parts[1]).replace(wildcard, ".*") + "$") 578 | if (reProp.test(declaration.property) && reValue.test(declaration.value)) { 579 | return true 580 | } 581 | } 582 | } 583 | 584 | function matchSelectors(rule, media, supports) { 585 | if (!keywords.selectors) return 586 | 587 | if (containsKeyword(rule.selectors.join(","), keywords.selectors)) { 588 | addRule({ 589 | media: media, 590 | supports: supports, 591 | selectors: rule.selectors, 592 | declarations: rule.declarations 593 | }) 594 | return true 595 | } 596 | } 597 | 598 | function matchesDeclaration(rule, media, supports) { 599 | if (!keywords.declarations) return 600 | var declaration 601 | , i = 0 602 | while (declaration = rule.declarations[i++]) { 603 | if (matchesKeywordPattern(declaration, keywords.declarations)) { 604 | addRule({ 605 | media: media, 606 | supports: supports, 607 | selectors: rule.selectors, 608 | declarations: rule.declarations 609 | }) 610 | return true 611 | } 612 | } 613 | } 614 | 615 | function filterRules(rules, media, supports) { 616 | var rule 617 | , i = 0 618 | while (rule = rules[i++]) { 619 | if (rule.declarations) { 620 | matchSelectors(rule, media, supports) || matchesDeclaration(rule, media, supports) 621 | } 622 | else if (rule.rules && rule.media) { 623 | filterRules(rule.rules, safeConcat(media, rule.media), supports) 624 | } 625 | else if (rule.rules && rule.supports) { 626 | filterRules(rule.rules, media, safeConcat(supports, rule.supports)) 627 | } 628 | } 629 | 630 | } 631 | 632 | // start the filtering 633 | filterRules(rules) 634 | 635 | // return the results 636 | return filteredRules 637 | 638 | } 639 | } 640 | var MediaManager = (function() { 641 | 642 | var reMinWidth = /\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ 643 | , reMaxWidth = /\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ 644 | 645 | // a cache of the active media query info 646 | , mediaQueryMap = {} 647 | 648 | // the value of an `em` as used in a media query, 649 | // not necessarily the base font-size 650 | , emValueInPixels 651 | , currentWidth 652 | 653 | /** 654 | * Get the pixel value of 1em for use in parsing media queries 655 | * ems in media queries are not affected by CSS, instead they 656 | * are the value of the browsers default font size, usually 16px 657 | */ 658 | function getEmValueInPixels() { 659 | 660 | // cache this value because it probably won't change and 661 | // it's expensive to lookup 662 | if (emValueInPixels) return emValueInPixels 663 | 664 | var html = document.documentElement 665 | , body = document.body 666 | , originalHTMLFontSize = html.style.fontSize 667 | , originalBodyFontSize = body.style.fontSize 668 | , div = document.createElement("div") 669 | 670 | // 1em is the value of the default font size of the browser 671 | // reset html and body to ensure the correct value is returned 672 | html.style.fontSize = "1em" 673 | body.style.fontSize = "1em" 674 | 675 | // add a test element and measure it 676 | body.appendChild(div) 677 | div.style.width = "1em" 678 | div.style.position = "absolute" 679 | emValueInPixels = div.offsetWidth 680 | 681 | // remove the test element and restore the previous values 682 | body.removeChild(div) 683 | body.style.fontSize = originalBodyFontSize 684 | html.style.fontSize = originalHTMLFontSize 685 | 686 | return emValueInPixels 687 | } 688 | 689 | /** 690 | * Use the browsers matchMedia function or existing shim 691 | */ 692 | function matchMediaNatively(query) { 693 | return window.matchMedia(query) 694 | } 695 | 696 | /** 697 | * Try to determine if a mediaQuery matches by 698 | * parsing the query and figuring it out manually 699 | * TODO: cache current width for repeated invocations 700 | */ 701 | function matchMediaManually(query) { 702 | var minWidth 703 | , maxWidth 704 | , matches = false 705 | 706 | // recalculate the width if it's not set 707 | // if (!currentWidth) currentWidth = document.documentElement.clientWidth 708 | currentWidth = document.documentElement.clientWidth 709 | 710 | // parse min and max widths from query 711 | if (reMinWidth.test(query)) { 712 | minWidth = RegExp.$2 === "em" 713 | ? parseFloat(RegExp.$1) * getEmValueInPixels() 714 | : parseFloat(RegExp.$1) 715 | } 716 | if (reMaxWidth.test(query)) { 717 | maxWidth = RegExp.$2 === "em" 718 | ? parseFloat(RegExp.$1) * getEmValueInPixels() 719 | : parseFloat(RegExp.$1) 720 | } 721 | 722 | // if both minWith and maxWidth are set 723 | if (minWidth && maxWidth) { 724 | matches = (minWidth <= currentWidth && maxWidth >= currentWidth) 725 | } else { 726 | if (minWidth && minWidth <= currentWidth) matches = true 727 | if (maxWidth && maxWidth >= currentWidth) matches = true 728 | } 729 | 730 | // return fake MediaQueryList object 731 | return { 732 | matches: matches, 733 | media: query 734 | } 735 | } 736 | 737 | 738 | return { 739 | /** 740 | * Similar to the window.matchMedia method 741 | * results are cached to avoid expensive relookups 742 | * @returns MediaQueryList (or a faked one) 743 | */ 744 | matchMedia: function(query) { 745 | return supports.matchMedia 746 | ? matchMediaNatively(query) 747 | : matchMediaManually(query) 748 | // return mediaQueryMap[query] || ( 749 | // mediaQueryMap[query] = supports.matchMedia 750 | // ? matchMediaNatively(query) 751 | // : matchMediaManually(query) 752 | // ) 753 | }, 754 | 755 | clearCache: function() { 756 | // we don't use cache when the browser supports matchMedia listeners 757 | if (!supports.nativeMatchMedia) { 758 | currentWidth = null 759 | mediaQueryMap = {} 760 | } 761 | } 762 | } 763 | 764 | }()) 765 | 766 | var EventManager = (function() { 767 | 768 | var MediaListener = (function() { 769 | var listeners = [] 770 | return { 771 | add: function(polyfill, mql, fn) { 772 | var listener 773 | , i = 0 774 | // if the listener is already in the array, return false 775 | while (listener = listeners[i++]) { 776 | if ( 777 | listener.polyfill == polyfill 778 | && listener.mql === mql 779 | && listener.fn === fn 780 | ) { 781 | return false 782 | } 783 | } 784 | // otherwise add it 785 | mql.addListener(fn) 786 | listeners.push({ 787 | polyfill: polyfill, 788 | mql: mql, 789 | fn: fn 790 | }) 791 | }, 792 | remove: function(polyfill) { 793 | var listener 794 | , i = 0 795 | while (listener = listeners[i++]) { 796 | if (listener.polyfill === polyfill) { 797 | listener.mql.removeListener(listener.fn) 798 | listeners.splice(--i, 1) 799 | } 800 | } 801 | } 802 | } 803 | }()) 804 | 805 | var ResizeListener = (function(listeners) { 806 | function onresize() { 807 | var listener 808 | , i = 0 809 | while (listener = listeners[i++]) { 810 | listener.fn() 811 | } 812 | } 813 | return { 814 | add: function(polyfill, fn) { 815 | if (!listeners.length) { 816 | if (window.addEventListener) { 817 | window.addEventListener("resize", onresize, false) 818 | } else { 819 | window.attachEvent("onresize", onresize) 820 | } 821 | } 822 | listeners.push({ 823 | polyfill: polyfill, 824 | fn: fn 825 | }) 826 | 827 | }, 828 | remove: function(polyfill) { 829 | var listener 830 | , i = 0 831 | while (listener = listeners[i++]) { 832 | if (listener.polyfill === polyfill) { 833 | listeners.splice(--i, 1) 834 | } 835 | } 836 | if (!listeners.length) { 837 | if (window.removeEventListener) { 838 | window.removeEventListener("resize", onresize, false) 839 | } else if (window.detachEvent) { 840 | window.detachEvent("onresize", onresize) 841 | } 842 | } 843 | } 844 | } 845 | }([])) 846 | 847 | 848 | /** 849 | * Simple debounce function 850 | */ 851 | function debounce(fn, wait) { 852 | var timeout 853 | return function() { 854 | clearTimeout(timeout) 855 | timeout = setTimeout(fn, wait) 856 | } 857 | } 858 | 859 | return { 860 | 861 | removeListeners: function(polyfill) { 862 | supports.nativeMatchMedia 863 | ? MediaListener.remove(polyfill) 864 | : ResizeListener.remove(polyfill) 865 | }, 866 | 867 | addListeners: function(polyfill, callback) { 868 | 869 | var queries = polyfill._mediaQueryMap 870 | , state = {} 871 | 872 | 873 | /** 874 | * Set up initial state 875 | */ 876 | ;(function() { 877 | for (var query in queries) { 878 | if (!queries.hasOwnProperty(query)) continue 879 | state[query] = MediaManager.matchMedia(query).matches 880 | } 881 | }()) 882 | 883 | /** 884 | * Register the listeners to detect media query changes 885 | * if the browser doesn't support this natively, use resize events instead 886 | */ 887 | function addListeners() { 888 | 889 | if (supports.nativeMatchMedia) { 890 | for (var query in queries) { 891 | if (queries.hasOwnProperty(query)) { 892 | // a closure is needed here to keep the variable reference 893 | (function(mql, query) { 894 | MediaListener.add(polyfill, mql, function() { 895 | callback.call(polyfill, query, mql.matches) 896 | }) 897 | }(queries[query], query)) 898 | } 899 | } 900 | } else { 901 | 902 | var fn = debounce((function(polyfill, queries) { 903 | return function() { 904 | updateMatchedMedia(polyfill, queries) 905 | } 906 | }(polyfill, queries)), polyfill._options.debounceTimeout || 100) 907 | 908 | ResizeListener.add(polyfill, fn) 909 | 910 | } 911 | } 912 | 913 | /** 914 | * Check each media query to see if it still matches 915 | * Note: this is only invoked when the browser doesn't 916 | * natively support window.matchMedia addListeners 917 | */ 918 | function updateMatchedMedia(polyfill, queries) { 919 | var query 920 | , current = {} 921 | 922 | // clear the cache since a resize just happened 923 | MediaManager.clearCache() 924 | 925 | // look for media matches that have changed since the last inspection 926 | for (query in queries) { 927 | if (!queries.hasOwnProperty(query)) continue 928 | current[query] = MediaManager.matchMedia(query).matches 929 | if (current[query] != state[query]) { 930 | callback.call(polyfill, query, MediaManager.matchMedia(query).matches) 931 | } 932 | } 933 | state = current 934 | } 935 | 936 | addListeners() 937 | 938 | } 939 | 940 | } 941 | 942 | }()) 943 | 944 | function Ruleset(rules) { 945 | var i = 0 946 | , rule 947 | this._rules = [] 948 | while (rule = rules[i++]) { 949 | this._rules.push(new Rule(rule)) 950 | } 951 | } 952 | 953 | Ruleset.prototype.each = function(iterator, context) { 954 | var rule 955 | , i = 0 956 | context || (context = this) 957 | while (rule = this._rules[i++]) { 958 | iterator.call(context, rule) 959 | } 960 | } 961 | 962 | Ruleset.prototype.size = function() { 963 | return this._rules.length 964 | } 965 | 966 | Ruleset.prototype.at = function(index) { 967 | return this._rules[index] 968 | } 969 | 970 | function Rule(rule) { 971 | this._rule = rule 972 | } 973 | 974 | Rule.prototype.getDeclaration = function() { 975 | var styles = {} 976 | , i = 0 977 | , declaration 978 | , declarations = this._rule.declarations 979 | while (declaration = declarations[i++]) { 980 | styles[declaration.property] = declaration.value 981 | } 982 | return styles 983 | } 984 | 985 | Rule.prototype.getSelectors = function() { 986 | return this._rule.selectors.join(", ") 987 | } 988 | 989 | Rule.prototype.getMedia = function() { 990 | return this._rule.media.join(" and ") 991 | } 992 | 993 | function Polyfill(options) { 994 | 995 | if (!(this instanceof Polyfill)) return new Polyfill(options) 996 | 997 | // set the options 998 | this._options = options 999 | 1000 | // allow the keywords option to be the only object passed 1001 | if (!options.keywords) this._options = { keywords: options } 1002 | 1003 | this._promise = [] 1004 | 1005 | // then do the stuff 1006 | this._getStylesheets() 1007 | this._downloadStylesheets() 1008 | this._parseStylesheets() 1009 | this._filterCSSByKeywords() 1010 | this._buildMediaQueryMap() 1011 | this._reportInitialMatches() 1012 | this._addMediaListeners() 1013 | } 1014 | 1015 | 1016 | /** 1017 | * Fired when the media change and new rules match 1018 | */ 1019 | Polyfill.prototype.doMatched = function(fn) { 1020 | this._doMatched = fn 1021 | this._resolve() 1022 | return this 1023 | } 1024 | 1025 | 1026 | /** 1027 | * Fired when the media changes and previously matching rules no longer match 1028 | */ 1029 | Polyfill.prototype.undoUnmatched = function(fn) { 1030 | this._undoUnmatched = fn 1031 | this._resolve() 1032 | return this 1033 | } 1034 | 1035 | 1036 | /** 1037 | * Get all the rules the match the current media 1038 | */ 1039 | Polyfill.prototype.getCurrentMatches = function() { 1040 | var i = 0 1041 | , rule 1042 | , media 1043 | , matches = [] 1044 | while (rule = this._filteredRules[i++]) { 1045 | // rules are considered matches if they they have 1046 | // no media query or the media query curently matches 1047 | media = rule.media && rule.media.join(" and ") 1048 | if (!media || MediaManager.matchMedia(media).matches) { 1049 | matches.push(rule) 1050 | } 1051 | } 1052 | return new Ruleset(matches) 1053 | } 1054 | 1055 | 1056 | /** 1057 | * Destroy the instance 1058 | * Remove any bound events and send all current 1059 | * matches to the callback as unmatches 1060 | */ 1061 | Polyfill.prototype.destroy = function() { 1062 | if (this._undoUnmatched) { 1063 | this._undoUnmatched(this.getCurrentMatches()) 1064 | EventManager.removeListeners(this) 1065 | } 1066 | return 1067 | } 1068 | 1069 | 1070 | /** 1071 | * Defer a task until after a condition is met 1072 | */ 1073 | Polyfill.prototype._defer = function(condition, callback) { 1074 | condition.call(this) 1075 | ? callback.call(this) 1076 | : this._promise.push({condition: condition, callback: callback}) 1077 | } 1078 | 1079 | 1080 | /** 1081 | * Invoke any functions that have been deferred 1082 | */ 1083 | Polyfill.prototype._resolve = function() { 1084 | var promise 1085 | , i = 0 1086 | while (promise = this._promise[i]) { 1087 | if (promise.condition.call(this)) { 1088 | this._promise.splice(i, 1) 1089 | promise.callback.call(this) 1090 | } else { 1091 | i++ 1092 | } 1093 | } 1094 | } 1095 | 1096 | 1097 | /** 1098 | * Get a list of tags in the head 1099 | * optionally filter by the include/exclude options 1100 | */ 1101 | Polyfill.prototype._getStylesheets = function() { 1102 | var i = 0 1103 | , id 1104 | , ids 1105 | , link 1106 | , links 1107 | , inline 1108 | , inlines 1109 | , stylesheet 1110 | , stylesheets = [] 1111 | 1112 | if (this._options.include) { 1113 | // get only the included stylesheets link tags 1114 | ids = this._options.include 1115 | while (id = ids[i++]) { 1116 | if (link = document.getElementById(id)) { 1117 | // if this tag is an inline style 1118 | if (link.nodeName === "STYLE") { 1119 | stylesheet = { text: link.textContent } 1120 | stylesheets.push(stylesheet) 1121 | continue 1122 | } 1123 | // ignore print stylesheets 1124 | if (link.media && link.media == "print") continue 1125 | // ignore non-local stylesheets 1126 | if (!isLocalURL(link.href)) continue 1127 | stylesheet = { href: link.href } 1128 | link.media && (stylesheet.media = link.media) 1129 | stylesheets.push(stylesheet) 1130 | } 1131 | } 1132 | } 1133 | else { 1134 | // otherwise get all the stylesheets stylesheets tags 1135 | // except the explicitely exluded ones 1136 | ids = this._options.exclude 1137 | links = document.getElementsByTagName( "link" ) 1138 | while (link = links[i++]) { 1139 | if ( 1140 | link.rel 1141 | && (link.rel == "stylesheet") 1142 | && (link.media != "print") // ignore print stylesheets 1143 | && (isLocalURL(link.href)) // only request local stylesheets 1144 | && (!inArray(link.id, ids)) 1145 | ) { 1146 | stylesheet = { href: link.href } 1147 | link.media && (stylesheet.media = link.media) 1148 | stylesheets.push(stylesheet) 1149 | } 1150 | } 1151 | inlines = document.getElementsByTagName('style'); 1152 | i = 0; 1153 | while (inline = inlines[i++]){ 1154 | stylesheet = { text: inline.textContent } 1155 | stylesheets.push(stylesheet); 1156 | } 1157 | } 1158 | return this._stylesheets = stylesheets 1159 | } 1160 | 1161 | 1162 | /** 1163 | * Download each stylesheet in the _stylesheetURLs array 1164 | */ 1165 | Polyfill.prototype._downloadStylesheets = function() { 1166 | var self = this 1167 | , stylesheet 1168 | , urls = [] 1169 | , i = 0 1170 | while (stylesheet = this._stylesheets[i++]) { 1171 | urls.push(stylesheet.href) 1172 | } 1173 | DownloadManager.request(urls, function(stylesheets) { 1174 | var stylesheet 1175 | , i = 0 1176 | while (stylesheet = stylesheets[i]) { 1177 | self._stylesheets[i++].text = stylesheet 1178 | } 1179 | self._resolve() 1180 | }) 1181 | } 1182 | 1183 | Polyfill.prototype._parseStylesheets = function() { 1184 | this._defer( 1185 | function() { 1186 | return this._stylesheets 1187 | && this._stylesheets.length 1188 | && this._stylesheets[0].text }, 1189 | function() { 1190 | var i = 0 1191 | , stylesheet 1192 | while (stylesheet = this._stylesheets[i++]) { 1193 | stylesheet.rules = StyleManager.parse(stylesheet.text, stylesheet.url) 1194 | } 1195 | } 1196 | ) 1197 | } 1198 | 1199 | Polyfill.prototype._filterCSSByKeywords = function() { 1200 | this._defer( 1201 | function() { 1202 | return this._stylesheets 1203 | && this._stylesheets.length 1204 | && this._stylesheets[0].rules 1205 | }, 1206 | function() { 1207 | var stylesheet 1208 | , media 1209 | , rules = [] 1210 | , i = 0 1211 | while (stylesheet = this._stylesheets[i++]) { 1212 | media = stylesheet.media 1213 | // Treat stylesheets with a media attribute as being contained inside 1214 | // a single @media block, but ignore `all` and `screen` media values 1215 | // since they're basically meaningless in this context 1216 | if (media && media != "all" && media != "screen") { 1217 | rules.push({rules: stylesheet.rules, media: stylesheet.media}) 1218 | } else { 1219 | rules = rules.concat(stylesheet.rules) 1220 | } 1221 | } 1222 | this._filteredRules = StyleManager.filter(rules, this._options.keywords) 1223 | } 1224 | ) 1225 | } 1226 | 1227 | Polyfill.prototype._buildMediaQueryMap = function() { 1228 | this._defer( 1229 | function() { return this._filteredRules }, 1230 | function() { 1231 | var i = 0 1232 | , media 1233 | , rule 1234 | this._mediaQueryMap = {} 1235 | while (rule = this._filteredRules[i++]) { 1236 | if (rule.media) { 1237 | media = rule.media.join(" and ") 1238 | this._mediaQueryMap[media] = MediaManager.matchMedia(media) 1239 | } 1240 | } 1241 | } 1242 | ) 1243 | } 1244 | 1245 | Polyfill.prototype._reportInitialMatches = function() { 1246 | this._defer( 1247 | function() { 1248 | return this._filteredRules && this._doMatched 1249 | }, 1250 | function() { 1251 | this._doMatched(this.getCurrentMatches()) 1252 | } 1253 | ) 1254 | } 1255 | 1256 | Polyfill.prototype._addMediaListeners = function() { 1257 | this._defer( 1258 | function() { 1259 | return this._filteredRules 1260 | && this._doMatched 1261 | && this._undoUnmatched 1262 | }, 1263 | function() { 1264 | EventManager.addListeners( 1265 | this, 1266 | function(query, isMatch) { 1267 | var i = 0 1268 | , rule 1269 | , matches = [] 1270 | , unmatches = [] 1271 | while (rule = this._filteredRules[i++]) { 1272 | if (rule.media && rule.media.join(" and ") == query) { 1273 | (isMatch ? matches : unmatches).push(rule) 1274 | } 1275 | } 1276 | matches.length && this._doMatched(new Ruleset(matches)) 1277 | unmatches.length && this._undoUnmatched(new Ruleset(unmatches)) 1278 | } 1279 | ) 1280 | } 1281 | ) 1282 | } 1283 | 1284 | Polyfill.modules = { 1285 | DownloadManager: DownloadManager, 1286 | StyleManager: StyleManager, 1287 | MediaManager: MediaManager, 1288 | EventManager: EventManager 1289 | } 1290 | Polyfill.constructors = { 1291 | Ruleset: Ruleset, 1292 | Rule: Rule 1293 | } 1294 | 1295 | window.Polyfill = Polyfill 1296 | 1297 | }(window, document)); --------------------------------------------------------------------------------