diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index ee6e1a4..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = function(grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - jshint: { all: ['js/*.js'] }, - concat: { - dist: { - src: [ - 'js/libs/*', - 'js/srcset-info.js', - 'js/viewport-info.js', - 'js/main.js' - ], - dest: 'build/srcset.js' - } - }, - uglify: { - my_target: { - files: { - 'build/srcset.min.js': ['build/srcset.js'] - } - } - } - }); - - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - - grunt.registerTask('default', ['jshint', 'concat', 'uglify']); -}; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c66060 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: build/srcset.js build/srcset.min.js + +build/srcset.js: ./node_modules/.bin/browserify $(shell find -L js -name '*.js') + ./node_modules/.bin/browserify ./js/srcset.js > $@ + +build/srcset.min.js: build/srcset.js + ./node_modules/.bin/uglifyjs $^ -o $@ -m + +node_modules ./node_modules/%: + npm install \ No newline at end of file diff --git a/README.md b/README.md index 4b43035..09411a7 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,48 @@ See [the specification][spec] for the reference algorithm. +## INSTALL + +with NPM: + +```sh +npm install https://github.com/abernier/srcset-polyfill/archive/patch-abernier-bunchofthings.tar.gz +``` + +or with a plain old ` +``` + +or with the minified version: + +```html + +``` + ## Usage -Use the `srcset` attribute of `` elements. For example: +Use the `data-srcset` attribute of `` elements. For example: + +```html +The Breakfast Combo +``` + +Include `build/srcset.min.js` in your page. Then, you'll have a `srcset` object with the following API : - The Breakfast Combo + - `srcset.update()` -- update all images in the page + - `srcset.imgs.get().update()` -- update one image +A `'srcchange'` event will also be triggered when the `src` of an image changes : -Include `build/srcset.min.js` in your page. +```javascript +myimg.addEventListener('srcchanged', function (data) { + console.log("img with previous src %s was changed to %s", data.previous, data.actual) +}, false); +``` ## Open questions diff --git a/build/srcset.js b/build/srcset.js index c716112..a130902 100644 --- a/build/srcset.js +++ b/build/srcset.js @@ -1,478 +1,5 @@ -/*! - * jsUri v1.1.1 - * https://github.com/derek-watson/jsUri - * - * Copyright 2011, Derek Watson - * Released under the MIT license. - * http://jquery.org/license - * - * Includes parseUri regular expressions - * http://blog.stevenlevithan.com/archives/parseuri - * Copyright 2007, Steven Levithan - * Released under the MIT license. - * - * Date: Mon Nov 14 20:06:34 2011 -0800 - */ - - -var Query = function (queryString) { - - // query string parsing, parameter manipulation and stringification - - 'use strict'; - - var // parseQuery(q) parses the uri query string and returns a multi-dimensional array of the components - parseQuery = function (q) { - var arr = [], i, ps, p, keyval; - - if (typeof (q) === 'undefined' || q === null || q === '') { - return arr; - } - - if (q.indexOf('?') === 0) { - q = q.substring(1); - } - - ps = q.toString().split(/[&;]/); - - for (i = 0; i < ps.length; i++) { - p = ps[i]; - keyval = p.split('='); - arr.push([keyval[0], keyval[1]]); - } - - return arr; - }, - - params = parseQuery(queryString), - - // toString() returns a string representation of the internal state of the object - toString = function () { - var s = '', i, param; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (s.length > 0) { - s += '&'; - } - s += param.join('='); - } - return s.length > 0 ? '?' + s : s; - }, - - decode = function (s) { - s = decodeURIComponent(s); - s = s.replace('+', ' '); - return s; - }, - - // getParamValues(key) returns the first query param value found for the key 'key' - getParamValue = function (key) { - var param, i; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(key) === decode(param[0])) { - return param[1]; - } - } - }, - - // getParamValues(key) returns an array of query param values for the key 'key' - getParamValues = function (key) { - var arr = [], i, param; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(key) === decode(param[0])) { - arr.push(param[1]); - } - } - return arr; - }, - - // deleteParam(key) removes all instances of parameters named (key) - // deleteParam(key, val) removes all instances where the value matches (val) - deleteParam = function (key, val) { - - var arr = [], i, param, keyMatchesFilter, valMatchesFilter; - - for (i = 0; i < params.length; i++) { - - param = params[i]; - keyMatchesFilter = decode(param[0]) === decode(key); - valMatchesFilter = decode(param[1]) === decode(val); - - if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && !keyMatchesFilter && !valMatchesFilter)) { - arr.push(param); - } - } - - params = arr; - - return this; - }, - - // addParam(key, val) Adds an element to the end of the list of query parameters - // addParam(key, val, index) adds the param at the specified position (index) - addParam = function (key, val, index) { - - if (arguments.length === 3 && index !== -1) { - index = Math.min(index, params.length); - params.splice(index, 0, [key, val]); - } else if (arguments.length > 0) { - params.push([key, val]); - } - return this; - }, - - // replaceParam(key, newVal) deletes all instances of params named (key) and replaces them with the new single value - // replaceParam(key, newVal, oldVal) deletes only instances of params named (key) with the value (val) and replaces them with the new single value - // this function attempts to preserve query param ordering - replaceParam = function (key, newVal, oldVal) { - - var index = -1, i, param; - - if (arguments.length === 3) { - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) { - index = i; - break; - } - } - deleteParam(key, oldVal).addParam(key, newVal, index); - } else { - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(param[0]) === decode(key)) { - index = i; - break; - } - } - deleteParam(key); - addParam(key, newVal, index); - } - return this; - }; - - // public api - return { - getParamValue: getParamValue, - getParamValues: getParamValues, - deleteParam: deleteParam, - addParam: addParam, - replaceParam: replaceParam, - - toString: toString - }; -}; - -var Uri = function (uriString) { - - // uri string parsing, attribute manipulation and stringification - - 'use strict'; - - /*global Query: true */ - /*jslint regexp: false, plusplus: false */ - - var strictMode = false, - - // parseUri(str) parses the supplied uri and returns an object containing its components - parseUri = function (str) { - - /*jslint unparam: true */ - var parsers = { - strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ - }, - keys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], - q = { - name: "queryKey", - parser: /(?:^|&)([^&=]*)=?([^&]*)/g - }, - m = parsers[strictMode ? "strict" : "loose"].exec(str), - uri = {}, - i = 14; - - while (i--) { - uri[keys[i]] = m[i] || ""; - } - - uri[q.name] = {}; - uri[keys[12]].replace(q.parser, function ($0, $1, $2) { - if ($1) { - uri[q.name][$1] = $2; - } - }); - - return uri; - }, - - uriParts = parseUri(uriString || ''), - - queryObj = new Query(uriParts.query), - - - /* - Basic get/set functions for all properties - */ - - protocol = function (val) { - if (typeof val !== 'undefined') { - uriParts.protocol = val; - } - return uriParts.protocol; - }, - - hasAuthorityPrefixUserPref = null, - - // hasAuthorityPrefix: if there is no protocol, the leading // can be enabled or disabled - hasAuthorityPrefix = function (val) { - - if (typeof val !== 'undefined') { - hasAuthorityPrefixUserPref = val; - } - - if (hasAuthorityPrefixUserPref === null) { - return (uriParts.source.indexOf('//') !== -1); - } else { - return hasAuthorityPrefixUserPref; - } - }, - - userInfo = function (val) { - if (typeof val !== 'undefined') { - uriParts.userInfo = val; - } - return uriParts.userInfo; - }, - - host = function (val) { - if (typeof val !== 'undefined') { - uriParts.host = val; - } - return uriParts.host; - }, - - port = function (val) { - if (typeof val !== 'undefined') { - uriParts.port = val; - } - return uriParts.port; - }, - - path = function (val) { - if (typeof val !== 'undefined') { - uriParts.path = val; - } - return uriParts.path; - }, - - query = function (val) { - if (typeof val !== 'undefined') { - queryObj = new Query(val); - } - return queryObj; - }, - - anchor = function (val) { - if (typeof val !== 'undefined') { - uriParts.anchor = val; - } - return uriParts.anchor; - }, - - - /* - Fluent setters for Uri uri properties - */ - - setProtocol = function (val) { - protocol(val); - return this; - }, - - setHasAuthorityPrefix = function (val) { - hasAuthorityPrefix(val); - return this; - }, - - setUserInfo = function (val) { - userInfo(val); - return this; - }, - - setHost = function (val) { - host(val); - return this; - }, - - setPort = function (val) { - port(val); - return this; - }, - - setPath = function (val) { - path(val); - return this; - }, - - setQuery = function (val) { - query(val); - return this; - }, - - setAnchor = function (val) { - anchor(val); - return this; - }, - - /* - Query method wrappers - */ - getQueryParamValue = function (key) { - return query().getParamValue(key); - }, - - getQueryParamValues = function (key) { - return query().getParamValues(key); - }, - - deleteQueryParam = function (key, val) { - if (arguments.length === 2) { - query().deleteParam(key, val); - } else { - query().deleteParam(key); - } - - return this; - }, - - addQueryParam = function (key, val, index) { - if (arguments.length === 3) { - query().addParam(key, val, index); - } else { - query().addParam(key, val); - } - return this; - }, - - replaceQueryParam = function (key, newVal, oldVal) { - if (arguments.length === 3) { - query().replaceParam(key, newVal, oldVal); - } else { - query().replaceParam(key, newVal); - } - - return this; - }, - - /* - Serialization - */ - - // toString() stringifies the current state of the uri - toString = function () { - - var s = '', - is = function (s) { - return (s !== null && s !== ''); - }; - - if (is(protocol())) { - s += protocol(); - if (protocol().indexOf(':') !== protocol().length - 1) { - s += ':'; - } - s += '//'; - } else { - if (hasAuthorityPrefix() && is(host())) { - s += '//'; - } - } - - if (is(userInfo()) && is(host())) { - s += userInfo(); - if (userInfo().indexOf('@') !== userInfo().length - 1) { - s += '@'; - } - } - - if (is(host())) { - s += host(); - if (is(port())) { - s += ':' + port(); - } - } - - if (is(path())) { - s += path(); - } else { - if (is(host()) && (is(query().toString()) || is(anchor()))) { - s += '/'; - } - } - if (is(query().toString())) { - if (query().toString().indexOf('?') !== 0) { - s += '?'; - } - s += query().toString(); - } - - if (is(anchor())) { - if (anchor().indexOf('#') !== 0) { - s += '#'; - } - s += anchor(); - } - - return s; - }, - - /* - Cloning - */ - - // clone() returns a new, identical Uri instance - clone = function () { - return new Uri(toString()); - }; - - // public api - return { - - protocol: protocol, - hasAuthorityPrefix: hasAuthorityPrefix, - userInfo: userInfo, - host: host, - port: port, - path: path, - query: query, - anchor: anchor, - - setProtocol: setProtocol, - setHasAuthorityPrefix: setHasAuthorityPrefix, - setUserInfo: setUserInfo, - setHost: setHost, - setPort: setPort, - setPath: setPath, - setQuery: setQuery, - setAnchor: setAnchor, - - getQueryParamValue: getQueryParamValue, - getQueryParamValues: getQueryParamValues, - deleteQueryParam: deleteQueryParam, - addQueryParam: addQueryParam, - replaceQueryParam: replaceQueryParam, - - toString: toString, - clone: clone - }; -}; - -/* add compatibility for users of jsUri <= 1.1.1 */ -var jsUri = Uri; - -(function(exports) { +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = new Date().getTime(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + } + + // https://gist.github.com/stucox/5231211 + var hasMO = (function () { + var prefixes = ['WebKit', 'Moz', 'O', 'Ms', '']; + + for (var i=0; i < prefixes.length; i++) { + if ((prefixes[i] + 'MutationObserver') in window) { + return window[prefixes[i] + 'MutationObserver']; + } + } + + return false; + }()); + + function SrcsetView(el) { + this.el = el; + + this.srcsetInfo = new SrcsetInfo({ + src: this.el.src, + srcset: this.el.dataset.srcset + }); -(function(exports) { + // + // Observe data-srcset attributes mutations to keep this.srcsetInfo up-to-date (if available) + // + + if (hasMO) { + this.mo = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + //console.log(mutation); + + if (mutation.target === this.el && mutation.type === 'attributes') { + if (mutation.attributeName === 'src' || mutation.attributeName === 'data-srcset') { + this.update(); + } + } + }.bind(this)); + }.bind(this)); + + this.mo.observe(this.el, {attributes: true}); + } + } + SrcsetView.prototype.update = function (options) { + options || (options = {}); + /*options = $.extend({}, options, { + force: false + });*/ + + if (this.srcsetInfo.srcValue !== this.el.src) { + this.srcsetInfo.srcValue = this.el.src; + } + + var srcsetchanged; + if (this.srcsetInfo.srcsetValue !== this.el.dataset.srcset) { + srcsetchanged = true; + + this.srcsetInfo.imageCandidates = []; // reset imageCandidates + this.srcsetInfo.srcsetValue = this.el.dataset.srcset; + this.srcsetInfo._parse(this.srcsetInfo.srcsetValue); + if (!this.srcsetInfo.isValid) { + console.error('Error: ' + this.srcsetInfo.error); + } + } + + var needUpdate = (!this.srcupdatedat || this.srcupdatedat < windowResizedAt); + if (!this.el.src || needUpdate || srcsetchanged || options.force) { + + if (this.srcsetInfo) { + var bestImageInfo = viewportInfo.getBestImage(this.srcsetInfo); + + var oldsrc = this.el.src; + var newsrc = bestImageInfo.src; + + if (newsrc === oldsrc) return false; // same, no need to update + + //console.log('updating src', this.el, oldsrc, newsrc); + + // + // 'srcchanged' event + // + + var srcchanged = new CustomEvent('srcchanged', { + detail: { + previous: oldsrc, + actual: newsrc + }, + bubbles: true + }); + + this.el.src = newsrc; + // Dispatch 'srcchanged' + setTimeout(function () { + this.el.dispatchEvent(srcchanged); + }.bind(this), 0); + } + + // Remember when updated to compare with window's resizeAt timestamp + this.srcupdatedat = (new Date).getTime(); + } + }; + + var srcsetViews = new WeakMap(); + srcset.imgs = srcsetViews; + function update() { + // update timestamp + windowResizedAt = (new Date).getTime(); + viewportInfo.compute(); + + // Update every images + [].forEach.call(document.querySelectorAll('img[data-srcset]'), function (el) { + var srcsetview = srcsetViews.get(el); + if (!srcsetview) { + srcsetview = new SrcsetView(el); + srcsetViews.set(el, srcsetview); + } + + srcsetview.update(); + }); + } + window.onresize = debounce(update, 200); + update(); + srcset.update = update; + + // Exports + this.srcset = srcset; + if (typeof module !== "undefined" && module !== null) { + module.exports = this.srcset; + } +})(this); + +},{"./srcset-info":1,"./viewport-info":3,"weak-map":4}],3:[function(require,module,exports){ +(function() { + var SrcsetInfo = this.SrcsetInfo || require('./srcset-info'); function ViewportInfo() { this.w = null; @@ -754,57 +465,699 @@ var jsUri = Uri; return bestMatch; }; - exports.ViewportInfo = ViewportInfo; + // Exports + this.ViewportInfo = ViewportInfo; + if (typeof module !== "undefined" && module !== null) { + module.exports = this.ViewportInfo; + } -})(window); +})(this); + +},{"./srcset-info":1}],4:[function(require,module,exports){ +// Copyright (C) 2011 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Install a leaky WeakMap emulation on platforms that + * don't provide a built-in one. + * + *

Assumes that an ES5 platform where, if {@code WeakMap} is + * already present, then it conforms to the anticipated ES6 + * specification. To run this file on an ES5 or almost ES5 + * implementation where the {@code WeakMap} specification does not + * quite conform, run repairES5.js first. + * + *

Even though WeakMapModule is not global, the linter thinks it + * is, which is why it is in the overrides list below. + * + *

NOTE: Before using this WeakMap emulation in a non-SES + * environment, see the note below about hiddenRecord. + * + * @author Mark S. Miller + * @requires crypto, ArrayBuffer, Uint8Array, navigator, console + * @overrides WeakMap, ses, Proxy + * @overrides WeakMapModule + */ -(function(exports) { +/** + * This {@code WeakMap} emulation is observably equivalent to the + * ES-Harmony WeakMap, but with leakier garbage collection properties. + * + *

As with true WeakMaps, in this emulation, a key does not + * retain maps indexed by that key and (crucially) a map does not + * retain the keys it indexes. A map by itself also does not retain + * the values associated with that map. + * + *

However, the values associated with a key in some map are + * retained so long as that key is retained and those associations are + * not overridden. For example, when used to support membranes, all + * values exported from a given membrane will live for the lifetime + * they would have had in the absence of an interposed membrane. Even + * when the membrane is revoked, all objects that would have been + * reachable in the absence of revocation will still be reachable, as + * far as the GC can tell, even though they will no longer be relevant + * to ongoing computation. + * + *

The API implemented here is approximately the API as implemented + * in FF6.0a1 and agreed to by MarkM, Andreas Gal, and Dave Herman, + * rather than the offially approved proposal page. TODO(erights): + * upgrade the ecmascript WeakMap proposal page to explain this API + * change and present to EcmaScript committee for their approval. + * + *

The first difference between the emulation here and that in + * FF6.0a1 is the presence of non enumerable {@code get___, has___, + * set___, and delete___} methods on WeakMap instances to represent + * what would be the hidden internal properties of a primitive + * implementation. Whereas the FF6.0a1 WeakMap.prototype methods + * require their {@code this} to be a genuine WeakMap instance (i.e., + * an object of {@code [[Class]]} "WeakMap}), since there is nothing + * unforgeable about the pseudo-internal method names used here, + * nothing prevents these emulated prototype methods from being + * applied to non-WeakMaps with pseudo-internal methods of the same + * names. + * + *

Another difference is that our emulated {@code + * WeakMap.prototype} is not itself a WeakMap. A problem with the + * current FF6.0a1 API is that WeakMap.prototype is itself a WeakMap + * providing ambient mutability and an ambient communications + * channel. Thus, if a WeakMap is already present and has this + * problem, repairES5.js wraps it in a safe wrappper in order to + * prevent access to this channel. (See + * PATCH_MUTABLE_FROZEN_WEAKMAP_PROTO in repairES5.js). + */ - function isSrcsetImplemented() { - var img = new Image(); - return 'srcset' in img; +/** + * If this is a full secureable ES5 platform and the ES-Harmony {@code WeakMap} is + * absent, install an approximate emulation. + * + *

If WeakMap is present but cannot store some objects, use our approximate + * emulation as a wrapper. + * + *

If this is almost a secureable ES5 platform, then WeakMap.js + * should be run after repairES5.js. + * + *

See {@code WeakMap} for documentation of the garbage collection + * properties of this WeakMap emulation. + */ +(function WeakMapModule() { + "use strict"; + + if (typeof ses !== 'undefined' && ses.ok && !ses.ok()) { + // already too broken, so give up + return; } - function main() { - // If the browser supports @srcset natively, don't do any polyfill. - if (isSrcsetImplemented()) { - return; + /** + * In some cases (current Firefox), we must make a choice betweeen a + * WeakMap which is capable of using all varieties of host objects as + * keys and one which is capable of safely using proxies as keys. See + * comments below about HostWeakMap and DoubleWeakMap for details. + * + * This function (which is a global, not exposed to guests) marks a + * WeakMap as permitted to do what is necessary to index all host + * objects, at the cost of making it unsafe for proxies. + * + * Do not apply this function to anything which is not a genuine + * fresh WeakMap. + */ + function weakMapPermitHostObjects(map) { + // identity of function used as a secret -- good enough and cheap + if (map.permitHostObjects___) { + map.permitHostObjects___(weakMapPermitHostObjects); } + } + if (typeof ses !== 'undefined') { + ses.weakMapPermitHostObjects = weakMapPermitHostObjects; + } - // Get the user agent's capabilities (viewport width, viewport height, dPR). - var viewportInfo = new ViewportInfo(); - viewportInfo.compute(); - // Go through all images on the page. - var images = document.querySelectorAll('img'); - // If they have srcset attributes, apply JS to handle that correctly. - for (var i = 0; i < images.length; i++) { - var img = images[i]; - // Parse the srcset from the image element. - var srcset = img.getAttribute('srcset'); - if (srcset) { - var srcsetInfo = new SrcsetInfo({src: img.src, - srcset: srcset}); - // Go through all the candidates, pick the best one that matches. - var imageInfo = viewportInfo.getBestImage(srcsetInfo); - // TODO: consider using -webkit-image-set instead (if available). - // Replace the with this image. - img.src = imageInfo.src; - // If there's no set size, then we scale the image if necessary - // (e.g. x != 1) - if (!(img.width || img.height || img.style.height || img.style.width)) { - img.style.webkitTransform = 'scale(' + (1/imageInfo.x) + ')'; - img.style.webkitTransformOrigin = '0 0'; - } + // IE 11 has no Proxy but has a broken WeakMap such that we need to patch + // it using DoubleWeakMap; this flag tells DoubleWeakMap so. + var doubleWeakMapCheckSilentFailure = false; + + // Check if there is already a good-enough WeakMap implementation, and if so + // exit without replacing it. + if (typeof WeakMap === 'function') { + var HostWeakMap = WeakMap; + // There is a WeakMap -- is it good enough? + if (typeof navigator !== 'undefined' && + /Firefox/.test(navigator.userAgent)) { + // We're now *assuming not*, because as of this writing (2013-05-06) + // Firefox's WeakMaps have a miscellany of objects they won't accept, and + // we don't want to make an exhaustive list, and testing for just one + // will be a problem if that one is fixed alone (as they did for Event). + + // If there is a platform that we *can* reliably test on, here's how to + // do it: + // var problematic = ... ; + // var testHostMap = new HostWeakMap(); + // try { + // testHostMap.set(problematic, 1); // Firefox 20 will throw here + // if (testHostMap.get(problematic) === 1) { + // return; + // } + // } catch (e) {} + + } else { + // IE 11 bug: WeakMaps silently fail to store frozen objects. + var testMap = new HostWeakMap(); + var testObject = Object.freeze({}); + testMap.set(testObject, 1); + if (testMap.get(testObject) !== 1) { + doubleWeakMapCheckSilentFailure = true; + // Fall through to installing our WeakMap. + } else { + module.exports = WeakMap; + return; + } + } + } + + var hop = Object.prototype.hasOwnProperty; + var gopn = Object.getOwnPropertyNames; + var defProp = Object.defineProperty; + var isExtensible = Object.isExtensible; + + /** + * Security depends on HIDDEN_NAME being both unguessable and + * undiscoverable by untrusted code. + * + *

Given the known weaknesses of Math.random() on existing + * browsers, it does not generate unguessability we can be confident + * of. + * + *

It is the monkey patching logic in this file that is intended + * to ensure undiscoverability. The basic idea is that there are + * three fundamental means of discovering properties of an object: + * The for/in loop, Object.keys(), and Object.getOwnPropertyNames(), + * as well as some proposed ES6 extensions that appear on our + * whitelist. The first two only discover enumerable properties, and + * we only use HIDDEN_NAME to name a non-enumerable property, so the + * only remaining threat should be getOwnPropertyNames and some + * proposed ES6 extensions that appear on our whitelist. We monkey + * patch them to remove HIDDEN_NAME from the list of properties they + * returns. + * + *

TODO(erights): On a platform with built-in Proxies, proxies + * could be used to trap and thereby discover the HIDDEN_NAME, so we + * need to monkey patch Proxy.create, Proxy.createFunction, etc, in + * order to wrap the provided handler with the real handler which + * filters out all traps using HIDDEN_NAME. + * + *

TODO(erights): Revisit Mike Stay's suggestion that we use an + * encapsulated function at a not-necessarily-secret name, which + * uses the Stiegler shared-state rights amplification pattern to + * reveal the associated value only to the WeakMap in which this key + * is associated with that value. Since only the key retains the + * function, the function can also remember the key without causing + * leakage of the key, so this doesn't violate our general gc + * goals. In addition, because the name need not be a guarded + * secret, we could efficiently handle cross-frame frozen keys. + */ + var HIDDEN_NAME_PREFIX = 'weakmap:'; + var HIDDEN_NAME = HIDDEN_NAME_PREFIX + 'ident:' + Math.random() + '___'; + + if (typeof crypto !== 'undefined' && + typeof crypto.getRandomValues === 'function' && + typeof ArrayBuffer === 'function' && + typeof Uint8Array === 'function') { + var ab = new ArrayBuffer(25); + var u8s = new Uint8Array(ab); + crypto.getRandomValues(u8s); + HIDDEN_NAME = HIDDEN_NAME_PREFIX + 'rand:' + + Array.prototype.map.call(u8s, function(u8) { + return (u8 % 36).toString(36); + }).join('') + '___'; + } + + function isNotHiddenName(name) { + return !( + name.substr(0, HIDDEN_NAME_PREFIX.length) == HIDDEN_NAME_PREFIX && + name.substr(name.length - 3) === '___'); + } + + /** + * Monkey patch getOwnPropertyNames to avoid revealing the + * HIDDEN_NAME. + * + *

The ES5.1 spec requires each name to appear only once, but as + * of this writing, this requirement is controversial for ES6, so we + * made this code robust against this case. If the resulting extra + * search turns out to be expensive, we can probably relax this once + * ES6 is adequately supported on all major browsers, iff no browser + * versions we support at that time have relaxed this constraint + * without providing built-in ES6 WeakMaps. + */ + defProp(Object, 'getOwnPropertyNames', { + value: function fakeGetOwnPropertyNames(obj) { + return gopn(obj).filter(isNotHiddenName); + } + }); + + /** + * getPropertyNames is not in ES5 but it is proposed for ES6 and + * does appear in our whitelist, so we need to clean it too. + */ + if ('getPropertyNames' in Object) { + var originalGetPropertyNames = Object.getPropertyNames; + defProp(Object, 'getPropertyNames', { + value: function fakeGetPropertyNames(obj) { + return originalGetPropertyNames(obj).filter(isNotHiddenName); + } + }); + } + + /** + *

To treat objects as identity-keys with reasonable efficiency + * on ES5 by itself (i.e., without any object-keyed collections), we + * need to add a hidden property to such key objects when we + * can. This raises several issues: + *

+ * We do so by + * + * Unfortunately, because of same-origin iframes, we cannot reliably + * add this hidden property before an object becomes + * non-extensible. Instead, if we encounter a non-extensible object + * without a hidden record that we can detect (whether or not it has + * a hidden record stored under a name secret to us), then we just + * use the key object itself to represent its identity in a brute + * force leaky map stored in the weak map, losing all the advantages + * of weakness for these. + */ + function getHiddenRecord(key) { + if (key !== Object(key)) { + throw new TypeError('Not an object: ' + key); + } + var hiddenRecord = key[HIDDEN_NAME]; + if (hiddenRecord && hiddenRecord.key === key) { return hiddenRecord; } + if (!isExtensible(key)) { + // Weak map must brute force, as explained in doc-comment above. + return void 0; + } + + // The hiddenRecord and the key point directly at each other, via + // the "key" and HIDDEN_NAME properties respectively. The key + // field is for quickly verifying that this hidden record is an + // own property, not a hidden record from up the prototype chain. + // + // NOTE: Because this WeakMap emulation is meant only for systems like + // SES where Object.prototype is frozen without any numeric + // properties, it is ok to use an object literal for the hiddenRecord. + // This has two advantages: + // * It is much faster in a performance critical place + // * It avoids relying on Object.create(null), which had been + // problematic on Chrome 28.0.1480.0. See + // https://code.google.com/p/google-caja/issues/detail?id=1687 + hiddenRecord = { key: key }; + + // When using this WeakMap emulation on platforms where + // Object.prototype might not be frozen and Object.create(null) is + // reliable, use the following two commented out lines instead. + // hiddenRecord = Object.create(null); + // hiddenRecord.key = key; + + // Please contact us if you need this to work on platforms where + // Object.prototype might not be frozen and + // Object.create(null) might not be reliable. + + try { + defProp(key, HIDDEN_NAME, { + value: hiddenRecord, + writable: false, + enumerable: false, + configurable: false + }); + return hiddenRecord; + } catch (error) { + // Under some circumstances, isExtensible seems to misreport whether + // the HIDDEN_NAME can be defined. + // The circumstances have not been isolated, but at least affect + // Node.js v0.10.26 on TravisCI / Linux, but not the same version of + // Node.js on OS X. + return void 0; + } + } + + /** + * Monkey patch operations that would make their argument + * non-extensible. + * + *

The monkey patched versions throw a TypeError if their + * argument is not an object, so it should only be done to functions + * that should throw a TypeError anyway if their argument is not an + * object. + */ + (function(){ + var oldFreeze = Object.freeze; + defProp(Object, 'freeze', { + value: function identifyingFreeze(obj) { + getHiddenRecord(obj); + return oldFreeze(obj); + } + }); + var oldSeal = Object.seal; + defProp(Object, 'seal', { + value: function identifyingSeal(obj) { + getHiddenRecord(obj); + return oldSeal(obj); } + }); + var oldPreventExtensions = Object.preventExtensions; + defProp(Object, 'preventExtensions', { + value: function identifyingPreventExtensions(obj) { + getHiddenRecord(obj); + return oldPreventExtensions(obj); + } + }); + })(); + + function constFunc(func) { + func.prototype = null; + return Object.freeze(func); + } + + var calledAsFunctionWarningDone = false; + function calledAsFunctionWarning() { + // Future ES6 WeakMap is currently (2013-09-10) expected to reject WeakMap() + // but we used to permit it and do it ourselves, so warn only. + if (!calledAsFunctionWarningDone && typeof console !== 'undefined') { + calledAsFunctionWarningDone = true; + console.warn('WeakMap should be invoked as new WeakMap(), not ' + + 'WeakMap(). This will be an error in the future.'); } } - // Small cross browser document ready. - var readyTimer = setInterval(function () { - if (document.readyState === "complete") { - main(); - clearInterval(readyTimer); + var nextId = 0; + + var OurWeakMap = function() { + if (!(this instanceof OurWeakMap)) { // approximate test for new ...() + calledAsFunctionWarning(); } - }, 10); -})(window); + // We are currently (12/25/2012) never encountering any prematurely + // non-extensible keys. + var keys = []; // brute force for prematurely non-extensible keys. + var values = []; // brute force for corresponding values. + var id = nextId++; + + function get___(key, opt_default) { + var index; + var hiddenRecord = getHiddenRecord(key); + if (hiddenRecord) { + return id in hiddenRecord ? hiddenRecord[id] : opt_default; + } else { + index = keys.indexOf(key); + return index >= 0 ? values[index] : opt_default; + } + } + + function has___(key) { + var hiddenRecord = getHiddenRecord(key); + if (hiddenRecord) { + return id in hiddenRecord; + } else { + return keys.indexOf(key) >= 0; + } + } + + function set___(key, value) { + var index; + var hiddenRecord = getHiddenRecord(key); + if (hiddenRecord) { + hiddenRecord[id] = value; + } else { + index = keys.indexOf(key); + if (index >= 0) { + values[index] = value; + } else { + // Since some browsers preemptively terminate slow turns but + // then continue computing with presumably corrupted heap + // state, we here defensively get keys.length first and then + // use it to update both the values and keys arrays, keeping + // them in sync. + index = keys.length; + values[index] = value; + // If we crash here, values will be one longer than keys. + keys[index] = key; + } + } + return this; + } + + function delete___(key) { + var hiddenRecord = getHiddenRecord(key); + var index, lastIndex; + if (hiddenRecord) { + return id in hiddenRecord && delete hiddenRecord[id]; + } else { + index = keys.indexOf(key); + if (index < 0) { + return false; + } + // Since some browsers preemptively terminate slow turns but + // then continue computing with potentially corrupted heap + // state, we here defensively get keys.length first and then use + // it to update both the keys and the values array, keeping + // them in sync. We update the two with an order of assignments, + // such that any prefix of these assignments will preserve the + // key/value correspondence, either before or after the delete. + // Note that this needs to work correctly when index === lastIndex. + lastIndex = keys.length - 1; + keys[index] = void 0; + // If we crash here, there's a void 0 in the keys array, but + // no operation will cause a "keys.indexOf(void 0)", since + // getHiddenRecord(void 0) will always throw an error first. + values[index] = values[lastIndex]; + // If we crash here, values[index] cannot be found here, + // because keys[index] is void 0. + keys[index] = keys[lastIndex]; + // If index === lastIndex and we crash here, then keys[index] + // is still void 0, since the aliasing killed the previous key. + keys.length = lastIndex; + // If we crash here, keys will be one shorter than values. + values.length = lastIndex; + return true; + } + } + + return Object.create(OurWeakMap.prototype, { + get___: { value: constFunc(get___) }, + has___: { value: constFunc(has___) }, + set___: { value: constFunc(set___) }, + delete___: { value: constFunc(delete___) } + }); + }; + + OurWeakMap.prototype = Object.create(Object.prototype, { + get: { + /** + * Return the value most recently associated with key, or + * opt_default if none. + */ + value: function get(key, opt_default) { + return this.get___(key, opt_default); + }, + writable: true, + configurable: true + }, + + has: { + /** + * Is there a value associated with key in this WeakMap? + */ + value: function has(key) { + return this.has___(key); + }, + writable: true, + configurable: true + }, + + set: { + /** + * Associate value with key in this WeakMap, overwriting any + * previous association if present. + */ + value: function set(key, value) { + return this.set___(key, value); + }, + writable: true, + configurable: true + }, + + 'delete': { + /** + * Remove any association for key in this WeakMap, returning + * whether there was one. + * + *

Note that the boolean return here does not work like the + * {@code delete} operator. The {@code delete} operator returns + * whether the deletion succeeds at bringing about a state in + * which the deleted property is absent. The {@code delete} + * operator therefore returns true if the property was already + * absent, whereas this {@code delete} method returns false if + * the association was already absent. + */ + value: function remove(key) { + return this.delete___(key); + }, + writable: true, + configurable: true + } + }); + + if (typeof HostWeakMap === 'function') { + (function() { + // If we got here, then the platform has a WeakMap but we are concerned + // that it may refuse to store some key types. Therefore, make a map + // implementation which makes use of both as possible. + + // In this mode we are always using double maps, so we are not proxy-safe. + // This combination does not occur in any known browser, but we had best + // be safe. + if (doubleWeakMapCheckSilentFailure && typeof Proxy !== 'undefined') { + Proxy = undefined; + } + + function DoubleWeakMap() { + if (!(this instanceof OurWeakMap)) { // approximate test for new ...() + calledAsFunctionWarning(); + } + + // Preferable, truly weak map. + var hmap = new HostWeakMap(); + + // Our hidden-property-based pseudo-weak-map. Lazily initialized in the + // 'set' implementation; thus we can avoid performing extra lookups if + // we know all entries actually stored are entered in 'hmap'. + var omap = undefined; + + // Hidden-property maps are not compatible with proxies because proxies + // can observe the hidden name and either accidentally expose it or fail + // to allow the hidden property to be set. Therefore, we do not allow + // arbitrary WeakMaps to switch to using hidden properties, but only + // those which need the ability, and unprivileged code is not allowed + // to set the flag. + // + // (Except in doubleWeakMapCheckSilentFailure mode in which case we + // disable proxies.) + var enableSwitching = false; + + function dget(key, opt_default) { + if (omap) { + return hmap.has(key) ? hmap.get(key) + : omap.get___(key, opt_default); + } else { + return hmap.get(key, opt_default); + } + } + + function dhas(key) { + return hmap.has(key) || (omap ? omap.has___(key) : false); + } + + var dset; + if (doubleWeakMapCheckSilentFailure) { + dset = function(key, value) { + hmap.set(key, value); + if (!hmap.has(key)) { + if (!omap) { omap = new OurWeakMap(); } + omap.set(key, value); + } + return this; + }; + } else { + dset = function(key, value) { + if (enableSwitching) { + try { + hmap.set(key, value); + } catch (e) { + if (!omap) { omap = new OurWeakMap(); } + omap.set___(key, value); + } + } else { + hmap.set(key, value); + } + return this; + }; + } + + function ddelete(key) { + var result = !!hmap['delete'](key); + if (omap) { return omap.delete___(key) || result; } + return result; + } + + return Object.create(OurWeakMap.prototype, { + get___: { value: constFunc(dget) }, + has___: { value: constFunc(dhas) }, + set___: { value: constFunc(dset) }, + delete___: { value: constFunc(ddelete) }, + permitHostObjects___: { value: constFunc(function(token) { + if (token === weakMapPermitHostObjects) { + enableSwitching = true; + } else { + throw new Error('bogus call to permitHostObjects___'); + } + })} + }); + } + DoubleWeakMap.prototype = OurWeakMap.prototype; + module.exports = DoubleWeakMap; + + // define .constructor to hide OurWeakMap ctor + Object.defineProperty(WeakMap.prototype, 'constructor', { + value: WeakMap, + enumerable: false, // as default .constructor is + configurable: true, + writable: true + }); + })(); + } else { + // There is no host WeakMap, so we must use the emulation. + + // Emulated WeakMaps are incompatible with native proxies (because proxies + // can observe the hidden name), so we must disable Proxy usage (in + // ArrayLike and Domado, currently). + if (typeof Proxy !== 'undefined') { + Proxy = undefined; + } + + module.exports = OurWeakMap; + } +})(); + +},{}]},{},[2]); diff --git a/build/srcset.min.js b/build/srcset.min.js index 010fe61..bdbc1fe 100644 --- a/build/srcset.min.js +++ b/build/srcset.min.js @@ -1 +1 @@ -var Query=function(t){"use strict";var e=function(t){var e,r,n,i,s=[];if(t===void 0||null===t||""===t)return s;for(0===t.indexOf("?")&&(t=t.substring(1)),r=(""+t).split(/[&;]/),e=0;r.length>e;e++)n=r[e],i=n.split("="),s.push([i[0],i[1]]);return s},r=e(t),n=function(){var t,e,n="";for(t=0;r.length>t;t++)e=r[t],n.length>0&&(n+="&"),n+=e.join("=");return n.length>0?"?"+n:n},i=function(t){return t=decodeURIComponent(t),t=t.replace("+"," ")},s=function(t){var e,n;for(n=0;r.length>n;n++)if(e=r[n],i(t)===i(e[0]))return e[1]},o=function(t){var e,n,s=[];for(e=0;r.length>e;e++)n=r[e],i(t)===i(n[0])&&s.push(n[1]);return s},a=function(t,e){var n,s,o,a,u=[];for(n=0;r.length>n;n++)s=r[n],o=i(s[0])===i(t),a=i(s[1])===i(e),(1===arguments.length&&!o||2===arguments.length&&!o&&!a)&&u.push(s);return r=u,this},u=function(t,e,n){return 3===arguments.length&&-1!==n?(n=Math.min(n,r.length),r.splice(n,0,[t,e])):arguments.length>0&&r.push([t,e]),this},h=function(t,e,n){var s,o,h=-1;if(3===arguments.length){for(s=0;r.length>s;s++)if(o=r[s],i(o[0])===i(t)&&decodeURIComponent(o[1])===i(n)){h=s;break}a(t,n).addParam(t,e,h)}else{for(s=0;r.length>s;s++)if(o=r[s],i(o[0])===i(t)){h=s;break}a(t),u(t,e,h)}return this};return{getParamValue:s,getParamValues:o,deleteParam:a,addParam:u,replaceParam:h,toString:n}},Uri=function(t){"use strict";var e=!1,r=function(t){for(var r={strict:/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,loose:/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/},n=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],i={name:"queryKey",parser:/(?:^|&)([^&=]*)=?([^&]*)/g},s=r[e?"strict":"loose"].exec(t),o={},a=14;a--;)o[n[a]]=s[a]||"";return o[i.name]={},o[n[12]].replace(i.parser,function(t,e,r){e&&(o[i.name][e]=r)}),o},n=r(t||""),i=new Query(n.query),s=function(t){return t!==void 0&&(n.protocol=t),n.protocol},o=null,a=function(t){return t!==void 0&&(o=t),null===o?-1!==n.source.indexOf("//"):o},u=function(t){return t!==void 0&&(n.userInfo=t),n.userInfo},h=function(t){return t!==void 0&&(n.host=t),n.host},c=function(t){return t!==void 0&&(n.port=t),n.port},f=function(t){return t!==void 0&&(n.path=t),n.path},l=function(t){return t!==void 0&&(i=new Query(t)),i},d=function(t){return t!==void 0&&(n.anchor=t),n.anchor},g=function(t){return s(t),this},p=function(t){return a(t),this},m=function(t){return u(t),this},v=function(t){return h(t),this},w=function(t){return c(t),this},x=function(t){return f(t),this},y=function(t){return l(t),this},I=function(t){return d(t),this},C=function(t){return l().getParamValue(t)},P=function(t){return l().getParamValues(t)},_=function(t,e){return 2===arguments.length?l().deleteParam(t,e):l().deleteParam(t),this},V=function(t,e,r){return 3===arguments.length?l().addParam(t,e,r):l().addParam(t,e),this},B=function(t,e,r){return 3===arguments.length?l().replaceParam(t,e,r):l().replaceParam(t,e),this},O=function(){var t="",e=function(t){return null!==t&&""!==t};return e(s())?(t+=s(),s().indexOf(":")!==s().length-1&&(t+=":"),t+="//"):a()&&e(h())&&(t+="//"),e(u())&&e(h())&&(t+=u(),u().indexOf("@")!==u().length-1&&(t+="@")),e(h())&&(t+=h(),e(c())&&(t+=":"+c())),e(f())?t+=f():e(h())&&(e(""+l())||e(d()))&&(t+="/"),e(""+l())&&(0!==(""+l()).indexOf("?")&&(t+="?"),t+=""+l()),e(d())&&(0!==d().indexOf("#")&&(t+="#"),t+=d()),t},Q=function(){return new Uri(O())};return{protocol:s,hasAuthorityPrefix:a,userInfo:u,host:h,port:c,path:f,query:l,anchor:d,setProtocol:g,setHasAuthorityPrefix:p,setUserInfo:m,setHost:v,setPort:w,setPath:x,setQuery:y,setAnchor:I,getQueryParamValue:C,getQueryParamValues:P,deleteQueryParam:_,addQueryParam:V,replaceQueryParam:B,toString:O,clone:Q}},jsUri=Uri;(function(t){function e(t){this.imageCandidates=[],this.srcValue=t.src,this.srcsetValue=t.srcset,this.isValid=!0,this.error="",this._parse(this.srcsetValue),this.isValid||console.error("Error: "+this.error)}function r(t){this.src=t.src,this.w=t.w||1/0,this.h=t.h||1/0,this.x=t.x||1}var n=/^[0-9]+$/;e.prototype._parse=function(){for(var t,e,n=this.srcsetValue,i=0,s=[];""!==n;){for(;" "===n.charAt(0);)n=n.slice(1);if(i=n.indexOf(" "),-1!==i){if(t=n.slice(0,i),""===t)break;n=n.slice(i+1),i=n.indexOf(","),-1===i?(e=n,n=""):(e=n.slice(0,i),n=n.slice(i+1)),s.push({url:t,descriptors:e})}else s.push({url:n,descriptors:""}),n=""}for(var o=0,a=s.length;a>o;o++){var u=s[o],h=this._parseDescriptors(u.descriptors);this._addCandidate(new r({src:u.url,x:h.x,w:h.w,h:h.h}))}this.srcValue&&this._addCandidate(new r({src:this.srcValue}))},e.prototype._addCandidate=function(t){for(var e=0;this.imageCandidates.length>e;e++){var r=this.imageCandidates[e];if(r.x==t.x&&r.w==t.w&&r.h==t.h)return}this.imageCandidates.push(t)},e.prototype._parseDescriptors=function(t){for(var e=t.split(/\s/),r={},i=0;e.length>i;i++){var s=e[i];if(s.length>0){var o=s[s.length-1],a=s.substring(0,s.length-1),u=parseInt(a,10),h=parseFloat(a);a.match(n)&&"w"===o?r[o]=u:a.match(n)&&"h"==o?r[o]=u:isNaN(h)||"x"!=o?(this.error='Invalid srcset descriptor found in "'+s+'".',this.isValid=!1):r[o]=h}}return r},t.SrcsetInfo=e})(window),function(t){function e(){this.w=null,this.h=null,this.x=null}e.prototype.compute=function(){this.w=window.innerWidth||document.documentElement.clientWidth,this.h=window.innerHeight||document.documentElement.clientHeight,this.x=window.devicePixelRatio},e.prototype.setForTesting=function(t){this.w=t.w,this.h=t.h,this.x=t.x},e.prototype.getBestImage=function(t){var e=t.imageCandidates.slice(0),r=this._getBestCandidateIf(e,function(t,e){return t.w>e.w});this._removeCandidatesIf(e,function(t){return function(e){return e.we.h});this._removeCandidatesIf(e,function(t){return function(e){return e.he.x});this._removeCandidatesIf(e,function(t){return function(e){return e.xs.w}),this._getBestCandidateIf(e,function(t,e){return t.hs.h});var o=this._getBestCandidateIf(e,function(t,e){return t.xo.x}),e[0]},e.prototype._getBestCandidateIf=function(t,e){for(var r=t[0],n=0;t.length>n;n++){var i=t[n];e(i,r)&&(r=i)}return r},e.prototype._removeCandidatesIf=function(t,e){for(var r=t.length-1;r>=0;r--){var n=t[r];e(n)&&t.splice(r,1)}return t},e.prototype.getBestImage2=function(t){for(var e=null,r=t.imageCandidates,n=0;r.length>n;n++){var i=r[n],s=e?e.x:0;if(i.x>=s&&i.x<=this.x){if(null===e){e=i;continue}this.w<=i.w&&i.w<=e.w&&(e=i)}}return e},t.ViewportInfo=e}(window),function(){function t(){var t=new Image;return"srcset"in t}function e(){if(!t()){var e=new ViewportInfo;e.compute();for(var r=document.querySelectorAll("img"),n=0;r.length>n;n++){var i=r[n],s=i.getAttribute("srcset");if(s){var o=new SrcsetInfo({src:i.src,srcset:s}),a=e.getBestImage(o);i.src=a.src,i.width||i.height||i.style.height||i.style.width||(i.style.webkitTransform="scale("+1/a.x+")",i.style.webkitTransformOrigin="0 0")}}}}var r=setInterval(function(){"complete"===document.readyState&&(e(),clearInterval(r))},10)}(window); \ No newline at end of file +(function e(t,r,n){function i(a,o){if(!r[a]){if(!t[a]){var u=typeof require=="function"&&require;if(!o&&u)return u(a,!0);if(s)return s(a,!0);var f=new Error("Cannot find module '"+a+"'");throw f.code="MODULE_NOT_FOUND",f}var c=r[a]={exports:{}};t[a][0].call(c.exports,function(e){var r=t[a][1][e];return i(r?r:e)},c,c.exports,e,t,r,n)}return r[a].exports}var s=typeof require=="function"&&require;for(var a=0;a0){var a=s[s.length-1];var o=s.substring(0,s.length-1);var u=parseInt(o,10);var f=parseFloat(o);if(o.match(e)&&a==="w"){n[a]=u}else if(o.match(e)&&a=="h"){n[a]=u}else if(!isNaN(f)&&a=="x"){n[a]=f}else{this.error='Invalid srcset descriptor found in "'+s+'".';this.isValid=false}}}return n};function n(e){this.src=e.src;this.w=e.w||Infinity;this.h=e.h||Infinity;this.x=e.x||1}this.SrcsetInfo=r;if(typeof t!=="undefined"&&t!==null){t.exports=this.SrcsetInfo}})(this)},{}],2:[function(e,t,r){(function(){var r=this.ViewportInfo||e("./viewport-info");var n=this.SrcsetInfo||e("./srcset-info");var i=e("weak-map");var s={};var a=new r;s.viewportInfo=a;a.compute();var o=(new Date).getTime();s.windowResizedAt=o;function u(e,t,r){var n,i,s,a,o;var u=function(){var f=(new Date).getTime()-a;if(f=0){n=setTimeout(u,t-f)}else{n=null;if(!r){o=e.apply(s,i);if(!n)s=i=null}}};return function(){s=this;i=arguments;a=(new Date).getTime();var f=r&&!n;if(!n)n=setTimeout(u,t);if(f){o=e.apply(s,i);s=i=null}return o}}var f=function(){var e=["WebKit","Moz","O","Ms",""];for(var t=0;tt.w});this._removeCandidatesIf(t,function(e){return function(t){return t.wt.h});this._removeCandidatesIf(t,function(e){return function(t){return t.ht.x});this._removeCandidatesIf(t,function(e){return function(t){return t.xs.w});var a=this._getBestCandidateIf(t,function(e,t){return e.hs.h});var o=this._getBestCandidateIf(t,function(e,t){return e.xo.x});return t[0]};n.prototype._getBestCandidateIf=function(e,t){var r=e[0];for(var n=0;n=0;r--){var n=e[r];if(t(n)){e.splice(r,1)}}return e};n.prototype.getBestImage2=function(e){var t=null;var r=e.imageCandidates;for(var n=0;n=0?t[s]:i}}function i(t){var n=_(t);if(n){return r in n}else{return e.indexOf(t)>=0}}function s(n,i){var s;var a=_(n);if(a){a[r]=i}else{s=e.indexOf(n);if(s>=0){t[s]=i}else{s=e.length;t[s]=i;e[s]=n}}return this}function a(n){var i=_(n);var s,a;if(i){return r in i&&delete i[r]}else{s=e.indexOf(n);if(s<0){return false}a=e.length-1;e[s]=void 0;t[s]=t[a];e[s]=e[a];e.length=a;t.length=a;return true}}return Object.create(m.prototype,{get___:{value:w(n)},has___:{value:w(i)},set___:{value:w(s)},delete___:{value:w(a)}})};m.prototype=Object.create(Object.prototype,{get:{value:function I(e,t){return this.get___(e,t)},writable:true,configurable:true},has:{value:function j(e){return this.has___(e)},writable:true,configurable:true},set:{value:function C(e,t){return this.set___(e,t)},writable:true,configurable:true},"delete":{value:function V(e){return this.delete___(e)},writable:true,configurable:true}});if(typeof n==="function"){(function(){if(r&&typeof Proxy!=="undefined"){Proxy=undefined}function i(){if(!(this instanceof m)){y()}var t=new n;var i=undefined;var s=false;function a(e,r){if(i){return t.has(e)?t.get(e):i.get___(e,r)}else{return t.get(e,r)}}function o(e){return t.has(e)||(i?i.has___(e):false)}var u;if(r){u=function(e,r){t.set(e,r);if(!t.has(e)){if(!i){i=new m}i.set(e,r)}return this}}else{u=function(e,r){if(s){try{t.set(e,r)}catch(n){if(!i){i=new m}i.set___(e,r)}}else{t.set(e,r)}return this}}function f(e){var r=!!t["delete"](e);if(i){return i.delete___(e)||r}return r}return Object.create(m.prototype,{get___:{value:w(a)},has___:{value:w(o)},set___:{value:w(u)},delete___:{value:w(f)},permitHostObjects___:{value:w(function(t){if(t===e){s=true}else{throw new Error("bogus call to permitHostObjects___")}})}})}i.prototype=m.prototype;t.exports=i;Object.defineProperty(WeakMap.prototype,"constructor",{value:WeakMap,enumerable:false,configurable:true,writable:true})})()}else{if(typeof Proxy!=="undefined"){Proxy=undefined}t.exports=m}})()},{}]},{},[2]); \ No newline at end of file diff --git a/demo/1024x768.png b/demo/1024x768.png new file mode 100644 index 0000000..3cdf957 Binary files /dev/null and b/demo/1024x768.png differ diff --git a/demo/1536x1152.png b/demo/1536x1152.png new file mode 100644 index 0000000..38afb92 Binary files /dev/null and b/demo/1536x1152.png differ diff --git a/demo/160x120.png b/demo/160x120.png new file mode 100644 index 0000000..a96da38 Binary files /dev/null and b/demo/160x120.png differ diff --git a/demo/2048x1536.png b/demo/2048x1536.png new file mode 100644 index 0000000..b7be632 Binary files /dev/null and b/demo/2048x1536.png differ diff --git a/demo/320x240.png b/demo/320x240.png new file mode 100644 index 0000000..40a65f8 Binary files /dev/null and b/demo/320x240.png differ diff --git a/demo/640x480.png b/demo/640x480.png new file mode 100644 index 0000000..e0bd234 Binary files /dev/null and b/demo/640x480.png differ diff --git a/demo/768x576.png b/demo/768x576.png new file mode 100644 index 0000000..9fad221 Binary files /dev/null and b/demo/768x576.png differ diff --git a/demo/banner-HD.jpeg b/demo/banner-HD.jpeg deleted file mode 100644 index a3a3c88..0000000 Binary files a/demo/banner-HD.jpeg and /dev/null differ diff --git a/demo/banner-phone-HD.jpeg b/demo/banner-phone-HD.jpeg deleted file mode 100644 index 38f44c1..0000000 Binary files a/demo/banner-phone-HD.jpeg and /dev/null differ diff --git a/demo/banner-phone.jpeg b/demo/banner-phone.jpeg deleted file mode 100644 index a32d92a..0000000 Binary files a/demo/banner-phone.jpeg and /dev/null differ diff --git a/demo/banner.jpeg b/demo/banner.jpeg deleted file mode 100644 index 602ba89..0000000 Binary files a/demo/banner.jpeg and /dev/null differ diff --git a/demo/index.html b/demo/index.html index ba74079..1c923b7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,15 +2,21 @@ Srcset Polyfill Demo - + + + + +

+ The Breakfast Combo +
-
The Breakfast Combo
+ diff --git a/js/libs/jsuri-1.1.1.js b/js/libs/jsuri-1.1.1.js deleted file mode 100644 index c5ecee8..0000000 --- a/js/libs/jsuri-1.1.1.js +++ /dev/null @@ -1,473 +0,0 @@ -/*! - * jsUri v1.1.1 - * https://github.com/derek-watson/jsUri - * - * Copyright 2011, Derek Watson - * Released under the MIT license. - * http://jquery.org/license - * - * Includes parseUri regular expressions - * http://blog.stevenlevithan.com/archives/parseuri - * Copyright 2007, Steven Levithan - * Released under the MIT license. - * - * Date: Mon Nov 14 20:06:34 2011 -0800 - */ - - -var Query = function (queryString) { - - // query string parsing, parameter manipulation and stringification - - 'use strict'; - - var // parseQuery(q) parses the uri query string and returns a multi-dimensional array of the components - parseQuery = function (q) { - var arr = [], i, ps, p, keyval; - - if (typeof (q) === 'undefined' || q === null || q === '') { - return arr; - } - - if (q.indexOf('?') === 0) { - q = q.substring(1); - } - - ps = q.toString().split(/[&;]/); - - for (i = 0; i < ps.length; i++) { - p = ps[i]; - keyval = p.split('='); - arr.push([keyval[0], keyval[1]]); - } - - return arr; - }, - - params = parseQuery(queryString), - - // toString() returns a string representation of the internal state of the object - toString = function () { - var s = '', i, param; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (s.length > 0) { - s += '&'; - } - s += param.join('='); - } - return s.length > 0 ? '?' + s : s; - }, - - decode = function (s) { - s = decodeURIComponent(s); - s = s.replace('+', ' '); - return s; - }, - - // getParamValues(key) returns the first query param value found for the key 'key' - getParamValue = function (key) { - var param, i; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(key) === decode(param[0])) { - return param[1]; - } - } - }, - - // getParamValues(key) returns an array of query param values for the key 'key' - getParamValues = function (key) { - var arr = [], i, param; - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(key) === decode(param[0])) { - arr.push(param[1]); - } - } - return arr; - }, - - // deleteParam(key) removes all instances of parameters named (key) - // deleteParam(key, val) removes all instances where the value matches (val) - deleteParam = function (key, val) { - - var arr = [], i, param, keyMatchesFilter, valMatchesFilter; - - for (i = 0; i < params.length; i++) { - - param = params[i]; - keyMatchesFilter = decode(param[0]) === decode(key); - valMatchesFilter = decode(param[1]) === decode(val); - - if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && !keyMatchesFilter && !valMatchesFilter)) { - arr.push(param); - } - } - - params = arr; - - return this; - }, - - // addParam(key, val) Adds an element to the end of the list of query parameters - // addParam(key, val, index) adds the param at the specified position (index) - addParam = function (key, val, index) { - - if (arguments.length === 3 && index !== -1) { - index = Math.min(index, params.length); - params.splice(index, 0, [key, val]); - } else if (arguments.length > 0) { - params.push([key, val]); - } - return this; - }, - - // replaceParam(key, newVal) deletes all instances of params named (key) and replaces them with the new single value - // replaceParam(key, newVal, oldVal) deletes only instances of params named (key) with the value (val) and replaces them with the new single value - // this function attempts to preserve query param ordering - replaceParam = function (key, newVal, oldVal) { - - var index = -1, i, param; - - if (arguments.length === 3) { - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) { - index = i; - break; - } - } - deleteParam(key, oldVal).addParam(key, newVal, index); - } else { - for (i = 0; i < params.length; i++) { - param = params[i]; - if (decode(param[0]) === decode(key)) { - index = i; - break; - } - } - deleteParam(key); - addParam(key, newVal, index); - } - return this; - }; - - // public api - return { - getParamValue: getParamValue, - getParamValues: getParamValues, - deleteParam: deleteParam, - addParam: addParam, - replaceParam: replaceParam, - - toString: toString - }; -}; - -var Uri = function (uriString) { - - // uri string parsing, attribute manipulation and stringification - - 'use strict'; - - /*global Query: true */ - /*jslint regexp: false, plusplus: false */ - - var strictMode = false, - - // parseUri(str) parses the supplied uri and returns an object containing its components - parseUri = function (str) { - - /*jslint unparam: true */ - var parsers = { - strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ - }, - keys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], - q = { - name: "queryKey", - parser: /(?:^|&)([^&=]*)=?([^&]*)/g - }, - m = parsers[strictMode ? "strict" : "loose"].exec(str), - uri = {}, - i = 14; - - while (i--) { - uri[keys[i]] = m[i] || ""; - } - - uri[q.name] = {}; - uri[keys[12]].replace(q.parser, function ($0, $1, $2) { - if ($1) { - uri[q.name][$1] = $2; - } - }); - - return uri; - }, - - uriParts = parseUri(uriString || ''), - - queryObj = new Query(uriParts.query), - - - /* - Basic get/set functions for all properties - */ - - protocol = function (val) { - if (typeof val !== 'undefined') { - uriParts.protocol = val; - } - return uriParts.protocol; - }, - - hasAuthorityPrefixUserPref = null, - - // hasAuthorityPrefix: if there is no protocol, the leading // can be enabled or disabled - hasAuthorityPrefix = function (val) { - - if (typeof val !== 'undefined') { - hasAuthorityPrefixUserPref = val; - } - - if (hasAuthorityPrefixUserPref === null) { - return (uriParts.source.indexOf('//') !== -1); - } else { - return hasAuthorityPrefixUserPref; - } - }, - - userInfo = function (val) { - if (typeof val !== 'undefined') { - uriParts.userInfo = val; - } - return uriParts.userInfo; - }, - - host = function (val) { - if (typeof val !== 'undefined') { - uriParts.host = val; - } - return uriParts.host; - }, - - port = function (val) { - if (typeof val !== 'undefined') { - uriParts.port = val; - } - return uriParts.port; - }, - - path = function (val) { - if (typeof val !== 'undefined') { - uriParts.path = val; - } - return uriParts.path; - }, - - query = function (val) { - if (typeof val !== 'undefined') { - queryObj = new Query(val); - } - return queryObj; - }, - - anchor = function (val) { - if (typeof val !== 'undefined') { - uriParts.anchor = val; - } - return uriParts.anchor; - }, - - - /* - Fluent setters for Uri uri properties - */ - - setProtocol = function (val) { - protocol(val); - return this; - }, - - setHasAuthorityPrefix = function (val) { - hasAuthorityPrefix(val); - return this; - }, - - setUserInfo = function (val) { - userInfo(val); - return this; - }, - - setHost = function (val) { - host(val); - return this; - }, - - setPort = function (val) { - port(val); - return this; - }, - - setPath = function (val) { - path(val); - return this; - }, - - setQuery = function (val) { - query(val); - return this; - }, - - setAnchor = function (val) { - anchor(val); - return this; - }, - - /* - Query method wrappers - */ - getQueryParamValue = function (key) { - return query().getParamValue(key); - }, - - getQueryParamValues = function (key) { - return query().getParamValues(key); - }, - - deleteQueryParam = function (key, val) { - if (arguments.length === 2) { - query().deleteParam(key, val); - } else { - query().deleteParam(key); - } - - return this; - }, - - addQueryParam = function (key, val, index) { - if (arguments.length === 3) { - query().addParam(key, val, index); - } else { - query().addParam(key, val); - } - return this; - }, - - replaceQueryParam = function (key, newVal, oldVal) { - if (arguments.length === 3) { - query().replaceParam(key, newVal, oldVal); - } else { - query().replaceParam(key, newVal); - } - - return this; - }, - - /* - Serialization - */ - - // toString() stringifies the current state of the uri - toString = function () { - - var s = '', - is = function (s) { - return (s !== null && s !== ''); - }; - - if (is(protocol())) { - s += protocol(); - if (protocol().indexOf(':') !== protocol().length - 1) { - s += ':'; - } - s += '//'; - } else { - if (hasAuthorityPrefix() && is(host())) { - s += '//'; - } - } - - if (is(userInfo()) && is(host())) { - s += userInfo(); - if (userInfo().indexOf('@') !== userInfo().length - 1) { - s += '@'; - } - } - - if (is(host())) { - s += host(); - if (is(port())) { - s += ':' + port(); - } - } - - if (is(path())) { - s += path(); - } else { - if (is(host()) && (is(query().toString()) || is(anchor()))) { - s += '/'; - } - } - if (is(query().toString())) { - if (query().toString().indexOf('?') !== 0) { - s += '?'; - } - s += query().toString(); - } - - if (is(anchor())) { - if (anchor().indexOf('#') !== 0) { - s += '#'; - } - s += anchor(); - } - - return s; - }, - - /* - Cloning - */ - - // clone() returns a new, identical Uri instance - clone = function () { - return new Uri(toString()); - }; - - // public api - return { - - protocol: protocol, - hasAuthorityPrefix: hasAuthorityPrefix, - userInfo: userInfo, - host: host, - port: port, - path: path, - query: query, - anchor: anchor, - - setProtocol: setProtocol, - setHasAuthorityPrefix: setHasAuthorityPrefix, - setUserInfo: setUserInfo, - setHost: setHost, - setPort: setPort, - setPath: setPath, - setQuery: setQuery, - setAnchor: setAnchor, - - getQueryParamValue: getQueryParamValue, - getQueryParamValues: getQueryParamValues, - deleteQueryParam: deleteQueryParam, - addQueryParam: addQueryParam, - replaceQueryParam: replaceQueryParam, - - toString: toString, - clone: clone - }; -}; - -/* add compatibility for users of jsUri <= 1.1.1 */ -var jsUri = Uri; diff --git a/js/main.js b/js/main.js deleted file mode 100644 index f9f22ca..0000000 --- a/js/main.js +++ /dev/null @@ -1,49 +0,0 @@ -(function(exports) { - - function isSrcsetImplemented() { - return 'srcset' in new Image(); - } - - function main() { - // If the browser supports @srcset natively, don't do any polyfill. - if (isSrcsetImplemented()) { - return; - } - - // Get the user agent's capabilities (viewport width, viewport height, dPR). - var viewportInfo = new ViewportInfo(); - viewportInfo.compute(); - // Go through all images on the page. - var images = document.querySelectorAll('img'); - // If they have srcset attributes, apply JS to handle that correctly. - for (var i = 0; i < images.length; i++) { - var img = images[i]; - // Parse the srcset from the image element. - var srcset = img.getAttribute('srcset'); - if (srcset) { - var srcsetInfo = new SrcsetInfo({src: img.src, - srcset: srcset}); - // Go through all the candidates, pick the best one that matches. - var imageInfo = viewportInfo.getBestImage(srcsetInfo); - // TODO: consider using -webkit-image-set instead (if available). - // Replace the with this image. - img.src = imageInfo.src; - // If there's no set size, then we scale the image if necessary - // (e.g. x != 1) - if (!(img.width || img.height || img.style.height || img.style.width)) { - img.style.webkitTransform = 'scale(' + (1/imageInfo.x) + ')'; - img.style.webkitTransformOrigin = '0 0'; - } - } - } - } - - // Small cross browser document ready. - var readyTimer = setInterval(function () { - if (document.readyState === "complete") { - main(); - clearInterval(readyTimer); - } - }, 10); - -})(window); diff --git a/js/srcset-info.js b/js/srcset-info.js index 3c40e15..4fb585d 100644 --- a/js/srcset-info.js +++ b/js/srcset-info.js @@ -1,4 +1,4 @@ -(function(exports) { +(function() { var INT_REGEXP = /^[0-9]+$/; function SrcsetInfo(options) { @@ -145,6 +145,11 @@ this.x = options.x || 1; } - exports.SrcsetInfo = SrcsetInfo; -})(window); + // Exports + this.SrcsetInfo = SrcsetInfo; + if (typeof module !== "undefined" && module !== null) { + module.exports = this.SrcsetInfo; + } + +})(this); diff --git a/js/srcset.js b/js/srcset.js new file mode 100644 index 0000000..c64aeb7 --- /dev/null +++ b/js/srcset.js @@ -0,0 +1,194 @@ +(function () { + // + // CustomEvent polyfill (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill) + // + + if ( typeof window.CustomEvent === "function" ) return false; + + function CustomEvent ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(); + +(function () { + var ViewportInfo = this.ViewportInfo || require('./viewport-info'); + var SrcsetInfo = this.SrcsetInfo || require('./srcset-info'); + var WeakMap = require('weak-map'); + + var srcset = {}; + + var viewportInfo = new ViewportInfo(); + srcset.viewportInfo = viewportInfo; + viewportInfo.compute(); + + var windowResizedAt = (new Date).getTime(); + srcset.windowResizedAt = windowResizedAt; + + // Picked from underscore.js + function debounce(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = new Date().getTime() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = new Date().getTime(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + } + + // https://gist.github.com/stucox/5231211 + var hasMO = (function () { + var prefixes = ['WebKit', 'Moz', 'O', 'Ms', '']; + + for (var i=0; i < prefixes.length; i++) { + if ((prefixes[i] + 'MutationObserver') in window) { + return window[prefixes[i] + 'MutationObserver']; + } + } + + return false; + }()); + + function SrcsetView(el) { + this.el = el; + + this.srcsetInfo = new SrcsetInfo({ + src: this.el.src, + srcset: this.el.dataset.srcset + }); + + // + // Observe data-srcset attributes mutations to keep this.srcsetInfo up-to-date (if available) + // + + if (hasMO) { + this.mo = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + //console.log(mutation); + + if (mutation.target === this.el && mutation.type === 'attributes') { + if (mutation.attributeName === 'src' || mutation.attributeName === 'data-srcset') { + this.update(); + } + } + }.bind(this)); + }.bind(this)); + + this.mo.observe(this.el, {attributes: true}); + } + } + SrcsetView.prototype.update = function (options) { + options || (options = {}); + /*options = $.extend({}, options, { + force: false + });*/ + + if (this.srcsetInfo.srcValue !== this.el.src) { + this.srcsetInfo.srcValue = this.el.src; + } + + var srcsetchanged; + if (this.srcsetInfo.srcsetValue !== this.el.dataset.srcset) { + srcsetchanged = true; + + this.srcsetInfo.imageCandidates = []; // reset imageCandidates + this.srcsetInfo.srcsetValue = this.el.dataset.srcset; + this.srcsetInfo._parse(this.srcsetInfo.srcsetValue); + if (!this.srcsetInfo.isValid) { + console.error('Error: ' + this.srcsetInfo.error); + } + } + + var needUpdate = (!this.srcupdatedat || this.srcupdatedat < windowResizedAt); + if (!this.el.src || needUpdate || srcsetchanged || options.force) { + + if (this.srcsetInfo) { + var bestImageInfo = viewportInfo.getBestImage(this.srcsetInfo); + + var oldsrc = this.el.src; + var newsrc = bestImageInfo.src; + + if (newsrc === oldsrc) return false; // same, no need to update + + //console.log('updating src', this.el, oldsrc, newsrc); + + // + // 'srcchanged' event + // + + var srcchanged = new CustomEvent('srcchanged', { + detail: { + previous: oldsrc, + actual: newsrc + }, + bubbles: true + }); + + this.el.src = newsrc; + // Dispatch 'srcchanged' + setTimeout(function () { + this.el.dispatchEvent(srcchanged); + }.bind(this), 0); + } + + // Remember when updated to compare with window's resizeAt timestamp + this.srcupdatedat = (new Date).getTime(); + } + }; + + var srcsetViews = new WeakMap(); + srcset.imgs = srcsetViews; + function update() { + // update timestamp + windowResizedAt = (new Date).getTime(); + viewportInfo.compute(); + + // Update every images + [].forEach.call(document.querySelectorAll('img[data-srcset]'), function (el) { + var srcsetview = srcsetViews.get(el); + if (!srcsetview) { + srcsetview = new SrcsetView(el); + srcsetViews.set(el, srcsetview); + } + + srcsetview.update(); + }); + } + window.onresize = debounce(update, 200); + update(); + srcset.update = update; + + // Exports + this.srcset = srcset; + if (typeof module !== "undefined" && module !== null) { + module.exports = this.srcset; + } +})(this); diff --git a/js/viewport-info.js b/js/viewport-info.js index 15fe42b..0afd071 100644 --- a/js/viewport-info.js +++ b/js/viewport-info.js @@ -1,4 +1,5 @@ -(function(exports) { +(function() { + var SrcsetInfo = this.SrcsetInfo || require('./srcset-info'); function ViewportInfo() { this.w = null; @@ -129,6 +130,10 @@ return bestMatch; }; - exports.ViewportInfo = ViewportInfo; + // Exports + this.ViewportInfo = ViewportInfo; + if (typeof module !== "undefined" && module !== null) { + module.exports = this.ViewportInfo; + } -})(window); +})(this); diff --git a/package.json b/package.json index d29cf53..e331cae 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "srcset-polyfill", "version": "0.1.0", + "main": "js/srcset.js", "devDependencies": { - "grunt": "~0.4.0", - "grunt-contrib-jshint": "~0.1.1", - "grunt-contrib-nodeunit": "~0.1.2", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-uglify": "~0.1.2" + "browserify": "^11.0.1", + "uglify-js": "^2.4.24" + }, + "dependencies": { + "weak-map": "^1.0.2" } }