forked from RevillWeb/img-2
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
RevillWeb
committed
Dec 10, 2017
0 parents
commit 8bc563e
Showing
7 changed files
with
1,295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"presets": [], | ||
"plugins": ["transform-class-properties"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.idea | ||
node_modules |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ` | ||
<style> | ||
:host { | ||
position: relative; | ||
overflow: hidden; | ||
display: inline-block; | ||
outline: none; | ||
} | ||
img { | ||
position: absolute; | ||
} | ||
img.img2-src { | ||
z-index: 1; | ||
} | ||
img.img2-preview { | ||
z-index: 2; | ||
filter: blur(2vw); | ||
transform: scale(1.5); | ||
width: 100%; | ||
height: 100%; | ||
top: 0; | ||
left: 0; | ||
} | ||
</style> | ||
`; | ||
} | ||
|
||
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); |
Oops, something went wrong.