diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3bd5df6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 +indent_style = space + +[*.{yml,json}] +# The indent size used in the `package.json` file cannot be changed +# https://github.com/npm/npm/pull/3180#issuecomment-16336516 +indent_size = 2 +indent_style = space + +[composer.json] +indent_size = 4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1492edc --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Lazyloaded images for SilverStripe + +## Introduction + +This module implements the LQIP (Low Quality Image Placeholder) pattern (combined with a blur-up CSS transition) powered by [lazysizes](https://github.com/aFarkas/lazysizes). + +## Requirements + +* SilverStripe 4 + +## Installation + +```bash +composer require dnadesign/silverstripe-lazyloaded-image +``` + +## Usage + +```html +$Image.Lazyloaded +``` diff --git a/_config/config.yml b/_config/config.yml new file mode 100644 index 0000000..496b944 --- /dev/null +++ b/_config/config.yml @@ -0,0 +1,11 @@ +--- +Name: lazyloadedimage +--- + +SilverStripe\Assets\Image: + extensions: + - DNADesign\LazyloadedImage\LazyloadedImageExtension + +SilverStripe\Assets\Storage\DBFile: + extensions: + - DNADesign\LazyloadedImage\LazyloadedImageExtension diff --git a/client/javascript/lazysizes.min.js b/client/javascript/lazysizes.min.js new file mode 100644 index 0000000..585dd11 --- /dev/null +++ b/client/javascript/lazysizes.min.js @@ -0,0 +1,2 @@ +/*! lazysizes - v4.1.4 */ +!function(a,b){var c=b(a,a.document);a.lazySizes=c,"object"==typeof module&&module.exports&&(module.exports=c)}(window,function(a,b){"use strict";if(b.getElementsByClassName){var c,d,e=b.documentElement,f=a.Date,g=a.HTMLPictureElement,h="addEventListener",i="getAttribute",j=a[h],k=a.setTimeout,l=a.requestAnimationFrame||k,m=a.requestIdleCallback,n=/^picture$/i,o=["load","error","lazyincluded","_lazyloaded"],p={},q=Array.prototype.forEach,r=function(a,b){return p[b]||(p[b]=new RegExp("(\\s|^)"+b+"(\\s|$)")),p[b].test(a[i]("class")||"")&&p[b]},s=function(a,b){r(a,b)||a.setAttribute("class",(a[i]("class")||"").trim()+" "+b)},t=function(a,b){var c;(c=r(a,b))&&a.setAttribute("class",(a[i]("class")||"").replace(c," "))},u=function(a,b,c){var d=c?h:"removeEventListener";c&&u(a,b),o.forEach(function(c){a[d](c,b)})},v=function(a,d,e,f,g){var h=b.createEvent("Event");return e||(e={}),e.instance=c,h.initEvent(d,!f,!g),h.detail=e,a.dispatchEvent(h),h},w=function(b,c){var e;!g&&(e=a.picturefill||d.pf)?(c&&c.src&&!b[i]("srcset")&&b.setAttribute("srcset",c.src),e({reevaluate:!0,elements:[b]})):c&&c.src&&(b.src=c.src)},x=function(a,b){return(getComputedStyle(a,null)||{})[b]},y=function(a,b,c){for(c=c||a.offsetWidth;c49?function(){m(h,{timeout:g}),g!==d.ricTimeout&&(g=d.ricTimeout)}:A(function(){k(h)},!0);return function(a){var d;(a=a===!0)&&(g=33),b||(b=!0,d=e-(f.now()-c),0>d&&(d=0),a||9>d?i():k(i,d))}},C=function(a){var b,c,d=99,e=function(){b=null,a()},g=function(){var a=f.now()-c;d>a?k(g,d-a):(m||e)(e)};return function(){c=f.now(),b||(b=k(g,d))}};!function(){var b,c={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",errorClass:"lazyerror",autosizesClass:"lazyautosizes",srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:!0,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:!0,ricTimeout:0,throttleDelay:125};d=a.lazySizesConfig||a.lazysizesConfig||{};for(b in c)b in d||(d[b]=c[b]);a.lazySizesConfig=d,k(function(){d.init&&F()})}();var D=function(){var g,l,m,o,p,y,D,F,G,H,I,J,K,L,M=/^img$/i,N=/^iframe$/i,O="onscroll"in a&&!/(gle|ing)bot/.test(navigator.userAgent),P=0,Q=0,R=0,S=-1,T=function(a){R--,a&&a.target&&u(a.target,T),(!a||0>R||!a.target)&&(R=0)},U=function(a,c){var d,f=a,g="hidden"==x(b.body,"visibility")||"hidden"!=x(a.parentNode,"visibility")&&"hidden"!=x(a,"visibility");for(F-=c,I+=c,G-=c,H+=c;g&&(f=f.offsetParent)&&f!=b.body&&f!=e;)g=(x(f,"opacity")||1)>0,g&&"visible"!=x(f,"overflow")&&(d=f.getBoundingClientRect(),g=H>d.left&&Gd.top-1&&FR&&(a=r.length)){f=0,S++,null==K&&("expand"in d||(d.expand=e.clientHeight>500&&e.clientWidth>500?500:370),J=d.expand,K=J*d.expFactor),K>Q&&1>R&&S>2&&o>2&&!b.hidden?(Q=K,S=0):Q=o>1&&S>1&&6>R?J:P;for(;a>f;f++)if(r[f]&&!r[f]._lazyRace)if(O)if((p=r[f][i]("data-expand"))&&(m=1*p)||(m=Q),q!==m&&(y=innerWidth+m*L,D=innerHeight+m,n=-1*m,q=m),h=r[f].getBoundingClientRect(),(I=h.bottom)>=n&&(F=h.top)<=D&&(H=h.right)>=n*L&&(G=h.left)<=y&&(I||H||G||F)&&(d.loadHidden||"hidden"!=x(r[f],"visibility"))&&(l&&3>R&&!p&&(3>o||4>S)||U(r[f],m))){if(ba(r[f]),k=!0,R>9)break}else!k&&l&&!j&&4>R&&4>S&&o>2&&(g[0]||d.preloadAfterLoad)&&(g[0]||!p&&(I||H||G||F||"auto"!=r[f][i](d.sizesAttr)))&&(j=g[0]||r[f]);else ba(r[f]);j&&!k&&ba(j)}},W=B(V),X=function(a){s(a.target,d.loadedClass),t(a.target,d.loadingClass),u(a.target,Z),v(a.target,"lazyloaded")},Y=A(X),Z=function(a){Y({target:a.target})},$=function(a,b){try{a.contentWindow.location.replace(b)}catch(c){a.src=b}},_=function(a){var b,c=a[i](d.srcsetAttr);(b=d.customMedia[a[i]("data-media")||a[i]("media")])&&a.setAttribute("media",b),c&&a.setAttribute("srcset",c)},aa=A(function(a,b,c,e,f){var g,h,j,l,o,p;(o=v(a,"lazybeforeunveil",b)).defaultPrevented||(e&&(c?s(a,d.autosizesClass):a.setAttribute("sizes",e)),h=a[i](d.srcsetAttr),g=a[i](d.srcAttr),f&&(j=a.parentNode,l=j&&n.test(j.nodeName||"")),p=b.firesLoad||"src"in a&&(h||g||l),o={target:a},p&&(u(a,T,!0),clearTimeout(m),m=k(T,2500),s(a,d.loadingClass),u(a,Z,!0)),l&&q.call(j.getElementsByTagName("source"),_),h?a.setAttribute("srcset",h):g&&!l&&(N.test(a.nodeName)?$(a,g):a.src=g),f&&(h||l)&&w(a,{src:g})),a._lazyRace&&delete a._lazyRace,t(a,d.lazyClass),z(function(){(!p||a.complete&&a.naturalWidth>1)&&(p?T(o):R--,X(o))},!0)}),ba=function(a){var b,c=M.test(a.nodeName),e=c&&(a[i](d.sizesAttr)||a[i]("sizes")),f="auto"==e;(!f&&l||!c||!a[i]("src")&&!a.srcset||a.complete||r(a,d.errorClass)||!r(a,d.lazyClass))&&(b=v(a,"lazyunveilread").detail,f&&E.updateElem(a,!0,a.offsetWidth),a._lazyRace=!0,R++,aa(a,b,f,e,c))},ca=function(){if(!l){if(f.now()-p<999)return void k(ca,999);var a=C(function(){d.loadMode=3,W()});l=!0,d.loadMode=3,W(),j("scroll",function(){3==d.loadMode&&(d.loadMode=2),a()},!0)}};return{_:function(){p=f.now(),c.elements=b.getElementsByClassName(d.lazyClass),g=b.getElementsByClassName(d.lazyClass+" "+d.preloadClass),L=d.hFac,j("scroll",W,!0),j("resize",W,!0),a.MutationObserver?new MutationObserver(W).observe(e,{childList:!0,subtree:!0,attributes:!0}):(e[h]("DOMNodeInserted",W,!0),e[h]("DOMAttrModified",W,!0),setInterval(W,999)),j("hashchange",W,!0),["focus","mouseover","click","load","transitionend","animationend","webkitAnimationEnd"].forEach(function(a){b[h](a,W,!0)}),/d$|^c/.test(b.readyState)?ca():(j("load",ca),b[h]("DOMContentLoaded",W),k(ca,2e4)),c.elements.length?(V(),z._lsFlush()):W()},checkElems:W,unveil:ba}}(),E=function(){var a,c=A(function(a,b,c,d){var e,f,g;if(a._lazysizesWidth=d,d+="px",a.setAttribute("sizes",d),n.test(b.nodeName||""))for(e=b.getElementsByTagName("source"),f=0,g=e.length;g>f;f++)e[f].setAttribute("sizes",d);c.detail.dataAttr||w(a,c.detail)}),e=function(a,b,d){var e,f=a.parentNode;f&&(d=y(a,f,d),e=v(a,"lazybeforesizes",{width:d,dataAttr:!!b}),e.defaultPrevented||(d=e.detail.width,d&&d!==a._lazysizesWidth&&c(a,f,e,d)))},f=function(){var b,c=a.length;if(c)for(b=0;c>b;b++)e(a[b])},g=C(f);return{_:function(){a=b.getElementsByClassName(d.autosizesClass),j("resize",g)},checkElems:g,updateElem:e}}(),F=function(){F.i||(F.i=!0,E._(),D._())};return c={cfg:d,autoSizer:E,loader:D,init:F,uP:w,aC:s,rC:t,hC:r,fire:v,gW:y,rAF:z}}}); \ No newline at end of file diff --git a/client/javascript/ls.blur-up.min.js b/client/javascript/ls.blur-up.min.js new file mode 100644 index 0000000..a66924d --- /dev/null +++ b/client/javascript/ls.blur-up.min.js @@ -0,0 +1,2 @@ +/*! lazysizes - v4.1.4 */ +!function(a,b){var c=function(){b(a.lazySizes),a.removeEventListener("lazyunveilread",c,!0)};b=b.bind(null,a,a.document),"object"==typeof module&&module.exports?b(require("lazysizes")):a.lazySizes?c():a.addEventListener("lazyunveilread",c,!0)}(window,function(a,b,c){"use strict";var d=[].slice,e=/blur-up["']*\s*:\s*["']*(always|auto|unobtrusive)/,f=/image\/(jpeg|png|gif|svg\+xml)/,g="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",h=function(b){var d=b.getAttribute("data-media")||b.getAttribute("media"),e=b.getAttribute("type");return(!e||f.test(e))&&(!d||a.matchMedia(c.cfg.customMedia[d]||d).matches)},i=function(a,b){var c=a?d.call(a.querySelectorAll("source, img")):[b],e=c.find(function(a){return a.getAttribute("data-lowsrc")&&h(a)});return e&&e.getAttribute("data-lowsrc")},j=function(a,d,e,f){var g,h=!1,i=!1,j="always"==f?0:Date.now(),k=0,l=(a||d).parentNode,m=function(){if(e){var j=function(){h=!0,g&&(c.rAF(function(){g&&c.aC(g,"ls-blur-up-loaded")}),g.removeEventListener("load",j),g.removeEventListener("error",j))};g=b.createElement("img"),g.addEventListener("load",j),g.addEventListener("error",j),g.className="ls-blur-up-img",g.src=e,g.className+=" ls-inview",l.insertBefore(g,(a||d).nextSibling),"always"!=f&&(g.style.visibility="hidden",setTimeout(function(){c.rAF(function(){i||(g.style.visibility="")})},20))}},n=function(){g&&c.rAF(function(){try{g.parentNode.removeChild(g)}catch(a){}g=null})},o=function(a){k++,i=a||i,a?n():k>1&&setTimeout(n,5e3)},p=function(){d.removeEventListener("load",p),d.removeEventListener("error",p),g&&c.rAF(function(){c.aC(g,"ls-original-loaded")}),!h||Date.now()-j<66?o(!0):o()};if(m(),d.addEventListener("load",p),d.addEventListener("error",p),"unobtrusive"==f)o();else{var q=function(a){l==a.target&&(c.aC(g||d,"ls-inview"),o(),l.removeEventListener("lazybeforeunveil",q))};l.getAttribute("data-expand")||l.setAttribute("data-expand",-1),l.addEventListener("lazybeforeunveil",q),c.aC(l,c.cfg.lazyClass)}};a.addEventListener("lazybeforeunveil",function(a){var b=a.detail;if(b.instance==c&&b.blurUp){var d=a.target,e=d.parentNode;"PICTURE"!=e.nodeName&&(e=null),j(e,d,i(e,d)||g,b.blurUp)}}),a.addEventListener("lazyunveilread",function(a){var b=a.detail;if(b.instance==c){var d=a.target,f=(getComputedStyle(d,null)||{fontFamily:""}).fontFamily.match(e);(f||d.getAttribute("data-lowsrc"))&&(b.blurUp=f&&f[1]||"always")}})}); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2d1564d --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "dnadesign/silverstripe-lazyloaded-image", + "description": "Implements the LQIP (Low Quality Image Placeholder) pattern powered by lazysizes", + "type": "silverstripe-vendormodule", + "keywords": [ + "silverstripe", + "lazyload", + "lazysizes", + "image" + ], + "license": "MIT", + "require": { + "silverstripe/vendor-plugin": "^1.0", + "silverstripe/framework": "^4.0", + "chrometoaster/silverstripe-image-quality": "~1.0" + }, + "autoload": { + "psr-4": { + "DNADesign\\LazyloadedImage\\": "src/" + } + }, + "extra": { + "installer-name": "silverstripe-lazyloaded-image", + "expose": [ + "client" + ] + } +} diff --git a/src/LazyloadedImage.php b/src/LazyloadedImage.php new file mode 100644 index 0000000..dd98d9c --- /dev/null +++ b/src/LazyloadedImage.php @@ -0,0 +1,19 @@ +owner->customise([ + 'LQIP' => $this->owner->Quality(20) + ])->renderWith('Includes/LazyloadedImage'); + } +} diff --git a/src/templates/Includes/LazyloadedImage.ss b/src/templates/Includes/LazyloadedImage.ss new file mode 100644 index 0000000..e6cf85b --- /dev/null +++ b/src/templates/Includes/LazyloadedImage.ss @@ -0,0 +1,17 @@ + + +$Title.ATT