From d941820ebf61746d7ba8d655ea478b15a0e66d48 Mon Sep 17 00:00:00 2001 From: techfg Date: Thu, 4 Feb 2021 22:24:22 -0800 Subject: [PATCH 1/5] Resolves #170 - Fix errors when shape attribute non-confirming/missing/empty --- examples/index.html | 1 + examples/shapes-spec.html | 171 ++++++++++++++++++++++++++++++++++++++ src/areacorners.js | 4 +- src/areadata.js | 2 +- src/core.js | 12 ++- src/graphics.js | 4 + 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 examples/shapes-spec.html diff --git a/examples/index.html b/examples/index.html index 2c41fed..034ee66 100644 --- a/examples/index.html +++ b/examples/index.html @@ -21,6 +21,7 @@

ImageMapster Examples

  • Navigate - Simple
  • Navigate - Full
  • Automatic Resize
  • +
  • Shape Attribute Values
  • diff --git a/examples/shapes-spec.html b/examples/shapes-spec.html new file mode 100644 index 0000000..e58ea64 --- /dev/null +++ b/examples/shapes-spec.html @@ -0,0 +1,171 @@ + + + + + Shapes HTML Spec Test + + + + + + + + + + + + + + + + +

    Shapes HTML Spec Test

    +

    + Test 'shape' attributes using various combinations of values. If working + as expected, behavior of all 4 imagemaps should function identically. This + should be converted to a unit test once the testing framework is updated. +

    +

    The behavior of each shape should be:

    + +

    All 'shape' attributes use conforming value (rect/poly/circle)

    + + + + + + + + + +

    + All 'shape' attributes use non-conforming value (rectangle/polygon/circ) +

    + + + + + + + + +

    + All 'shape' attributes use conforming value (poly/circle) with rectangle + shape attribute missing which should default to rect per HTML spec +

    + + + rectangle + + + + + +

    + All 'shape' attributes use non-conforming value (polygon/circ) with + rectangle shape attribute empty which should default to rect per HTML spec +

    + + + rectangle + + + + + diff --git a/src/areacorners.js b/src/areacorners.js index 49176e6..ab77516 100644 --- a/src/areacorners.js +++ b/src/areacorners.js @@ -72,8 +72,9 @@ if (el.nodeName === 'AREA') { iCoords = u.split(el.coords, parseInt); - switch (el.shape) { + switch (u.getShape(el)) { case 'circle': + case 'circ': curX = iCoords[0]; curY = iCoords[1]; radius = iCoords[2]; @@ -86,6 +87,7 @@ ); } break; + case 'rectangle': case 'rect': coords.push( iCoords[0], diff --git a/src/areadata.js b/src/areadata.js index 5a9ccb6..a7e76f0 100644 --- a/src/areadata.js +++ b/src/areadata.js @@ -278,7 +278,7 @@ me.originalCoords.push(parseFloat(el)); }); me.length = me.originalCoords.length; - me.shape = areaEl.shape.toLowerCase(); + me.shape = u.getShape(areaEl); me.nohref = areaEl.nohref || !areaEl.href; me.configure(keys); }; diff --git a/src/core.js b/src/core.js index 16b9ad7..39dc9f9 100644 --- a/src/core.js +++ b/src/core.js @@ -342,7 +342,17 @@ } }; return fade_func; - })() + })(), + getShape: function(areaEl) { + // per HTML spec, invalid value and missing value default is 'rect' + // Handling as follows: + // - Missing/Empty value will be treated as 'rect' per spec + // - Avoid handling invalid values do to perf impact + // Note - IM currently does not support shape of 'default' so while its technically + // a valid attribute value it should not be used. + // https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element + return (areaEl.shape || 'rect').toLowerCase(); + } }, getBoundList: function (opts, key_list) { if (!opts.boundList) { diff --git a/src/graphics.js b/src/graphics.js index dd56465..8cb690e 100644 --- a/src/graphics.js +++ b/src/graphics.js @@ -200,9 +200,11 @@ switch (mapArea.shape) { case 'rect': + case 'rectangle': context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]); break; case 'poly': + case 'polygon': context.moveTo(c[0], c[1]); for (i = 2; i < mapArea.length; i += 2) { @@ -396,6 +398,7 @@ switch (mapArea.shape) { case 'rect': + case 'rectangle': template = ''; break; case 'poly': + case 'polygon': template = ' Date: Thu, 4 Feb 2021 23:32:51 -0800 Subject: [PATCH 2/5] Resolves #364 - unhandled promise rejection --- src/mapdata.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mapdata.js b/src/mapdata.js index f5d9bf0..d3bfc19 100644 --- a/src/mapdata.js +++ b/src/mapdata.js @@ -98,7 +98,7 @@ function cbFinal(areaId) { if (me.currentAreaId !== areaId && me.highlightId >= 0) { - deferred.resolve(); + deferred.resolve({ completeAction: true }); } } if (me.activeAreaEvent) { @@ -106,7 +106,7 @@ me.activeAreaEvent = 0; } if (delay < 0) { - deferred.reject(); + deferred.resolve({ completeAction: false }); } else { if (area.owner.currentAction || delay) { me.activeAreaEvent = window.setTimeout( @@ -212,7 +212,12 @@ me.currentAreaId = -1; ar.area = null; - queueMouseEvent(me, opts.mouseoutDelay, ar).then(me.clearEffects); + queueMouseEvent(me, opts.mouseoutDelay, ar).then(function (result) { + if (!result.completeAction) { + return; + } + me.clearEffects(); + }); if (u.isFunction(opts.onMouseout)) { opts.onMouseout.call(this, { From b4e883cfba86d414074d4b47460320f1cf7d9192 Mon Sep 17 00:00:00 2001 From: techfg Date: Sat, 6 Feb 2021 21:03:29 -0800 Subject: [PATCH 3/5] Resolves #137 - fix href attribute missing/empty issues --- examples/index.html | 1 + examples/nohref.html | 297 +++++++++++++++++++++++++++++++++++++++++++ src/areadata.js | 15 +-- src/core.js | 9 +- src/mapdata.js | 12 +- 5 files changed, 320 insertions(+), 14 deletions(-) create mode 100644 examples/nohref.html diff --git a/examples/index.html b/examples/index.html index 034ee66..4e01f91 100644 --- a/examples/index.html +++ b/examples/index.html @@ -22,6 +22,7 @@

    ImageMapster Examples

  • Navigate - Full
  • Automatic Resize
  • Shape Attribute Values
  • +
  • Nohref & Href Attribute Values
  • diff --git a/examples/nohref.html b/examples/nohref.html new file mode 100644 index 0000000..735005a --- /dev/null +++ b/examples/nohref.html @@ -0,0 +1,297 @@ + + + + + Nohref & Href Attribute Values Test + + + + + + + + + + + + + + + + +

    Nohref & Href Attribute Values Test

    +

    + Test 'nohref/href' attributes using various combinations of values. This + should be converted to a unit test once the testing framework is updated. +

    +

    The behavior of each area should be:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Vegetablenohrefhrefhighlightselectnavigationtooltipdescription
    Red Pepperyesnonononononohref disables IM completely
    Celerynonononononomissing href treated as nohref per HTML spec
    Carrotsnoyes - emptyyesyesnoyesnavigation disabled due to empty/#
    Asparagusnoyes - #yesyesnoyesnavigation disabled due to empty/#
    Squashnoyes - https://www.google.comyesyesyesyesnavigates due to href != empty/#
    Yellow Peppernoyes - emptyyesyesnoyesnavigation disabled due to empty/#
    Broccolinoyes - emptyyesyesnoyesnavigation disabled due to empty/#
    Broccoli #2noyes - emptyyesyesnoyesnavigation disabled due to empty/#
    Dipnoyes - emptyyesyesnoyesnavigation disabled due to empty/#
    +

    + You can toggle the values of navigateMode and clickNavigate. The behavior + should be identical regardless of their values. +

    +
    + NavigateMode: TBD +
    +
    + ClickNavigate: TBD +
    +
    + + + + + + + + + + + + + + + + + diff --git a/src/areadata.js b/src/areadata.js index a7e76f0..8cc95a1 100644 --- a/src/areadata.js +++ b/src/areadata.js @@ -86,6 +86,11 @@ return me.isSelected(); } + function isNoHref(areaEl) { + var $area = $(areaEl); + return u.hasAttribute($area, 'nohref') || !u.hasAttribute($area, 'href'); + } + /** * An AreaData object; represents a conceptual area that can be composed of * one or more MapArea objects @@ -177,14 +182,8 @@ : u.boolOrDefault(this.effectiveOptions().isDeselectable, true); }, isNotRendered: function () { - var area = $(this.area); - return ( - area.attr('nohref') || - !area.attr('href') || - this.effectiveOptions().isMask - ); + return isNoHref(this.area) || this.effectiveOptions().isMask; }, - /** * Return the overall options effective for this area. * This should get the default options, and merge in area-specific options, finally @@ -279,7 +278,7 @@ }); me.length = me.originalCoords.length; me.shape = u.getShape(areaEl); - me.nohref = areaEl.nohref || !areaEl.href; + me.nohref = isNoHref(areaEl); me.configure(keys); }; m.MapArea.prototype = { diff --git a/src/core.js b/src/core.js index 39dc9f9..48af47d 100644 --- a/src/core.js +++ b/src/core.js @@ -343,7 +343,7 @@ }; return fade_func; })(), - getShape: function(areaEl) { + getShape: function (areaEl) { // per HTML spec, invalid value and missing value default is 'rect' // Handling as follows: // - Missing/Empty value will be treated as 'rect' per spec @@ -352,7 +352,12 @@ // a valid attribute value it should not be used. // https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element return (areaEl.shape || 'rect').toLowerCase(); - } + }, + hasAttribute: function (el, attrName) { + var attr = $(el).attr(attrName); + // For some browsers, `attr` is undefined; for others, `attr` is false. + return typeof attr !== 'undefined' && attr !== false; + } }, getBoundList: function (opts, key_list) { if (!opts.boundList) { diff --git a/src/mapdata.js b/src/mapdata.js index d3bfc19..c4cde57 100644 --- a/src/mapdata.js +++ b/src/mapdata.js @@ -124,6 +124,10 @@ return deferred; } + function shouldNavigateTo(href) { + return !!href && href !== '#'; + } + /** * Mousedown event. This is captured only to prevent browser from drawing an outline around an * area when it's clicked. @@ -288,7 +292,7 @@ function getNavDetails(ar, mode, defaultHref) { if (mode === 'open') { var elHref = $(ar.area).attr('href'), - useEl = elHref && elHref !== '#'; + useEl = shouldNavigateTo(elHref); return { href: useEl ? elHref : ar.href, @@ -331,7 +335,7 @@ opts.navigateMode, $(ar.area).attr('href') ); - if (target.href !== '#') { + if (shouldNavigateTo(target.href)) { navigateTo(opts.navigateMode, target.href, target.target); return false; } @@ -361,7 +365,7 @@ mousedown.call(this, e); navDetails = getNavDetails(ar, opts.navigateMode, ar.href); - if (opts.clickNavigate && navDetails.href) { + if (opts.clickNavigate && shouldNavigateTo(navDetails.href)) { navigateTo(opts.navigateMode, navDetails.href, navDetails.target); return; } @@ -935,7 +939,7 @@ } href = $area.attr('href'); - if (href && href !== '#' && !dataItem.href) { + if (shouldNavigateTo(href) && !dataItem.href) { dataItem.href = href; dataItem.hrefTarget = $area.attr('target'); } From de59cc8684e39c0ffb46d424f48def643a6ee379 Mon Sep 17 00:00:00 2001 From: techfg Date: Sat, 13 Feb 2021 18:13:18 -0800 Subject: [PATCH 4/5] Resolves #365 & #366 - fix staticState `selected` state --- examples/index.html | 1 + examples/staticstate.html | 363 ++++++++++++++++++++++++++++++++++++++ src/areadata.js | 12 +- 3 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 examples/staticstate.html diff --git a/examples/index.html b/examples/index.html index 4e01f91..43a29af 100644 --- a/examples/index.html +++ b/examples/index.html @@ -23,6 +23,7 @@

    ImageMapster Examples

  • Automatic Resize
  • Shape Attribute Values
  • Nohref & Href Attribute Values
  • +
  • StaticState Values
  • diff --git a/examples/staticstate.html b/examples/staticstate.html new file mode 100644 index 0000000..e8abfd7 --- /dev/null +++ b/examples/staticstate.html @@ -0,0 +1,363 @@ + + + + + StaticState Test + + + + + + + + + + + + + + + + + +

    StaticState Test

    +

    + Test 'staticState' option using various combinations of values. This + should be converted to a unit test once the testing framework is updated. +

    +

    The behavior of each area should be:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    VegetablestaticStatehrefhighlightselect via clickselect via apinavigationtooltipdescription
    Red Peppertrueyes - #yesnoyesnoyesstaticState true disables changing selected via click
    Celeryfalseyes - #yesnoyesnoyesstaticState false disables changing selected via click
    Carrotsnot specifiedyes - #yesyesyesnoyes
    Asparagusnot specifiedyes - #yesyesyesnoyes
    Squashnot specifiedyes - #yesyesyesnoyes
    Yellow Peppernot specifiedyes - #yesyesyesnoyes
    Broccolinot specifiedyes - #yesyesyesnoyes
    Broccoli #2not specifiedyes - #yesyesyesnoyes
    Dipnot specifiedyes - #yesyesyesnoyes
    +

    + You can toggle the values of navigateMode and clickNavigate. The behavior + should be identical regardless of their values. +

    +
    + NavigateMode: TBD +
    +
    + ClickNavigate: TBD +
    +
    +
    Selected Keys:
    + + + + + + + + + + + + + + + + diff --git a/src/areadata.js b/src/areadata.js index 8cc95a1..a8ad540 100644 --- a/src/areadata.js +++ b/src/areadata.js @@ -38,6 +38,9 @@ me.drawSelection(); me.selected = true; + me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) + ? true + : false; me.changeState('select', true); } @@ -56,6 +59,9 @@ function deselect(partial) { var me = this; me.selected = false; + me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) + ? true + : false; me.changeState('select', false); // release information about last area options when deselecting. @@ -113,6 +119,8 @@ options: {}, // "null" means unchanged. Use "isSelected" method to just test true/false selected: null, + // "true" means selected has been set via API AND staticState is true/false + staticStateOverridden: false, // xref to MapArea objects areasXref: [], // (temporary storage) - the actual area moused over @@ -158,7 +166,9 @@ // Return the effective selected state of an area, incorporating staticState isSelectedOrStatic: function () { var o = this.effectiveOptions(); - return u.isBool(o.staticState) ? o.staticState : this.isSelected(); + return !u.isBool(o.staticState) || this.staticStateOverridden + ? this.isSelected() + : o.staticState; }, isSelected: function () { return u.isBool(this.selected) From 57d908628cbbf36e67722e376b1d6fc36504b1c7 Mon Sep 17 00:00:00 2001 From: techfg Date: Sat, 13 Feb 2021 23:53:24 -0800 Subject: [PATCH 5/5] Resolves #367 - fix applying options on set/select --- examples/staticstate.html | 226 +++++++++++++++++++++++++++----------- src/areadata.js | 114 ++++++++++++++----- 2 files changed, 249 insertions(+), 91 deletions(-) diff --git a/examples/staticstate.html b/examples/staticstate.html index e8abfd7..1566045 100644 --- a/examples/staticstate.html +++ b/examples/staticstate.html @@ -54,12 +54,57 @@ $(document).ready(function () { 'use strict'; - var $image = $('#veg_image'); + var $image = $('#veg_image'), + imageOptions = { + mapKey: 'name', + fillOpacity: 0.4, + fillColor: 'd42e16', + strokeColor: '3320FF', + strokeOpacity: 0.8, + strokeWidth: 5, + render_select: { + strokeWidth: 10 + }, + render_highlight: { + strokeWidth: 5 + }, + stroke: true, + clickNavigate: false, + onConfigured: function () { + updateOptions(); + updateKeys(); + }, + onStateChange: function () { + updateKeys(); + }, + onClick: function () { + return true; + }, + showToolTip: true, + toolTip: function (data) { + return $(data.target).data('tooltip'); + }, + areas: [ + { + key: 'redpepper', + staticState: true + }, + { + key: 'celery', + staticState: false + } + ] + }; + + $('#resetMap').on('click', function () { + $image.mapster('unbind').mapster(imageOptions); + }); $('.toggleSelected').on('click', function () { var key = $(this).data('name'), - isSelected = $image.mapster('get', key); - $image.mapster('set', !isSelected, key); + isSelected = $(this).data('selected'), + options = $(this).data('options') ? { strokeWidth: 3 } : null; + $image.mapster('set', isSelected, key, options); }); function updateKeys() { @@ -89,46 +134,7 @@ updateOptions(); }); - $image.mapster({ - mapKey: 'name', - fillOpacity: 0.4, - fillColor: 'd42e16', - strokeColor: '3320FF', - strokeOpacity: 0.8, - strokeWidth: 5, - render_select: { - strokeWidth: 10 - }, - render_highlight: { - strokeWidth: 5 - }, - stroke: true, - clickNavigate: false, - onConfigured: function () { - updateOptions(); - updateKeys(); - }, - onStateChange: function () { - updateKeys(); - }, - onClick: function () { - return true; - }, - showToolTip: true, - toolTip: function (data) { - return $(data.target).data('tooltip'); - }, - areas: [ - { - key: 'redpepper', - staticState: true - }, - { - key: 'celery', - staticState: false - } - ] - }); + $image.mapster(imageOptions); }); @@ -272,26 +278,120 @@

    StaticState Test


    Selected Keys:
    - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    areaSelect No OptionsSelect With OptionsDeselect
    Red Pepper + + + + + +
    Celery + + + + + +
    Carrots + + + + + +
    +
    + +
    diff --git a/src/areadata.js b/src/areadata.js index a8ad540..8ccc804 100644 --- a/src/areadata.js +++ b/src/areadata.js @@ -8,6 +8,33 @@ var m = $.mapster, u = m.utils; + function optsAreEqual(opts1, opts2) { + // deep compare is not trivial and current testing framework + // doesn't provide a way to detect this accurately so only + // implementing basic compare at this time. + // TODO: Implement deep obj compare or for perf reasons shallow + // with a short-circuit if deep is required for full compare + // since config options should only require shallow + return opts1 === opts2; + } + + /** + * Update selected state of this area + * + * @param {boolean} selected Determines whether areas are selected or deselected + */ + function updateSelected(selected) { + var me = this, + prevSelected = me.selected; + + me.selected = selected; + me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) + ? true + : false; + + return prevSelected !== selected; + } + /** * Select this area * @@ -15,37 +42,62 @@ * @param {object} options Options for rendering the selection */ function select(options) { - // need to add the new one first so that the double-opacity effect leaves the current one highlighted for singleSelect + function buildOptions() { + // map the altImageId if an altimage was passed + return $.extend(me.effectiveRenderOptions('select'), options, { + altImageId: o.images.add(options.altImage) + }); + } var me = this, - o = me.owner; + o = me.owner, + hasOptions = !$.isEmptyObject(options), + newOptsCache = hasOptions ? buildOptions() : null, + // Per docs, options changed via set_options for an area that is + // already selected will not be reflected until the next time + // the area becomes selected. + changeOptions = hasOptions + ? !optsAreEqual(me.optsCache, newOptsCache) + : false, + selectedHasChanged = false, + isDrawn = me.isSelectedOrStatic(); + + // This won't clear staticState === true areas that have not been overridden via API set/select/deselect. + // This could be optimized to only clear if we are the only one selected. However, there are scenarios + // that do not respect singleSelect (e.g. initialization) so we force clear if there should only be one. + // TODO: Only clear if we aren't the only one selected (depends on #370) if (o.options.singleSelect) { o.clearSelections(); + // we may (staticState === true) or may not still be visible + isDrawn = me.isSelectedOrStatic(); } - // because areas can overlap - we can't depend on the selection state to tell us anything about the inner areas. - // don't check if it's already selected - if (!me.isSelected()) { - if (options) { - // cache the current options, and map the altImageId if an altimage - // was passed + if (changeOptions) { + me.optsCache = newOptsCache; + } - me.optsCache = $.extend(me.effectiveRenderOptions('select'), options, { - altImageId: o.images.add(options.altImage) - }); - } + // Update before we start drawing for methods + // that rely on internal selected value. + // Force update because area can be selected + // at multiple levels (selected / area_options.selected / staticState / etc.) + // and could have been cleared. + selectedHasChanged = me.updateSelected(true); - me.drawSelection(); + if (isDrawn && changeOptions) { + // no way to remove just this area from canvas so must refresh everything - me.selected = true; - me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) - ? true - : false; - me.changeState('select', true); + // explicitly remove vml element since it uses removeSelections instead of refreshSelections + // TODO: Not sure why removeSelections isn't incorporated in to refreshSelections + // need to investigate and possibly consolidate + o.graphics.removeSelections(me.areaId); + o.graphics.refreshSelections(); + } else if (!isDrawn) { + me.drawSelection(); } - if (o.options.singleSelect) { - o.graphics.refreshSelections(); + // don't fire until everything is done + if (selectedHasChanged) { + me.changeState('select', true); } } @@ -57,24 +109,29 @@ */ function deselect(partial) { - var me = this; - me.selected = false; - me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) - ? true - : false; - me.changeState('select', false); + var me = this, + selectedHasChanged = false; - // release information about last area options when deselecting. + // update before we start drawing for methods + // that rely on internal selected value + // force update because area can be selected + // at multiple levels (selected / area_options.selected / staticState / etc.) + selectedHasChanged = me.updateSelected(false); + // release information about last area options when deselecting. me.optsCache = null; me.owner.graphics.removeSelections(me.areaId); // Complete selection removal process. This is separated because it's very inefficient to perform the whole // process for multiple removals, as the canvas must be totally redrawn at the end of the process.ar.remove - if (!partial) { me.owner.removeSelectionFinish(); } + + // don't fire until everything is done + if (selectedHasChanged) { + me.changeState('select', false); + } } /** @@ -140,6 +197,7 @@ select: select, deselect: deselect, toggle: toggle, + updateSelected: updateSelected, areas: function () { var i, result = [];