From de1ce70c882bbaf396f418fb7426d23293c207b7 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 27 Jul 2020 18:47:59 +0200 Subject: [PATCH 1/5] Added option to replace placeholder (default true) instead of using the wrapper --- core/lazyload/LazyLoad.php | 109 ++++++++++++++---------- core/resources/lazyload/lazyload.src.js | 23 +++-- tests/core/lazyload/LazyLoadTest.php | 8 ++ 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/core/lazyload/LazyLoad.php b/core/lazyload/LazyLoad.php index 7cb8eff46..c37ba3040 100644 --- a/core/lazyload/LazyLoad.php +++ b/core/lazyload/LazyLoad.php @@ -72,61 +72,71 @@ class LazyLoad extends Widget public $options = []; /** - * @var array Legacy support for older Browsers (Adds the IntersectionOberserver Polyfill, default: true) + * @var boolean Legacy support for older Browsers (Adds the IntersectionOberserver Polyfill, default: true) * @since 1.6.0 */ public $legacySupport = true; /** - * @var array Optionally disable the automatic init of the lazyload function so you can override the JS options + * @var boolean Optionally disable the automatic init of the lazyload function so you can override the JS options * @since 1.6.0 */ public $initJs = true; + /** + * @var boolean If set to false, the size will be set by the placeholder (based on width/height). This enables + * smoother fading of the image. Leave on true to have it work with CSS Frameworks like Bootstrap. + * Has no effect if `attributesOnly` is `true`. + * @since 1.6.1 + */ + public $replacePlaceholder = true; + /** * @var string The default classes which will be registered. * @since 1.6.1 */ - public $defaultCss = '.lazyimage-wrapper { - display: block; - width: 100%; - position: relative; - overflow: hidden; - } - .lazyimage { - position: absolute; - top: 50%; - left: 50%; - bottom: 0; - right: 0; - opacity: 0; - height: 100%; - width: 100%; - -webkit-transition: .5s ease-in-out opacity; - transition: .5s ease-in-out opacity; - -webkit-transform: translate(-50%,-50%); - transform: translate(-50%,-50%); - -o-object-fit: cover; - object-fit: cover; - -o-object-position: center center; - object-position: center center; - z-index: 20; - } - .lazyimage.loaded { - opacity: 1; - } - .lazyimage-placeholder { - display: block; - width: 100%; - height: auto; - background-color: #f0f0f0; - } - .nojs .lazyimage, - .nojs .lazyimage-placeholder, - .no-js .lazyimage, - .no-js .lazyimage-placeholder { - display: none; - }'; + public $defaultCss = ' + .lazyimage-wrapper { + display: block; + width: 100%; + position: relative; + overflow: hidden; + } + .lazyimage { + position: absolute; + top: 50%; + left: 50%; + bottom: 0; + right: 0; + opacity: 0; + height: 100%; + width: 100%; + -webkit-transition: .5s ease-in-out opacity; + transition: .5s ease-in-out opacity; + -webkit-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + -o-object-fit: cover; + object-fit: cover; + -o-object-position: center center; + object-position: center center; + z-index: 20; + } + .lazyimage.loaded { + opacity: 1; + } + .lazyimage-placeholder { + display: block; + width: 100%; + height: auto; + background-color: #f0f0f0; + } + .nojs .lazyimage, + .nojs .lazyimage-placeholder, + .no-js .lazyimage, + .no-js .lazyimage-placeholder { + display: none; + } + '; /** * @inheritdoc @@ -155,7 +165,7 @@ public function init() /** * Returns the aspect ration based on height or width. - * + * * If no width or height is provided, the default value 56.25 will be returned. * * @return float A dot seperated ratio value @@ -180,13 +190,22 @@ public function run() } $tag = '
'; - $tag .= Html::tag('img', '', array_merge($this->options, ['class' => 'js-lazyimage lazyimage', 'data-src' => $this->src, 'data-width' => $this->width, 'data-height' => $this->height])); + $tag .= Html::tag('img', '', array_merge( + $this->options, + [ + 'class' => 'js-lazyimage lazyimage', + 'data-src' => $this->src, + 'data-width' => $this->width, + 'data-height' => $this->height, + 'data-replace-placeholder' => $this->replacePlaceholder ? '1' : '0' + ] + )); if ($this->placeholderSrc) { $tag .= Html::tag('img', '', ['class' => 'lazyimage-placeholder', 'src' => $this->placeholderSrc]); } else { $tag .= '
'; } - $tag .= ''; + $tag .= ''; $tag .= '
'; return $tag; diff --git a/core/resources/lazyload/lazyload.src.js b/core/resources/lazyload/lazyload.src.js index 70d95a190..ab0431dbb 100644 --- a/core/resources/lazyload/lazyload.src.js +++ b/core/resources/lazyload/lazyload.src.js @@ -71,6 +71,7 @@ id, el: this, asBackground: !!$(this).data('as-background'), + replacePlaceholder: !!$(this).data('replace-placeholder'), isLoaded: false, isObserved: false }) @@ -128,11 +129,23 @@ } else { // Set the src value // apply the "loaded" class - $el - .attr('src', $el.data('src')) - .addClass('loaded') - .parent(`.${context.imageWrapperClass}`) - .addClass('loaded') + + if (!image.replacePlaceholder) { + $el + .attr('src', $el.data('src')) + .addClass('loaded') + .parent(`.${context.imageWrapperClass}`) + .addClass('loaded') + } else { + const $wrapper = $el.parent(`.${context.imageWrapperClass}`) + $wrapper + .replaceWith( + $el + .removeClass('lazyimage') + .addClass('loaded lazy-image') + .attr('src', $el.data('src')) + ) + } } context.log('Image loaded:', { id: image.id, image }) diff --git a/tests/core/lazyload/LazyLoadTest.php b/tests/core/lazyload/LazyLoadTest.php index e7d8decf1..566fc05be 100644 --- a/tests/core/lazyload/LazyLoadTest.php +++ b/tests/core/lazyload/LazyLoadTest.php @@ -30,4 +30,12 @@ public function testLazyLoadWithHeightCalculationWithNumbers() LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080]) ); } + + public function testLazyLoadAttributesOnly() + { + $this->assertSame( + 'class="js-lazyimage add" data-src="abc.jpg" data-width="1080" data-height="1920" data-as-background="1"', + LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080, 'attributesOnly' => true]) + ); + } } From b94d47134576616a4436962d1e4ccfe01e44554f Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 27 Jul 2020 18:51:05 +0200 Subject: [PATCH 2/5] Updated unit tests --- tests/core/lazyload/LazyLoadTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/lazyload/LazyLoadTest.php b/tests/core/lazyload/LazyLoadTest.php index 566fc05be..40ad1ec64 100644 --- a/tests/core/lazyload/LazyLoadTest.php +++ b/tests/core/lazyload/LazyLoadTest.php @@ -10,7 +10,7 @@ class LazyLoadTest extends LuyaWebTestCase public function testWidget() { $this->assertSame( - '
', + '
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add']) ); } @@ -18,7 +18,7 @@ public function testWidget() public function testWidgetWithOptions() { $this->assertSame( - '
The image alt
', + '
The image alt
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'options' => ['alt' => 'The image alt', 'title' => 'The image title']]) ); } @@ -26,7 +26,7 @@ public function testWidgetWithOptions() public function testLazyLoadWithHeightCalculationWithNumbers() { $this->assertSame( - '
', + '
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080]) ); } From 2f39957c2984f2cda55bc945054499fd14e174fe Mon Sep 17 00:00:00 2001 From: Marc Stampfli Date: Tue, 28 Jul 2020 18:28:50 +0200 Subject: [PATCH 3/5] Fixed Polyfill; Improved replace behaviour --- core/lazyload/LazyLoad.php | 18 +- .../intersectionOberserver.polyfill.js | 2 +- .../intersectionObserver.polyfill.src.js | 576 ++++++++++++++++++ core/resources/lazyload/lazyload.js | 2 +- core/resources/lazyload/lazyload.src.js | 436 ++++++------- tests/core/lazyload/LazyLoadTest.php | 14 +- 6 files changed, 817 insertions(+), 231 deletions(-) diff --git a/core/lazyload/LazyLoad.php b/core/lazyload/LazyLoad.php index c37ba3040..77294f427 100644 --- a/core/lazyload/LazyLoad.php +++ b/core/lazyload/LazyLoad.php @@ -85,8 +85,8 @@ class LazyLoad extends Widget /** * @var boolean If set to false, the size will be set by the placeholder (based on width/height). This enables - * smoother fading of the image. Leave on true to have it work with CSS Frameworks like Bootstrap. - * Has no effect if `attributesOnly` is `true`. + * smoother fading of the image. Leave on true to have it work with CSS Frameworks like Bootstrap. + * Has no effect if `attributesOnly` is `true`. * @since 1.6.1 */ public $replacePlaceholder = true; @@ -128,7 +128,6 @@ class LazyLoad extends Widget display: block; width: 100%; height: auto; - background-color: #f0f0f0; } .nojs .lazyimage, .nojs .lazyimage-placeholder, @@ -152,12 +151,15 @@ public function init() // register the asset file if ($this->legacySupport) { IntersectionObserverPolyfillAsset::register($this->view); + $this->view->registerJs("IntersectionObserver.prototype.POLL_INTERVAL = 100;", View::POS_READY); } LazyLoadAsset::register($this->view); if ($this->initJs) { // register js and css code with keys in order to ensure the registration is done only once - $this->view->registerJs("$.lazyLoad();", View::POS_READY, self::JS_ASSET_KEY); + $this->view->registerJs(" + $.lazyLoad(); + ", View::POS_READY, self::JS_ASSET_KEY); } $this->view->registerCss($this->defaultCss, [], self::CSS_ASSET_KEY); @@ -166,7 +168,7 @@ public function init() /** * Returns the aspect ration based on height or width. * - * If no width or height is provided, the default value 56.25 will be returned. + * If no width or height is provided, the default value 0 will be returned. * * @return float A dot seperated ratio value * @since 1.6.1 @@ -186,14 +188,14 @@ public function run() } if ($this->attributesOnly && !$this->placeholderSrc) { - return "class=\"js-lazyimage {$this->extraClass}\" data-src=\"$this->src\" data-width=\"$this->width\" data-height=\"$this->height\" data-as-background=\"1\""; + return "class=\"js-lazyimage $this->extraClass\" data-src=\"$this->src\" data-width=\"$this->width\" data-height=\"$this->height\" data-as-background=\"1\""; } $tag = '
'; $tag .= Html::tag('img', '', array_merge( $this->options, [ - 'class' => 'js-lazyimage lazyimage', + 'class' => 'js-lazyimage lazyimage' . ($this->replacePlaceholder ? (' ' . $this->extraClass) : ''), 'data-src' => $this->src, 'data-width' => $this->width, 'data-height' => $this->height, @@ -210,4 +212,4 @@ public function run() return $tag; } -} +} \ No newline at end of file diff --git a/core/resources/lazyload/intersectionOberserver.polyfill.js b/core/resources/lazyload/intersectionOberserver.polyfill.js index 4da01af75..a106de209 100644 --- a/core/resources/lazyload/intersectionOberserver.polyfill.js +++ b/core/resources/lazyload/intersectionOberserver.polyfill.js @@ -1 +1 @@ -function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}!function(){"use strict";if("object"===("undefined"==typeof window?"undefined":_typeof(window)))if("IntersectionObserver"in window&&"IntersectionObserverEntry"in window&&"intersectionRatio"in window.IntersectionObserverEntry.prototype)"isIntersecting"in window.IntersectionObserverEntry.prototype||Object.defineProperty(window.IntersectionObserverEntry.prototype,"isIntersecting",{get:function(){return 0<\/script>"),e.close(),t=e.parentWindow.Object.prototype,e=null,t}():function(){var t,e=document.createElement("iframe"),n=document.body||document.documentElement;return e.style.display="none",n.appendChild(e),e.src="javascript:",t=e.contentWindow.Object.prototype,n.removeChild(e),e=null,t}();delete t.constructor,delete t.hasOwnProperty,delete t.propertyIsEnumerable,delete t.isPrototypeOf,delete t.toLocaleString,delete t.toString,delete t.valueOf;function e(){}return e.prototype=t,r=function(){return new e},new e},Object.create=function(t,e){function n(){}var o;if(null===t)o=r();else{if(d(t))throw new TypeError("Object prototype may only be an Object or null");n.prototype=t,(o=new n).__proto__=t}return void 0!==e&&Object.defineProperties(o,e),o}}function y(t){try{return Object.defineProperty(t,"sentinel",{}),"sentinel"in t}catch(t){return!1}}if(Object.defineProperty){var _=y({}),m="undefined"==typeof document||y(document.createElement("div"));if(!_||!m)var g=Object.defineProperty,w=Object.defineProperties}if(!Object.defineProperty||g){Object.defineProperty=function(t,e,n){if(d(t))throw new TypeError("Object.defineProperty called on non-object: "+t);if(d(n))throw new TypeError("Property description must be an object: "+n);if(g)try{return g.call(Object,t,e,n)}catch(t){}if("value"in n)if(p&&(u(t,e)||l(t,e))){var o=t.__proto__;t.__proto__=h,delete t[e],t[e]=n.value,t.__proto__=o}else t[e]=n.value;else{var r="get"in n,i="set"in n;if(!p&&(r||i))throw new TypeError("getters & setters can not be defined on this javascript engine");r&&s(t,e,n.get),i&&c(t,e,n.set)}return t}}Object.defineProperties&&!w||(Object.defineProperties=function(e,n){if(w)try{return w.call(Object,e,n)}catch(t){}return Object.keys(n).forEach(function(t){"__proto__"!==t&&Object.defineProperty(e,t,n[t])}),e}),Object.seal||(Object.seal=function(t){if(Object(t)!==t)throw new TypeError("Object.seal can only be called on Objects.");return t}),Object.freeze||(Object.freeze=function(t){if(Object(t)!==t)throw new TypeError("Object.freeze can only be called on Objects.");return t});try{Object.freeze(function(){})}catch(t){Object.freeze=function(e){return function(t){return"function"==typeof t?t:e(t)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(t){if(Object(t)!==t)throw new TypeError("Object.preventExtensions can only be called on Objects.");return t}),Object.isSealed||(Object.isSealed=function(t){if(Object(t)!==t)throw new TypeError("Object.isSealed can only be called on Objects.");return!1}),Object.isFrozen||(Object.isFrozen=function(t){if(Object(t)!==t)throw new TypeError("Object.isFrozen can only be called on Objects.");return!1}),Object.isExtensible||(Object.isExtensible=function(t){if(Object(t)!==t)throw new TypeError("Object.isExtensible can only be called on Objects.");for(var e="";a(t,e);)e+="?";t[e]=!0;var n=a(t,e);return delete t[e],n})}),function(){"use strict";if("object"===("undefined"==typeof window?"undefined":_typeof(window)))if("IntersectionObserver"in window&&"IntersectionObserverEntry"in window&&"intersectionRatio"in window.IntersectionObserverEntry.prototype)"isIntersecting"in window.IntersectionObserverEntry.prototype||Object.defineProperty(window.IntersectionObserverEntry.prototype,"isIntersecting",{get:function(){return 0 not a reliable check + // Object.prototype.__proto__ === null + + // Check for document.domain and active x support + // No need to use active x approach when document.domain is not set + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + /* global ActiveXObject */ + var shouldUseActiveX = function shouldUseActiveX() { + // return early if document.domain not set + if (!document.domain) { + return false; + } + + try { + return !!new ActiveXObject('htmlfile'); + } catch (exception) { + return false; + } + }; + + // This supports IE8 when document.domain is used + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + var getEmptyViaActiveX = function getEmptyViaActiveX() { + var empty; + var xDoc; + + xDoc = new ActiveXObject('htmlfile'); + + var script = 'script'; + xDoc.write('<' + script + '>'); + xDoc.close(); + + empty = xDoc.parentWindow.Object.prototype; + xDoc = null; + + return empty; + }; + + // The original implementation using an iframe + // before the activex approach was added + // see https://github.com/es-shims/es5-shim/issues/150 + var getEmptyViaIFrame = function getEmptyViaIFrame() { + var iframe = document.createElement('iframe'); + var parent = document.body || document.documentElement; + var empty; + + iframe.style.display = 'none'; + parent.appendChild(iframe); + // eslint-disable-next-line no-script-url + iframe.src = 'javascript:'; + + empty = iframe.contentWindow.Object.prototype; + parent.removeChild(iframe); + iframe = null; + + return empty; + }; + + /* global document */ + if (supportsProto || typeof document === 'undefined') { + createEmpty = function () { + return { __proto__: null }; + }; + } else { + // In old IE __proto__ can't be used to manually set `null`, nor does + // any other method exist to make an object that inherits from nothing, + // aside from Object.prototype itself. Instead, create a new global + // object and *steal* its Object.prototype and strip it bare. This is + // used as the prototype to create nullary objects. + createEmpty = function () { + // Determine which approach to use + // see https://github.com/es-shims/es5-shim/issues/150 + var empty = shouldUseActiveX() ? getEmptyViaActiveX() : getEmptyViaIFrame(); + + delete empty.constructor; + delete empty.hasOwnProperty; + delete empty.propertyIsEnumerable; + delete empty.isPrototypeOf; + delete empty.toLocaleString; + delete empty.toString; + delete empty.valueOf; + + var Empty = function Empty() {}; + Empty.prototype = empty; + // short-circuit future calls + createEmpty = function () { + return new Empty(); + }; + return new Empty(); + }; + } + + Object.create = function create(prototype, properties) { + + var object; + var Type = function Type() {}; // An empty constructor. + + if (prototype === null) { + object = createEmpty(); + } else if (isPrimitive(prototype)) { + // In the native implementation `parent` can be `null` + // OR *any* `instanceof Object` (Object|Function|Array|RegExp|etc) + // Use `typeof` tho, b/c in old IE, DOM elements are not `instanceof Object` + // like they are in modern browsers. Using `Object.create` on DOM elements + // is...err...probably inappropriate, but the native version allows for it. + throw new TypeError('Object prototype may only be an Object or null'); // same msg as Chrome + } else { + Type.prototype = prototype; + object = new Type(); + // IE has no built-in implementation of `Object.getPrototypeOf` + // neither `__proto__`, but this manually setting `__proto__` will + // guarantee that `Object.getPrototypeOf` will work as expected with + // objects created using `Object.create` + // eslint-disable-next-line no-proto + object.__proto__ = prototype; + } + + if (properties !== void 0) { + Object.defineProperties(object, properties); + } + + return object; + }; + } + + // ES5 15.2.3.6 + // http://es5.github.com/#x15.2.3.6 + + // Patch for WebKit and IE8 standard mode + // Designed by hax + // related issue: https://github.com/es-shims/es5-shim/issues#issue/5 + // IE8 Reference: + // http://msdn.microsoft.com/en-us/library/dd282900.aspx + // http://msdn.microsoft.com/en-us/library/dd229916.aspx + // WebKit Bugs: + // https://bugs.webkit.org/show_bug.cgi?id=36423 + + var doesDefinePropertyWork = function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, 'sentinel', {}); + return 'sentinel' in object; + } catch (exception) { + return false; + } + }; + + // check whether defineProperty works if it's given. Otherwise, + // shim partially. + if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document === 'undefined' + || doesDefinePropertyWork(document.createElement('div')); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty, + definePropertiesFallback = Object.defineProperties; + } + } + + if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = 'Property description must be an object: '; + var ERR_NON_OBJECT_TARGET = 'Object.defineProperty called on non-object: '; + var ERR_ACCESSORS_NOT_SUPPORTED = 'getters & setters can not be defined on this javascript engine'; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if (isPrimitive(object)) { + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + } + if (isPrimitive(descriptor)) { + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + } + // make a valiant attempt to use the real defineProperty + // for I8's DOM elements. + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + // try the shim if the real one doesn't work + } + } + + // If it's a data property. + if ('value' in descriptor) { + // fail silently if 'writable', 'enumerable', or 'configurable' + // are requested but not supported + /* + // alternate approach: + if ( // can't implement these features; allow false but not true + ('writable' in descriptor && !descriptor.writable) || + ('enumerable' in descriptor && !descriptor.enumerable) || + ('configurable' in descriptor && !descriptor.configurable) + )) + throw new RangeError( + 'This implementation of Object.defineProperty does not support configurable, enumerable, or writable.' + ); + */ + + if (supportsAccessors && (lookupGetter(object, property) || lookupSetter(object, property))) { + // As accessors are supported only on engines implementing + // `__proto__` we can safely override `__proto__` while defining + // a property to make sure that we don't hit an inherited + // accessor. + /* eslint-disable no-proto, no-param-reassign */ + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + // Deleting a property anyway since getter / setter may be + // defined on object itself. + delete object[property]; + object[property] = descriptor.value; + // Setting original `__proto__` back now. + object.__proto__ = prototype; + /* eslint-enable no-proto, no-param-reassign */ + } else { + object[property] = descriptor.value; // eslint-disable-line no-param-reassign + } + } else { + var hasGetter = 'get' in descriptor; + var hasSetter = 'set' in descriptor; + if (!supportsAccessors && (hasGetter || hasSetter)) { + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + } + // If we got that far then getters and setters can be defined !! + if (hasGetter) { + defineGetter(object, property, descriptor.get); + } + if (hasSetter) { + defineSetter(object, property, descriptor.set); + } + } + return object; + }; + } + + // ES5 15.2.3.7 + // http://es5.github.com/#x15.2.3.7 + if (!Object.defineProperties || definePropertiesFallback) { + Object.defineProperties = function defineProperties(object, properties) { + // make a valiant attempt to use the real defineProperties + if (definePropertiesFallback) { + try { + return definePropertiesFallback.call(Object, object, properties); + } catch (exception) { + // try the shim if the real one doesn't work + } + } + + Object.keys(properties).forEach(function (property) { + if (property !== '__proto__') { + Object.defineProperty(object, property, properties[property]); + } + }); + return object; + }; + } + + // ES5 15.2.3.8 + // http://es5.github.com/#x15.2.3.8 + if (!Object.seal) { + Object.seal = function seal(object) { + if (Object(object) !== object) { + throw new TypeError('Object.seal can only be called on Objects.'); + } + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; + } + + // ES5 15.2.3.9 + // http://es5.github.com/#x15.2.3.9 + if (!Object.freeze) { + Object.freeze = function freeze(object) { + if (Object(object) !== object) { + throw new TypeError('Object.freeze can only be called on Objects.'); + } + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; + } + + // detect a Rhino bug and patch it + try { + Object.freeze(function () {}); + } catch (exception) { + Object.freeze = (function (freezeObject) { + return function freeze(object) { + if (typeof object === 'function') { + return object; + } else { + return freezeObject(object); + } + }; + }(Object.freeze)); + } + + // ES5 15.2.3.10 + // http://es5.github.com/#x15.2.3.10 + if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + if (Object(object) !== object) { + throw new TypeError('Object.preventExtensions can only be called on Objects.'); + } + // this is misleading and breaks feature-detection, but + // allows "securable" code to "gracefully" degrade to working + // but insecure code. + return object; + }; + } + + // ES5 15.2.3.11 + // http://es5.github.com/#x15.2.3.11 + if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + if (Object(object) !== object) { + throw new TypeError('Object.isSealed can only be called on Objects.'); + } + return false; + }; + } + + // ES5 15.2.3.12 + // http://es5.github.com/#x15.2.3.12 + if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + if (Object(object) !== object) { + throw new TypeError('Object.isFrozen can only be called on Objects.'); + } + return false; + }; + } + + // ES5 15.2.3.13 + // http://es5.github.com/#x15.2.3.13 + if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + // 1. If Type(O) is not Object throw a TypeError exception. + if (Object(object) !== object) { + throw new TypeError('Object.isExtensible can only be called on Objects.'); + } + // 2. Return the Boolean value of the [[Extensible]] internal property of O. + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; // eslint-disable-line no-param-reassign + var returnValue = owns(object, name); + delete object[name]; // eslint-disable-line no-param-reassign + return returnValue; + }; + } + +})); + /** * Copyright 2016 Google Inc. All Rights Reserved. * diff --git a/core/resources/lazyload/lazyload.js b/core/resources/lazyload/lazyload.js index 7c5a9067b..991f9e4ad 100644 --- a/core/resources/lazyload/lazyload.js +++ b/core/resources/lazyload/lazyload.js @@ -1 +1 @@ -function _typeof(e){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}!function(n){var t={debug:!1,initialized:!1,imageSelector:".js-lazyimage",imageClass:"lazyimage",imageWrapperClass:"lazyimage-wrapper",placeholderClass:"lazyimage-placeholder",loaderHtml:'
',images:[],observer:null,observerOptions:{root:document,rootMargin:"200px 0px 200px",threshold:.01},latestId:0,getId:function(){return this.latestId+=1},getImageById:function(t){return this.images.find(function(e){return e.id===t})},collectImages:function(){var t=this;n(this.imageSelector).each(function(){if(void 0===n(this).data("lazy-id")){var e=t.getId();n(this).data("lazy-id",e).addClass("".concat(t.imageClass,"-").concat(e)),t.images.push({id:e,el:this,asBackground:!!n(this).data("as-background"),isLoaded:!1,isObserved:!1})}}),this.log("Images",this.images)},observeImages:function(){this.observer||this.initObserver();var e=!0,t=!1,i=void 0;try{for(var a,r=this.images[Symbol.iterator]();!(e=(a=r.next()).done);e=!0){var o=a.value;o.isObserved||o.isLoaded||(this.observer.observe(o.el),o.isObserved=!0)}}catch(e){t=!0,i=e}finally{try{e||null==r.return||r.return()}finally{if(t)throw i}}},imageIntersects:function(e){var t=!0,i=!1,a=void 0;try{for(var r,o=e[Symbol.iterator]();!(t=(r=o.next()).done);t=!0){var s=r.value;s.isIntersecting&&(this.observer.unobserve(s.target),this.loadImage(this.getImageById(n(s.target).data("lazy-id"))))}}catch(e){i=!0,a=e}finally{try{t||null==o.return||o.return()}finally{if(i)throw a}}},loadImage:function(t){var i=this;this.log("Image loading:",{id:t.id,image:t}),n(document).trigger("lazyimage-loading",{image:t});var e=n("");e.on("load",function(){t.isLoaded=!0;var e=n(t.el);t.asBackground?e.css({backgroundImage:"url("+e.data("src")+")"}):e.attr("src",e.data("src")).addClass("loaded").parent(".".concat(i.imageWrapperClass)).addClass("loaded"),i.log("Image loaded:",{id:t.id,image:t}),n(document).trigger("lazyimage-loaded",{type:"success",image:t})}),e.on("error",function(){i.log("Image load error:",{id:t.id,image:t}),n(document).trigger("lazyimage-loaded",{type:"error",image:t})}),e.attr("src",n(t.el).data("src"))},initObserver:function(){this.observer=new IntersectionObserver(this.imageIntersects.bind(this),this.observerOptions)},init:function(e){for(var t in this.log("Init, options:",e||{}),e)this.hasOwnProperty(t)&&"function"!=typeof this[t]&&("object"===_typeof(this[t])?this[t]=n.extend(this[t],e[t]):this[t]=e[t]);this.initialized||(this.collectImages(),this.initObserver(),this.observeImages()),this.initialized=!0},log:function(){var e;this.debug&&(e=console).log.apply(e,arguments)}};n.lazyLoad=function(e){if("string"!=typeof e)return t.init(e),this;switch(e){case"refetchElements":case"collectImages":t.collectImages(),t.observeImages()}}}(jQuery); \ No newline at end of file +function _typeof(e){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}!function(n){var t={debug:!1,initialized:!1,imageSelector:".js-lazyimage",imageClass:"lazyimage",imageWrapperClass:"lazyimage-wrapper",placeholderClass:"lazyimage-placeholder",loaderHtml:'
',images:[],observer:null,observerOptions:{root:document.getElementsByTagName("body")[0],rootMargin:"200px 0px 200px",threshold:.01},latestId:0,getId:function(){return this.latestId+=1},getImageById:function(t){return this.images.find(function(e){return e.id===t})},collectImages:function(){var t=this;n(this.imageSelector).each(function(){if(void 0===n(this).data("lazy-id")){var e=t.getId();n(this).data("lazy-id",e).addClass("".concat(t.imageClass,"-").concat(e)),t.images.push({id:e,el:this,asBackground:!!n(this).data("as-background"),replacePlaceholder:!!n(this).data("replace-placeholder"),isLoaded:!1,isObserved:!1})}}),this.log("Images",this.images)},observeImages:function(){this.observer||this.initObserver();var e=!0,t=!1,a=void 0;try{for(var i,r=this.images[Symbol.iterator]();!(e=(i=r.next()).done);e=!0){var s=i.value;s.isObserved||s.isLoaded||(this.observer.observe(s.el),s.isObserved=!0)}}catch(e){t=!0,a=e}finally{try{e||null==r.return||r.return()}finally{if(t)throw a}}},imageIntersects:function(e){var t=!0,a=!1,i=void 0;try{for(var r,s=e[Symbol.iterator]();!(t=(r=s.next()).done);t=!0){var o=r.value;o.isIntersecting&&(this.observer.unobserve(o.target),this.loadImage(this.getImageById(n(o.target).data("lazy-id"))))}}catch(e){a=!0,i=e}finally{try{t||null==s.return||s.return()}finally{if(a)throw i}}},loadImage:function(t){var a=this;this.log("Image loading:",{id:t.id,image:t}),n(document).trigger("lazyimage-loading",{image:t});var e=n("");e.on("load",function(){t.isLoaded=!0;var e=n(t.el);t.asBackground?e.css({backgroundImage:"url("+e.data("src")+")"}):t.replacePlaceholder?e.parent(".".concat(a.imageWrapperClass)).replaceWith(e.removeClass("lazyimage").addClass("loaded lazy-image").attr("src",e.data("src"))):e.attr("src",e.data("src")).addClass("loaded").parent(".".concat(a.imageWrapperClass)).addClass("loaded");a.log("Image loaded:",{id:t.id,image:t}),n(document).trigger("lazyimage-loaded",{type:"success",image:t})}),e.on("error",function(){a.log("Image load error:",{id:t.id,image:t}),n(document).trigger("lazyimage-loaded",{type:"error",image:t})}),e.attr("src",n(t.el).data("src"))},initObserver:function(){this.observer=new IntersectionObserver(this.imageIntersects.bind(this),this.observerOptions)},init:function(e){for(var t in this.log("Init, options:",e||{}),e)this.hasOwnProperty(t)&&"function"!=typeof this[t]&&("object"===_typeof(this[t])?this[t]=n.extend(this[t],e[t]):this[t]=e[t]);this.initialized||(this.collectImages(),this.initObserver(),this.observeImages()),this.initialized=!0},log:function(){var e;this.debug&&(e=console).log.apply(e,arguments)}};n.lazyLoad=function(e){if("string"!=typeof e)return t.init(e),this;switch(e){case"refetchElements":case"collectImages":t.collectImages(),t.observeImages()}}}(jQuery); \ No newline at end of file diff --git a/core/resources/lazyload/lazyload.src.js b/core/resources/lazyload/lazyload.src.js index ab0431dbb..0e5f21c0e 100644 --- a/core/resources/lazyload/lazyload.src.js +++ b/core/resources/lazyload/lazyload.src.js @@ -5,221 +5,221 @@ (function ($) { - const lazyload = { - - debug: false, - initialized: false, - - // Used to identify all lazyload images - // including background images - imageSelector: '.js-lazyimage', - // The image class applied to all regular - // lazyload images - imageClass: 'lazyimage', - // The wrapper class around the lazyimage - // used for positioning and - // aspect ratio - imageWrapperClass: 'lazyimage-wrapper', - // The placeholder div that includes the - // loader html - placeholderClass: 'lazyimage-placeholder', - // The loader html, customize according - // to your needs - loaderHtml: '
', - - images: [], - observer: null, - observerOptions: { - root: document, - rootMargin: '200px 0px 200px', - threshold: 0.01 - }, - - latestId: 0, - - // Return a new id based on - // latestId - getId() { - return this.latestId += 1 - }, - - /** - * Returns the image object from the - * images array based on the image id - * - * @param integer id - * @returns object - */ - getImageById(id) { - return this.images.find(image => image.id === id) - }, - - /** - * Collects all images on page - * and writes them in the images array - */ - collectImages() { - const context = this - $(this.imageSelector).each(function() { - if ($(this).data('lazy-id') === undefined) { - const id = context.getId() - $(this) - .data('lazy-id', id) - .addClass(`${context.imageClass}-${id}`) - - context.images.push({ - id, - el: this, - asBackground: !!$(this).data('as-background'), - replacePlaceholder: !!$(this).data('replace-placeholder'), - isLoaded: false, - isObserved: false - }) - } - }) - - this.log('Images', this.images) - }, - - observeImages() { - if (!this.observer) { - this.initObserver() - } - - for (const image of this.images) { - if (!image.isObserved && !image.isLoaded) { - this.observer.observe(image.el) - image.isObserved = true - } - } - }, - - imageIntersects(entries) { - for (const entry of entries) { - if (entry.isIntersecting) { - this.observer.unobserve(entry.target) - this.loadImage(this.getImageById($(entry.target).data('lazy-id'))) - } - } - }, - - loadImage(image) { - const context = this - - this.log('Image loading:', { id: image.id, image }) - - $(document).trigger('lazyimage-loading', { - image - }) - - const $loadImage = $('') - - // If the image was loaded successfully - $loadImage.on('load', function () { - image.isLoaded = true - - const $el = $(image.el) - - if (image.asBackground) { - // If the image is a background-image we need to update the - // original div with the background image url - $el.css({ - backgroundImage: 'url(' + $el.data('src') + ')' - }) - } else { - // Set the src value - // apply the "loaded" class - - if (!image.replacePlaceholder) { - $el - .attr('src', $el.data('src')) - .addClass('loaded') - .parent(`.${context.imageWrapperClass}`) - .addClass('loaded') - } else { - const $wrapper = $el.parent(`.${context.imageWrapperClass}`) - $wrapper - .replaceWith( - $el - .removeClass('lazyimage') - .addClass('loaded lazy-image') - .attr('src', $el.data('src')) - ) - } - } - - context.log('Image loaded:', { id: image.id, image }) - - // Trigger a success event - $(document).trigger("lazyimage-loaded", { - type: 'success', - image - }) - }) - - // If the image can't be loaded - $loadImage.on('error', function () { - context.log('Image load error:', { id: image.id, image }) - - // Trigger a error event - $(document).trigger("lazyimage-loaded", { - type: 'error', - image - }) - }) - - // Load the image - $loadImage.attr('src', $(image.el).data('src')) - }, - - initObserver() { - this.observer = new IntersectionObserver(this.imageIntersects.bind(this), this.observerOptions) - }, - - init(options) { - this.log('Init, options:', options || {}) - - // Extend the lazyload object options - for (const option in options) { - if (this.hasOwnProperty(option) && typeof this[option] !== 'function') { - if (typeof this[option] === 'object') { - this[option] = $.extend(this[option], options[option]) - } else { - this[option] = options[option] - } - } - } - - // If not initialized already, do so - if (!this.initialized) { - this.collectImages() - this.initObserver() - this.observeImages() - } - - this.initialized = true - }, - - log() { - if (this.debug) { - console.log(...arguments) - } - } - } - - $.lazyLoad = function (options) { - if(typeof options === 'string') { - switch(options) { - case 'refetchElements': - case 'collectImages': - lazyload.collectImages() - lazyload.observeImages() - break; - } - } else { - lazyload.init(options) - return this - } - } - -}(jQuery)) + const lazyload = { + + debug: false, + initialized: false, + + // Used to identify all lazyload images + // including background images + imageSelector: '.js-lazyimage', + // The image class applied to all regular + // lazyload images + imageClass: 'lazyimage', + // The wrapper class around the lazyimage + // used for positioning and + // aspect ratio + imageWrapperClass: 'lazyimage-wrapper', + // The placeholder div that includes the + // loader html + placeholderClass: 'lazyimage-placeholder', + // The loader html, customize according + // to your needs + loaderHtml: '
', + + images: [], + observer: null, + observerOptions: { + root: document.getElementsByTagName('body')[0], + rootMargin: '200px 0px 200px', + threshold: 0.01 + }, + + latestId: 0, + + // Return a new id based on + // latestId + getId() { + return this.latestId += 1 + }, + + /** + * Returns the image object from the + * images array based on the image id + * + * @param integer id + * @returns object + */ + getImageById(id) { + return this.images.find(image => image.id === id) + }, + + /** + * Collects all images on page + * and writes them in the images array + */ + collectImages() { + const context = this + $(this.imageSelector).each(function() { + if ($(this).data('lazy-id') === undefined) { + const id = context.getId() + $(this) + .data('lazy-id', id) + .addClass(`${context.imageClass}-${id}`) + + context.images.push({ + id, + el: this, + asBackground: !!$(this).data('as-background'), + replacePlaceholder: !!$(this).data('replace-placeholder'), + isLoaded: false, + isObserved: false + }) + } + }) + + this.log('Images', this.images) + }, + + observeImages() { + if (!this.observer) { + this.initObserver() + } + + for (const image of this.images) { + if (!image.isObserved && !image.isLoaded) { + this.observer.observe(image.el) + image.isObserved = true + } + } + }, + + imageIntersects(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this.observer.unobserve(entry.target) + this.loadImage(this.getImageById($(entry.target).data('lazy-id'))) + } + } + }, + + loadImage(image) { + const context = this + + this.log('Image loading:', { id: image.id, image }) + + $(document).trigger('lazyimage-loading', { + image + }) + + const $loadImage = $('') + + // If the image was loaded successfully + $loadImage.on('load', function () { + image.isLoaded = true + + const $el = $(image.el) + + if (image.asBackground) { + // If the image is a background-image we need to update the + // original div with the background image url + $el.css({ + backgroundImage: 'url(' + $el.data('src') + ')' + }) + } else { + // Set the src value + // apply the "loaded" class + + if (!image.replacePlaceholder) { + $el + .attr('src', $el.data('src')) + .addClass('loaded') + .parent(`.${context.imageWrapperClass}`) + .addClass('loaded') + } else { + const $wrapper = $el.parent(`.${context.imageWrapperClass}`) + $wrapper + .replaceWith( + $el + .removeClass('lazyimage') + .addClass('loaded lazy-image') + .attr('src', $el.data('src')) + ) + } + } + + context.log('Image loaded:', { id: image.id, image }) + + // Trigger a success event + $(document).trigger("lazyimage-loaded", { + type: 'success', + image + }) + }) + + // If the image can't be loaded + $loadImage.on('error', function () { + context.log('Image load error:', { id: image.id, image }) + + // Trigger a error event + $(document).trigger("lazyimage-loaded", { + type: 'error', + image + }) + }) + + // Load the image + $loadImage.attr('src', $(image.el).data('src')) + }, + + initObserver() { + this.observer = new IntersectionObserver(this.imageIntersects.bind(this), this.observerOptions) + }, + + init(options) { + this.log('Init, options:', options || {}) + + // Extend the lazyload object options + for (const option in options) { + if (this.hasOwnProperty(option) && typeof this[option] !== 'function') { + if (typeof this[option] === 'object') { + this[option] = $.extend(this[option], options[option]) + } else { + this[option] = options[option] + } + } + } + + // If not initialized already, do so + if (!this.initialized) { + this.collectImages() + this.initObserver() + this.observeImages() + } + + this.initialized = true + }, + + log() { + if (this.debug) { + console.log(...arguments) + } + } + } + + $.lazyLoad = function (options) { + if(typeof options === 'string') { + switch(options) { + case 'refetchElements': + case 'collectImages': + lazyload.collectImages() + lazyload.observeImages() + break; + } + } else { + lazyload.init(options) + return this + } + } + +}(jQuery)) \ No newline at end of file diff --git a/tests/core/lazyload/LazyLoadTest.php b/tests/core/lazyload/LazyLoadTest.php index 40ad1ec64..15e5950a0 100644 --- a/tests/core/lazyload/LazyLoadTest.php +++ b/tests/core/lazyload/LazyLoadTest.php @@ -10,7 +10,7 @@ class LazyLoadTest extends LuyaWebTestCase public function testWidget() { $this->assertSame( - '
', + '
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add']) ); } @@ -18,7 +18,7 @@ public function testWidget() public function testWidgetWithOptions() { $this->assertSame( - '
The image alt
', + '
The image alt
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'options' => ['alt' => 'The image alt', 'title' => 'The image title']]) ); } @@ -26,7 +26,7 @@ public function testWidgetWithOptions() public function testLazyLoadWithHeightCalculationWithNumbers() { $this->assertSame( - '
', + '
', LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080]) ); } @@ -38,4 +38,12 @@ public function testLazyLoadAttributesOnly() LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080, 'attributesOnly' => true]) ); } + + public function testLazyLoadReplacePlaceholderOptionOff() + { + $this->assertSame( + '
', + LazyLoad::widget(['src' => 'abc.jpg', 'extraClass' => 'add', 'height' => 1920, 'width' => 1080, 'replacePlaceholder' => false]) + ); + } } From e6f8168475fe4f5803baa2852b94b12eb047f65e Mon Sep 17 00:00:00 2001 From: Marc Stampfli Date: Tue, 28 Jul 2020 18:34:39 +0200 Subject: [PATCH 4/5] Updated changelog --- core/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 7d2295a52..f2383e446 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -6,6 +6,7 @@ In order to read more about upgrading and BC breaks have a look at the [UPGRADE ## 1.6.1 + [#2040](https://github.com/luyadev/luya/pull/2040) Fixed an issue where LazyLoad widget returns aspect ratio values with commas instead of dots, because of locale formatting. ++ [#2041](https://github.com/luyadev/luya/issues/2041) Fixed Polyfill (for older Browsers). Added new option `replacePlaceholder` (default `true`) to define if the image should replace the placeholder or fade in "above" it. ## 1.6.0 (20. July 2020) From 552ce8b9cf0dec24f9462e0b19e88e6dc348de5b Mon Sep 17 00:00:00 2001 From: Marc Stampfli Date: Tue, 28 Jul 2020 19:05:58 +0200 Subject: [PATCH 5/5] Updated Upgrade and Changelog files --- core/CHANGELOG.md | 1 + core/UPGRADE.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index f2383e446..8214193b7 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -7,6 +7,7 @@ In order to read more about upgrading and BC breaks have a look at the [UPGRADE + [#2040](https://github.com/luyadev/luya/pull/2040) Fixed an issue where LazyLoad widget returns aspect ratio values with commas instead of dots, because of locale formatting. + [#2041](https://github.com/luyadev/luya/issues/2041) Fixed Polyfill (for older Browsers). Added new option `replacePlaceholder` (default `true`) to define if the image should replace the placeholder or fade in "above" it. ++ Fixed LazyLoad BC break introduced in 1.6.0 ## 1.6.0 (20. July 2020) diff --git a/core/UPGRADE.md b/core/UPGRADE.md index b1570337a..7256a1a21 100644 --- a/core/UPGRADE.md +++ b/core/UPGRADE.md @@ -5,7 +5,7 @@ This document will help you upgrading from a LUYA Version into another. For more ## 1.6.0 -+ [#2037](https://github.com/luyadev/luya/issues/2037) Because of the recent changes and to make the transition between placeholder and image smoother, the image won't have "position: relative" anymore. The image now relys on the wrapper and the placeholder to set its size. The only issue with that is, that you can't apply a width directly to the `` tag anymore. Now it has to be applied to the parent div (`.lazyimage-wrapper`) or any element above. ++ [#2037](https://github.com/luyadev/luya/issues/2037) The LazyLoad widget now surrounds the image with a wrapper class (that will have the extraClass applied), keep that in mind - you might need to tweak your CSS a little bit. By default this wrapper will then be replaced by the actual image tag (Option: `replacePlaceholder`). ## 1.0.21