diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..7d06030
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": [],
+ "plugins": ["transform-class-properties"]
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2d2b47d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea
+node_modules
\ No newline at end of file
diff --git a/demo.html b/demo.html
new file mode 100644
index 0000000..1e1d979
--- /dev/null
+++ b/demo.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/img-2.js b/dist/img-2.js
new file mode 100644
index 0000000..972224e
--- /dev/null
+++ b/dist/img-2.js
@@ -0,0 +1,351 @@
+/**
+ * Created by Leon Revill on 10/12/2017.
+ * Blog: blog.revillweb.com
+ * Twitter: @RevillWeb
+ * GitHub: github.com/RevillWeb
+ */
+const __style__ = Symbol();
+
+class Img2 extends HTMLElement {
+
+ constructor() {
+ super();
+
+ // Private class variables
+ this._root = null;
+ this._$img = null;
+ this._$preview = null;
+ this._preview = null;
+ this._src = null;
+ this._width = null;
+ this._height = null;
+ this._reset();
+
+ // Settings
+ this._renderOnPreCached = Img2.settings.RENDER_ON_PRECACHED;
+
+ // Bound class methods
+ this._precache = this._precache.bind(this);
+ this._onImgLoad = this._onImgLoad.bind(this);
+ this._onImgPreCached = this._onImgPreCached.bind(this);
+ }
+
+ /**
+ * Reset all private values
+ * @private
+ */
+ _reset() {
+ this._rendered = false;
+ this._loading = false;
+ this._loaded = false;
+ this._preCaching = false;
+ this._preCached = false;
+ }
+
+ [__style__]() {
+ return `
+
+ `;
+ }
+
+ connectedCallback() {
+
+ // Override any global settings
+ this._renderOnPreCached = this.getAttribute("render-on-pre-cached") === "true";
+ this._init();
+ }
+
+ _init() {
+
+ // Check to see if we have a src, if not return and do nothing else
+ this._src = this.getAttribute("src");
+ // Grab the initial attribute values
+ this._preview = this.getAttribute("src-preview");
+ this._width = this.getAttribute("width");
+ this._height = this.getAttribute("height");
+
+ if (!this._src || !this._width || !this._height) return;
+
+ // Set the height and width of the element so that we can figure out if it is on the screen or not
+ this.style.width = `${ this._width }px`;
+ this.style.height = `${ this._height }px`;
+
+ // Figure out if this image is within view
+ Img2.addIntersectListener(this, () => {
+ Img2._removePreCacheListener(this._precache);
+ this._render();
+ this._load();
+ Img2.removeIntersectListener(this);
+ });
+
+ // Listen for precache instruction
+ Img2._addPreCacheListener(this._precache, this._src);
+ }
+
+ /**
+ * Method which displays the image once ready to be displayed
+ * @private
+ */
+ _load() {
+ if (this._preCached === false) Img2._priorityCount += 1;
+ this._$img.onload = this._onImgLoad;
+ this._loading = true;
+ this._$img.src = this._src;
+ }
+
+ _onImgLoad() {
+ this._loading = false;
+ this._loaded = true;
+ if (this._$preview !== null) {
+ this._root.removeChild(this._$preview);
+ this._$preview = null;
+ }
+ this._$img.onload = null;
+ if (this._preCached === false) Img2._priorityCount -= 1;
+ }
+
+ _onImgPreCached() {
+ this._preCaching = false;
+ this._preCached = true;
+ if (this._renderOnPreCached !== false) {
+ this._render();
+ this._load();
+ }
+ }
+
+ static get observedAttributes() {
+ return ["src", "width", "height"];
+ }
+ attributeChangedCallback(name, oldValue, newValue) {
+
+ // If nothing has changed then just return
+ if (newValue === oldValue) return;
+
+ switch (name) {
+ case "src":
+ // If the src is changed then we need to reset and start again
+ this._reset();
+ this._init();
+ break;
+ case "width":
+ this._width = newValue;
+ if (this._$preview !== null) {
+ this._$preview.width = this._width;
+ }
+ if (this._$img !== null) {
+ this._$img.width = this._width;
+ }
+ this.style.width = `${ this._width }px`;
+ break;
+ case "height":
+ this._height = newValue;
+ if (this._$preview !== null) {
+ this._$preview.height = this._height;
+ }
+ if (this._$img !== null) {
+ this._$img.height = this._height;
+ }
+ this.style.height = `${ this._height }px`;
+ break;
+ case "render-on-pre-cached":
+ this._renderOnPreCached = !(this.getAttribute("render-on-pre-cached") === "false");
+ break;
+ }
+ }
+
+ /**
+ * Method which renders the DOM elements and displays any preview image
+ * @private
+ */
+ _render() {
+
+ if (this._rendered === true) return;
+
+ // Render the Shadow Root if not done already (src change can force this method to be called again)
+ if (this._root === null) {
+ // Attach the Shadow Root to the element
+ this._root = this.attachShadow({ mode: "open" });
+ // Create the initial template with styles
+ this._root.innerHTML = `${ this[__style__]() }`;
+ }
+
+ // If a preview image has been specified
+ if (this._$preview === null && this._preview !== null && this._loaded === false) {
+ // Create the element
+ this._$preview = document.createElement("img");
+ this._$preview.classList.add("img2-preview");
+ this._$preview.src = this._preview;
+ // Add the specified width and height
+ this._$preview.width = this._width;
+ this._$preview.height = this._height;
+ // Add it to the Shadow Root
+ this._root.appendChild(this._$preview);
+ }
+
+ // Render the img element if not done already
+ if (this._$img === null) {
+ // Create the actual image element to be used to display the image
+ this._$img = document.createElement("img");
+ this._$img.classList.add("img2-src");
+ // add the specified width and height to the image element
+ this._$img.width = this._width;
+ this._$img.height = this._height;
+ // Add the image to the Shadow Root
+ this._root.appendChild(this._$img);
+ }
+
+ // Flag as rendered
+ this._rendered = true;
+ }
+
+ _precache() {
+ this._preCaching = true;
+ Img2._preCache(this._src, this._onImgPreCached);
+ }
+
+ static _addPreCacheListener(cb, url) {
+ Img2._preCacheListeners.set(cb, url);
+ }
+
+ static _removePreCacheListener(cb) {
+ Img2._preCacheListeners.delete(cb);
+ }
+
+ static _startPreCache() {
+ for (let cb of Img2._preCacheListeners.keys()) cb();
+ }
+
+ /**
+ * Methods used to determine when currently visible (priority) elements have finished download to then inform other elements to pre-cache
+ */
+
+ static get _priorityCount() {
+ return Img2.__priorityCount;
+ }
+ static set _priorityCount(value) {
+ Img2.__priorityCount = value;
+ if (Img2.__priorityCount < 1) {
+ // Inform components that they can start to pre-cache their images
+ // Debounce in case the user scrolls because then there will be more priority images
+ if (Img2._startPreCacheDebounce !== null) {
+ clearTimeout(Img2._startPreCacheDebounce);
+ Img2._startPreCacheDebounce = null;
+ }
+ Img2._startPreCacheDebounce = setTimeout(function () {
+ if (Img2.__priorityCount < 1) Img2._startPreCache();
+ }, 500);
+ }
+ }
+
+ /**
+ * Methods used to determine when this element is in the visible viewport
+ */
+
+
+ static addIntersectListener($element, intersectCallback) {
+ Img2._intersectListeners.set($element, intersectCallback);
+ Img2._observer.observe($element);
+ }
+
+ static removeIntersectListener($element) {
+ if ($element) Img2._observer.unobserve($element);
+ }
+
+ static _handleIntersect(entries) {
+ entries.forEach(entry => {
+ if (entry.isIntersecting === true) {
+ const cb = Img2._intersectListeners.get(entry.target);
+ if (cb !== undefined) cb(entry);
+ }
+ });
+ }
+
+ static _preCache(url, cb) {
+
+ let slot = Img2._preCacheCallbacks[url];
+ if (slot === undefined) {
+ Img2._preCacheCallbacks[url] = {
+ cached: false,
+ cbs: [cb]
+ };
+ const location = url.indexOf("http") > -1 ? url : window.location.href + url;
+ Img2._worker.postMessage({ location: location, url: url });
+ } else {
+ if (slot.cached === true) {
+ cb();
+ } else {
+ slot.cbs.push(cb);
+ }
+ }
+ }
+}
+
+/**
+ * Methods used to pre-cache images using a WebWorker
+ */
+
+Img2._preCacheListeners = new Map();
+Img2.__priorityCount = 0;
+Img2._startPreCacheDebounce = null;
+Img2._intersectListeners = new Map();
+Img2._observer = new IntersectionObserver(Img2._handleIntersect, {
+ root: null,
+ rootMargin: "0px",
+ threshold: 0
+});
+Img2._preCacheCallbacks = {};
+Img2._worker = new Worker(window.URL.createObjectURL(new Blob([`self.onmessage=${ function (e) {
+ fetch(e.data.location).then(response => {
+ if (response.status === 200 || response.status === 0) {
+ return Promise.resolve(response);
+ } else {
+ return Promise.reject(new Error(`Couldn't pre-cache URL '${ e.data.url }'.`));
+ }
+ }).then(response => {
+ return response.blob();
+ }).then(() => {
+ self.postMessage(e.data.url);
+ }).catch(console.error);
+}.toString() };`], { type: "text/javascript" })));
+
+Img2._worker.onmessage = function (e) {
+ const slot = Img2._preCacheCallbacks[e.data];
+ if (slot !== undefined) {
+ slot.cached = true;
+ slot.cbs = slot.cbs.filter(cb => {
+ // Call the callback
+ cb();
+ // Remove the callback
+ return false;
+ });
+ }
+};
+
+/** Img2 Settings **/
+Img2.settings = {
+ "RENDER_ON_PRECACHED": false // Set this to false to save memory but can cause jank during scrolling
+};
+
+window.customElements.define("img-2", Img2);
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..1d08edd
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,483 @@
+{
+ "name": "img-2",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "babel-code-frame": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+ "dev": true,
+ "requires": {
+ "chalk": "1.1.3",
+ "esutils": "2.0.2",
+ "js-tokens": "3.0.2"
+ }
+ },
+ "babel-core": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz",
+ "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "6.26.0",
+ "babel-generator": "6.26.0",
+ "babel-helpers": "6.24.1",
+ "babel-messages": "6.23.0",
+ "babel-register": "6.26.0",
+ "babel-runtime": "6.26.0",
+ "babel-template": "6.26.0",
+ "babel-traverse": "6.26.0",
+ "babel-types": "6.26.0",
+ "babylon": "6.18.0",
+ "convert-source-map": "1.5.1",
+ "debug": "2.6.9",
+ "json5": "0.5.1",
+ "lodash": "4.17.4",
+ "minimatch": "3.0.4",
+ "path-is-absolute": "1.0.1",
+ "private": "0.1.8",
+ "slash": "1.0.0",
+ "source-map": "0.5.7"
+ }
+ },
+ "babel-generator": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz",
+ "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=",
+ "dev": true,
+ "requires": {
+ "babel-messages": "6.23.0",
+ "babel-runtime": "6.26.0",
+ "babel-types": "6.26.0",
+ "detect-indent": "4.0.0",
+ "jsesc": "1.3.0",
+ "lodash": "4.17.4",
+ "source-map": "0.5.7",
+ "trim-right": "1.0.1"
+ }
+ },
+ "babel-helper-function-name": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz",
+ "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
+ "dev": true,
+ "requires": {
+ "babel-helper-get-function-arity": "6.24.1",
+ "babel-runtime": "6.26.0",
+ "babel-template": "6.26.0",
+ "babel-traverse": "6.26.0",
+ "babel-types": "6.26.0"
+ }
+ },
+ "babel-helper-get-function-arity": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz",
+ "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "babel-types": "6.26.0"
+ }
+ },
+ "babel-helpers": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+ "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "babel-template": "6.26.0"
+ }
+ },
+ "babel-messages": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0"
+ }
+ },
+ "babel-plugin-syntax-class-properties": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
+ "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=",
+ "dev": true
+ },
+ "babel-plugin-transform-class-properties": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz",
+ "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=",
+ "dev": true,
+ "requires": {
+ "babel-helper-function-name": "6.24.1",
+ "babel-plugin-syntax-class-properties": "6.13.0",
+ "babel-runtime": "6.26.0",
+ "babel-template": "6.26.0"
+ }
+ },
+ "babel-register": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+ "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+ "dev": true,
+ "requires": {
+ "babel-core": "6.26.0",
+ "babel-runtime": "6.26.0",
+ "core-js": "2.5.2",
+ "home-or-tmp": "2.0.0",
+ "lodash": "4.17.4",
+ "mkdirp": "0.5.1",
+ "source-map-support": "0.4.18"
+ }
+ },
+ "babel-runtime": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+ "dev": true,
+ "requires": {
+ "core-js": "2.5.2",
+ "regenerator-runtime": "0.11.1"
+ }
+ },
+ "babel-template": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+ "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "babel-traverse": "6.26.0",
+ "babel-types": "6.26.0",
+ "babylon": "6.18.0",
+ "lodash": "4.17.4"
+ }
+ },
+ "babel-traverse": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "6.26.0",
+ "babel-messages": "6.23.0",
+ "babel-runtime": "6.26.0",
+ "babel-types": "6.26.0",
+ "babylon": "6.18.0",
+ "debug": "2.6.9",
+ "globals": "9.18.0",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4"
+ }
+ },
+ "babel-types": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "esutils": "2.0.2",
+ "lodash": "4.17.4",
+ "to-fast-properties": "1.0.3"
+ }
+ },
+ "babylon": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
+ "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
+ "dev": true,
+ "requires": {
+ "balanced-match": "1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "2.2.1",
+ "escape-string-regexp": "1.0.5",
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
+ "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=",
+ "dev": true
+ },
+ "core-js": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.2.tgz",
+ "integrity": "sha1-vEZIZW59ydyA19PHu8Fy2W50TmM=",
+ "dev": true
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "dev": true,
+ "requires": {
+ "repeating": "2.0.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "globals": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+ "dev": true
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "2.1.1"
+ }
+ },
+ "home-or-tmp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+ "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+ "dev": true,
+ "requires": {
+ "os-homedir": "1.0.2",
+ "os-tmpdir": "1.0.2"
+ }
+ },
+ "invariant": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
+ "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
+ "dev": true,
+ "requires": {
+ "loose-envify": "1.3.1"
+ }
+ },
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "1.0.1"
+ }
+ },
+ "js-tokens": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+ "dev": true
+ },
+ "jsesc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+ "dev": true
+ },
+ "json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true
+ },
+ "lodash": {
+ "version": "4.17.4",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
+ "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "dev": true
+ },
+ "loose-envify": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
+ "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
+ "dev": true,
+ "requires": {
+ "js-tokens": "3.0.2"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "1.1.8"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "private": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+ "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+ "dev": true
+ },
+ "regenerator-runtime": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+ "dev": true
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "1.0.2"
+ }
+ },
+ "slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.4.18",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+ "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+ "dev": true,
+ "requires": {
+ "source-map": "0.5.7"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "2.1.1"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ },
+ "to-fast-properties": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+ "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+ "dev": true
+ },
+ "trim-right": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d1d9c47
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "img-2",
+ "version": "1.0.0",
+ "description": "Replace elements with to automatically pre-cache images and improve page performance.",
+ "main": "index.js",
+ "scripts": {
+ "build": "babel src/img-2.js --out-file dist/img-2.js --watch"
+ },
+ "keywords": [
+ "pre-cache",
+ "pre-load"
+ ],
+ "author": "Leon Revill (@revillweb)",
+ "license": "MIT",
+ "devDependencies": {
+ "babel-core": "^6.26.0",
+ "babel-plugin-transform-class-properties": "^6.24.1"
+ }
+}
diff --git a/src/img-2.js b/src/img-2.js
new file mode 100644
index 0000000..7647150
--- /dev/null
+++ b/src/img-2.js
@@ -0,0 +1,355 @@
+/**
+ * Created by Leon Revill on 10/12/2017.
+ * Blog: blog.revillweb.com
+ * Twitter: @RevillWeb
+ * GitHub: github.com/RevillWeb
+ */
+const __style__ = Symbol();
+
+class Img2 extends HTMLElement {
+
+ constructor() {
+ super();
+
+ // Private class variables
+ this._root = null;
+ this._$img = null;
+ this._$preview = null;
+ this._preview = null;
+ this._src = null;
+ this._width = null;
+ this._height = null;
+ this._reset();
+
+ // Settings
+ this._renderOnPreCached = Img2.settings.RENDER_ON_PRECACHED;
+
+ // Bound class methods
+ this._precache = this._precache.bind(this);
+ this._onImgLoad = this._onImgLoad.bind(this);
+ this._onImgPreCached = this._onImgPreCached.bind(this);
+ }
+
+ /**
+ * Reset all private values
+ * @private
+ */
+ _reset() {
+ this._rendered = false;
+ this._loading = false;
+ this._loaded = false;
+ this._preCaching = false;
+ this._preCached = false;
+ }
+
+ [__style__]() {
+ return `
+
+ `;
+ }
+
+ connectedCallback() {
+
+ // Override any global settings
+ this._renderOnPreCached = (this.getAttribute("render-on-pre-cached") === "true");
+ this._init();
+
+ }
+
+ _init() {
+
+ // Check to see if we have a src, if not return and do nothing else
+ this._src = this.getAttribute("src");
+ // Grab the initial attribute values
+ this._preview = this.getAttribute("src-preview");
+ this._width = this.getAttribute("width");
+ this._height = this.getAttribute("height");
+
+ if (!this._src || !this._width || !this._height) return;
+
+ // Set the height and width of the element so that we can figure out if it is on the screen or not
+ this.style.width = `${this._width}px`;
+ this.style.height = `${this._height}px`;
+
+ // Figure out if this image is within view
+ Img2.addIntersectListener(this, () => {
+ Img2._removePreCacheListener(this._precache);
+ this._render();
+ this._load();
+ Img2.removeIntersectListener(this);
+ });
+
+ // Listen for precache instruction
+ Img2._addPreCacheListener(this._precache, this._src);
+
+ }
+
+ /**
+ * Method which displays the image once ready to be displayed
+ * @private
+ */
+ _load() {
+ if (this._preCached === false) Img2._priorityCount += 1;
+ this._$img.onload = this._onImgLoad;
+ this._loading = true;
+ this._$img.src = this._src;
+ }
+
+ _onImgLoad() {
+ this._loading = false;
+ this._loaded = true;
+ if (this._$preview !== null) {
+ this._root.removeChild(this._$preview);
+ this._$preview = null;
+ }
+ this._$img.onload = null;
+ if (this._preCached === false) Img2._priorityCount -= 1;
+ }
+
+ _onImgPreCached() {
+ this._preCaching = false;
+ this._preCached = true;
+ if (this._renderOnPreCached !== false) {
+ this._render();
+ this._load();
+ }
+ }
+
+ static get observedAttributes() {
+ return ["src", "width", "height"];
+ }
+ attributeChangedCallback(name, oldValue, newValue) {
+
+ // If nothing has changed then just return
+ if (newValue === oldValue) return;
+
+ switch (name) {
+ case "src":
+ // If the src is changed then we need to reset and start again
+ this._reset();
+ this._init();
+ break;
+ case "width":
+ this._width = newValue;
+ if (this._$preview !== null) {
+ this._$preview.width = this._width;
+ }
+ if (this._$img !== null) {
+ this._$img.width = this._width;
+ }
+ this.style.width = `${this._width}px`;
+ break;
+ case "height":
+ this._height = newValue;
+ if (this._$preview !== null) {
+ this._$preview.height = this._height;
+ }
+ if (this._$img !== null) {
+ this._$img.height = this._height;
+ }
+ this.style.height = `${this._height}px`;
+ break;
+ case "render-on-pre-cached":
+ this._renderOnPreCached = !(this.getAttribute("render-on-pre-cached") === "false");
+ break;
+ }
+ }
+
+ /**
+ * Method which renders the DOM elements and displays any preview image
+ * @private
+ */
+ _render() {
+
+ if (this._rendered === true) return;
+
+ // Render the Shadow Root if not done already (src change can force this method to be called again)
+ if (this._root === null) {
+ // Attach the Shadow Root to the element
+ this._root = this.attachShadow({mode: "open"});
+ // Create the initial template with styles
+ this._root.innerHTML = `${this[__style__]()}`;
+ }
+
+ // If a preview image has been specified
+ if (this._$preview === null && this._preview !== null && this._loaded === false) {
+ // Create the element
+ this._$preview = document.createElement("img");
+ this._$preview.classList.add("img2-preview");
+ this._$preview.src = this._preview;
+ // Add the specified width and height
+ this._$preview.width = this._width;
+ this._$preview.height = this._height;
+ // Add it to the Shadow Root
+ this._root.appendChild(this._$preview);
+ }
+
+ // Render the img element if not done already
+ if (this._$img === null) {
+ // Create the actual image element to be used to display the image
+ this._$img = document.createElement("img");
+ this._$img.classList.add("img2-src");
+ // add the specified width and height to the image element
+ this._$img.width = this._width;
+ this._$img.height = this._height;
+ // Add the image to the Shadow Root
+ this._root.appendChild(this._$img);
+ }
+
+ // Flag as rendered
+ this._rendered = true;
+
+ }
+
+ _precache() {
+ this._preCaching = true;
+ Img2._preCache(this._src, this._onImgPreCached);
+ }
+
+ static _preCacheListeners = new Map();
+ static _addPreCacheListener(cb, url) {
+ Img2._preCacheListeners.set(cb, url);
+ }
+
+ static _removePreCacheListener(cb) {
+ Img2._preCacheListeners.delete(cb);
+ }
+
+ static _startPreCache() {
+ for (let cb of Img2._preCacheListeners.keys()) cb();
+ }
+
+ /**
+ * Methods used to determine when currently visible (priority) elements have finished download to then inform other elements to pre-cache
+ */
+
+ static __priorityCount = 0;
+ static _startPreCacheDebounce = null;
+ static get _priorityCount() {
+ return Img2.__priorityCount;
+ }
+ static set _priorityCount(value) {
+ Img2.__priorityCount = value;
+ if (Img2.__priorityCount < 1) {
+ // Inform components that they can start to pre-cache their images
+ // Debounce in case the user scrolls because then there will be more priority images
+ if (Img2._startPreCacheDebounce !== null) {
+ clearTimeout(Img2._startPreCacheDebounce);
+ Img2._startPreCacheDebounce = null;
+ }
+ Img2._startPreCacheDebounce = setTimeout(function(){
+ if (Img2.__priorityCount < 1) Img2._startPreCache();
+ }, 500);
+ }
+ }
+
+ /**
+ * Methods used to determine when this element is in the visible viewport
+ */
+ static _intersectListeners = new Map();
+ static _observer = new IntersectionObserver(Img2._handleIntersect, {
+ root: null,
+ rootMargin: "0px",
+ threshold: 0
+ });
+
+ static addIntersectListener($element, intersectCallback) {
+ Img2._intersectListeners.set($element, intersectCallback);
+ Img2._observer.observe($element);
+ }
+
+ static removeIntersectListener($element) {
+ if ($element) Img2._observer.unobserve($element);
+ }
+
+ static _handleIntersect(entries) {
+ entries.forEach(entry => {
+ if (entry.isIntersecting === true) {
+ const cb = Img2._intersectListeners.get(entry.target);
+ if (cb !== undefined) cb(entry);
+ }
+ });
+ }
+
+ static _preCacheCallbacks = {};
+ static _preCache(url, cb) {
+
+ let slot = Img2._preCacheCallbacks[url];
+ if (slot === undefined) {
+ Img2._preCacheCallbacks[url] = {
+ cached: false,
+ cbs: [cb]
+ };
+ const location = (url.indexOf("http") > -1) ? url : window.location.href + url;
+ Img2._worker.postMessage({ location: location, url: url });
+ } else {
+ if (slot.cached === true) {
+ cb();
+ } else {
+ slot.cbs.push(cb);
+ }
+ }
+ }
+}
+
+/**
+ * Methods used to pre-cache images using a WebWorker
+ */
+
+Img2._worker = new Worker(window.URL.createObjectURL(
+ new Blob([`self.onmessage=${function (e) {
+ fetch(e.data.location).then((response) => {
+ if (response.status === 200 || response.status === 0) {
+ return Promise.resolve(response)
+ } else {
+ return Promise.reject(new Error(`Couldn't pre-cache URL '${e.data.url}'.`));
+ }
+ }).then((response) => {
+ return response.blob();
+ }).then(() => {
+ self.postMessage(e.data.url);
+ }).catch(console.error);
+ }.toString()};`], { type: "text/javascript"})
+));
+
+Img2._worker.onmessage = function (e) {
+ const slot = Img2._preCacheCallbacks[e.data];
+ if (slot !== undefined) {
+ slot.cached = true;
+ slot.cbs = slot.cbs.filter(cb => {
+ // Call the callback
+ cb();
+ // Remove the callback
+ return false;
+ });
+ }
+};
+
+/** Img2 Settings **/
+Img2.settings = {
+ "RENDER_ON_PRECACHED": false // Set this to false to save memory but can cause jank during scrolling
+};
+
+window.customElements.define("img-2", Img2);