From 5116cec6881e720a24fc3f5d26fa230c7fc0ac60 Mon Sep 17 00:00:00 2001 From: techfg Date: Wed, 1 May 2024 01:48:14 -0700 Subject: [PATCH] fix: autoresize exception when map anscestor is not visible (Resolves #421) --- src/jqueryextensions.js | 116 ++++++++++++++++++++++++++++ src/scale.js | 14 +++- src/zepto.js | 43 ++++++++++- tests/imagemapster-test-runner.html | 22 ++++-- tests/imagemapster-test-runner.js | 31 ++++++-- tests/resize.tests.js | 93 +++++++++++++++++++++- 6 files changed, 300 insertions(+), 19 deletions(-) diff --git a/src/jqueryextensions.js b/src/jqueryextensions.js index 1166a84..78f532f 100644 --- a/src/jqueryextensions.js +++ b/src/jqueryextensions.js @@ -64,3 +64,119 @@ setupPassiveListeners(); } })(jQuery); + +/* + When autoresize is enabled, we obtain the width of the wrapper element and resize to that, however when we're hidden because of + one of our ancenstors, jQuery width function returns 0. Ideally, we could use ResizeObserver/MutationObserver to detect + when we hide/show and resize on that event instead of resizing while we are not visible but until official support of older + browsers is dropped, we need to go this route. The plugin below will provide the actual width even when we're not visible. + + Source: https://raw.githubusercontent.com/dreamerslab/jquery.actual/master/jquery.actual.js +*/ +/*! Copyright 2012, Ben Lin (http://dreamerslab.com/) + * Licensed under the MIT License (LICENSE.txt). + * + * Version: 1.0.19 + * + * Requires: jQuery >= 1.2.3 + */ +/* eslint-disable one-var */ +(function ($) { + 'use strict'; + + $.fn.addBack = $.fn.addBack || $.fn.andSelf; + + $.fn.extend({ + actual: function (method, options) { + // check if the jQuery method exist + if (!this[method]) { + throw ( + '$.actual => The jQuery method "' + + method + + '" you called does not exist' + ); + } + + var defaults = { + absolute: false, + clone: false, + includeMargin: false, + display: 'block' + }; + + var configs = $.extend(defaults, options); + + var $target = this.eq(0); + var fix, restore; + + if (configs.clone === true) { + fix = function () { + var style = 'position: absolute !important; top: -1000 !important; '; + + // this is useful with css3pie + $target = $target.clone().attr('style', style).appendTo('body'); + }; + + restore = function () { + // remove DOM element after getting the width + $target.remove(); + }; + } else { + var tmp = []; + var style = ''; + var $hidden; + + fix = function () { + // get all hidden parents + $hidden = $target.parents().addBack().filter(':hidden'); + style += + 'visibility: hidden !important; display: ' + + configs.display + + ' !important; '; + + if (configs.absolute === true) + style += 'position: absolute !important; '; + + // save the origin style props + // set the hidden el css to be got the actual value later + $hidden.each(function () { + // Save original style. If no style was set, attr() returns undefined + var $this = $(this); + var thisStyle = $this.attr('style'); + + tmp.push(thisStyle); + // Retain as much of the original style as possible, if there is one + $this.attr('style', thisStyle ? thisStyle + ';' + style : style); + }); + }; + + restore = function () { + // restore origin style values + $hidden.each(function (i) { + var $this = $(this); + var _tmp = tmp[i]; + + if (_tmp === undefined) { + $this.removeAttr('style'); + } else { + $this.attr('style', _tmp); + } + }); + }; + } + + fix(); + // get the actual value with user specific methed + // it can be 'width', 'height', 'outerWidth', 'innerWidth'... etc + // configs.includeMargin only works for 'outerWidth' and 'outerHeight' + var actual = /(outer)/.test(method) + ? $target[method](configs.includeMargin) + : $target[method](); + + restore(); + // IMPORTANT, this plugin only return the value of the first element + return actual; + } + }); +})(jQuery); +/* eslint-enable one-var */ diff --git a/src/scale.js b/src/scale.js index 022a509..c66bb43 100644 --- a/src/scale.js +++ b/src/scale.js @@ -191,7 +191,19 @@ m.MapData.prototype.autoResize = function (duration, callback) { var me = this; - me.resize($(me.wrapper).width(), null, duration, callback); + + /* + When autoresize is enabled, we obtain the width of the wrapper element and resize to that, however when we're hidden because of + one of our ancenstors, jQuery width function returns 0. Ideally, we could use ResizeObserver/MutationObserver to detect + when we hide/show and resize on that event instead of resizing while we are not visible but until official support of older + browsers is dropped, we need to go this route. + */ + me.resize( + $(me.wrapper).width() || $(me.wrapper).actual('width'), + null, + duration, + callback + ); }; m.MapData.prototype.configureAutoResize = function () { diff --git a/src/zepto.js b/src/zepto.js index 6cccc97..bf7de78 100644 --- a/src/zepto.js +++ b/src/zepto.js @@ -1,4 +1,4 @@ -/* +/* zepto.js Monkey patch for Zepto to add some methods ImageMapster needs */ @@ -22,4 +22,45 @@ }; } }); + + var origFnExtend = $.fn.extend; + if (!origFnExtend) { + $.fn.extend = function (obj) { + $.extend($.fn, obj); + }; + } + + var origAddSelf = $.fn.addSelf; + if (!origAddSelf) { + /* + Including Zepto Stack module manually since it is small and avoids updating docs, rebuilding zepto dist, etc. + This is needed to support autoresize functionality which needs to resize when the map and/or one (or more) of + its parents is not visible. Ideally, we could use ResizeObserver/MutationObserver to detect when we hide/show + and resize on that event instead of resizing while we are not visible but until official support of older browsers + is dropped, we need to go this route. + + Source: https://github.com/madrobby/zepto/blob/main/src/stack.js + */ + // Zepto.js + // (c) 2010-2016 Thomas Fuchs + // Zepto.js may be freely distributed under the MIT license. + $.fn.end = function () { + return this.prevObject || $(); + }; + + $.fn.andSelf = function () { + return this.add(this.prevObject || $()); + }; + + 'filter,add,not,eq,first,last,find,closest,parents,parent,children,siblings' + .split(',') + .forEach(function (property) { + var fn = $.fn[property]; + $.fn[property] = function () { + var ret = fn.apply(this, arguments); + ret.prevObject = this; + return ret; + }; + }); + } })(jQuery); diff --git a/tests/imagemapster-test-runner.html b/tests/imagemapster-test-runner.html index d8ab41a..ac9b846 100644 --- a/tests/imagemapster-test-runner.html +++ b/tests/imagemapster-test-runner.html @@ -116,12 +116,24 @@

ImageMapster Test Runner

show/hide test image diff --git a/tests/imagemapster-test-runner.js b/tests/imagemapster-test-runner.js index ab0a386..5d1d31a 100644 --- a/tests/imagemapster-test-runner.js +++ b/tests/imagemapster-test-runner.js @@ -114,17 +114,32 @@ function bindUIEvents() { document.location.search = search.toString(); }); - // zepto returns 0 for element width/height when element is not visible - // so if we are running with zepto, show the images and don't allow toggle + // Zepto returns zero (0) when width/height is called on a non-visible + // element and therefore problems may arise in test execution if the test + // images are not visible. As of 2024.05.01, all tests have been updated to + // work around this issue and all tests pass with Zepto when image is hidden. + // If any Zepto tests fail due to dimension assertions, make the image + // visible and if the tests pass, investigate and update with a solution. If + // for some reason a solution is not possible, comment out lines 137 - 142 below + // and uncomment lines 127 to 135 to force show the image AND update the code + // in imagemapster-test-runner.html per the similar comment to this one there. + /* + if ($.zepto) { + $('#testElements').show(); + $('#zeptoImageToggleInfo').show(); + $('#toggleTestImage').hide(); + } else { + $('#toggleTestImage').on('click', function () { + $('#testElements').toggle(); + }); + } + */ if ($.zepto) { - $('#testElements').show(); $('#zeptoImageToggleInfo').show(); - $('#toggleTestImage').hide(); - } else { - $('#toggleTestImage').on('click', function () { - $('#testElements').toggle(); - }); } + $('#toggleTestImage').on('click', function () { + $('#testElements').toggle(); + }); enableTestLink(); diff --git a/tests/resize.tests.js b/tests/resize.tests.js index 9064332..76e50a9 100644 --- a/tests/resize.tests.js +++ b/tests/resize.tests.js @@ -91,14 +91,57 @@ this.tests.push( this.tests.push( iqtest .create('autoresize', 'autoresize feature') - .add('wrapper does not have width/height', function (a) { + .add('Ensure expected behavior', function (a) { 'use strict'; - var img = $('img'); + var me = this, + $img = $('img'), + $testElements = $('#testElements'), + testElementsInfo = {}, + getPromise = function (name) { + return me.promises(name); + }, + setCallback = function (opt, cb) { + var obj = {}; + obj[opt] = function (e) { + // clear the callback + var objClear = {}; + objClear[opt] = null; + $img.mapster('set_options', objClear); + var resolveWith = ((e || {}).this_context = this); + cb(resolveWith); + }; + $img.mapster('set_options', obj); + }, + getElementWidth = function ($el) { + // zepto returns outerWidth (content + padding + border) using width function + // while jQuery returns content width. Use css('width') for consistency + // across both libraries + return parseInt($el.css('width').replace('px', ''), 10); + }; this.when(function (cb) { - img.mapster({ enableAutoResizeSupport: true, onConfigured: cb }); + testElementsInfo = { + orig: { + isVisible: $testElements.is(':visible'), + cssWidth: $testElements.css('width') + }, + underTest: { + initial: 100, + delta: 10 + } + }; + $testElements.css('width', testElementsInfo.underTest.initial + 'px'); + $img.mapster({ + mapKey: 'state', + enableAutoResizeSupport: true, + autoResize: true, + onConfigured: cb + }); }).then(function () { - var wrapper = img.closest('div'); + /* + Test: wrapper should not have explicit width/height + */ + var wrapper = $img.closest('div'); a.equals( wrapper.attr('id').substring(0, 12), 'mapster_wrap', @@ -110,6 +153,48 @@ this.tests.push( '', 'wrapper height is not specified' ); + + /* + Test: autoresize should complete successfully + see https://github.com/jamietre/ImageMapster/issues/421 + */ + var imageWidth = getElementWidth($img); + $testElements.css( + 'width', + testElementsInfo.underTest.initial + + testElementsInfo.underTest.delta + + 'px' + ); + // make sure parent element is hidden + $testElements.hide(); + a.equals( + $testElements.is(':hidden'), + true, + 'sanity check - ensure map container is hidden' + ); + a.equals( + $img.is(':hidden'), + true, + 'sanity check - ensure map is hidden' + ); + // make sure an area has 'state' + $img.mapster('set', true, 'KS'); + setCallback('onAutoResize', function () { + a.equals( + getElementWidth($img), + imageWidth + testElementsInfo.underTest.delta, + 'image width is correct after autoresize' + ); + // restore back to initial state + $testElements.css('width', testElementsInfo.orig.cssWidth); + if (testElementsInfo.orig.isVisible) { + $testElements.show(); + } + getPromise('finished').resolve(); + }); + $(window).trigger('resize'); + + a.resolves(getPromise('finished'), 'The last test was run'); }); }) );