Skip to content

Commit

Permalink
fix: autoresize exception when map anscestor is not visible (Resolves #…
Browse files Browse the repository at this point in the history
  • Loading branch information
techfg committed May 1, 2024
1 parent 865dbc2 commit 5116cec
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 19 deletions.
116 changes: 116 additions & 0 deletions src/jqueryextensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
14 changes: 13 additions & 1 deletion src/scale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
43 changes: 42 additions & 1 deletion src/zepto.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
zepto.js
Monkey patch for Zepto to add some methods ImageMapster needs
*/
Expand All @@ -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);
22 changes: 17 additions & 5 deletions tests/imagemapster-test-runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,24 @@ <h1>ImageMapster Test Runner</h1>
<div id="oldRuns"></div>

<p id="zeptoImageToggleInfo" style="display: none">
<strong>*** Zepto Build Detected ***</strong>
<strong>*** IMPORTANT: Zepto Build Detected ***</strong>
<br />
When using Zepto, the test images MUST be visible otherwise the resize
tests will fail because Zepto returns width/height of 0/0 when content
isn't visible. For this reason, the ability to toggle visibility of the
image is not supported.
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, replace this text with the
commented out text below and update imagemapster-test-runner.js to show
the image by default (there is code commented out in that file that can be
uncommented to accomplish).
<!--
When using Zepto, the test images MUST be visible otherwise the resize
tests will fail because Zepto returns width/height of 0/0 when content
isn't visible. For this reason, the ability to toggle visibility of the
image is not supported.
-->
</p>
<a href="#" id="toggleTestImage">show/hide test image</a>

Expand Down
31 changes: 23 additions & 8 deletions tests/imagemapster-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
93 changes: 89 additions & 4 deletions tests/resize.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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');
});
})
);

0 comments on commit 5116cec

Please sign in to comment.