Search code examples
javascripthtmlcanvasfabricjstippyjs

How to trigger click on canvas object and DOM element that is placed on top of object at the same time?


I have some circles that can be added to a fabricjs canvas. Each circle is an object, while outside my javascript code I have a DOM element, that looks like this:

<span id="cirkel1" class="carttip inlineflexmenu" style="border-radius:100%;width: 25px;height:25px;z-index:9999999;position:absolute;cursor:pointer;">
  <div class="tooltipcontent darktext tooltippadding" style="position:relative;">
    Testtest
  </div>
</span>

This element triggers a tooltip with Tippjs (a js tooltip package), that has the following code (don't mind the each loop, I should also mention below code is outside the canvas function):

$( "#cirkel1" ).each(function( i ) {
  tippy(this, {
    theme: 'blue',
    trigger: 'click',
    allowHTML: true,
    placement: 'right',
    animation: 'scale-subtle',
    interactive: true,
    content: function (reference) {
      return reference.querySelector('.tooltipcontent');
    }
  });
});

Inside my function where I declare everything for the canvas, I have the following code to place the DOM element on top of the canvas object:

fabric.Canvas.prototype.getAbsoluteCoords = function(object) {
  return {
    left: object.left + this._offset.left,
    top: object.top + this._offset.top
  };
}

var cirkel1tooltip = document.getElementById('cirkel1'),
btnWidth = 40,
btnHeight = 40;

function positionBtn(obj) {
  var absCoords = canvas.getAbsoluteCoords(obj);
  cirkel1tooltip.style.left = (absCoords.left - btnWidth / 10) + 'px';
  cirkel1tooltip.style.top = (absCoords.top - btnHeight / 10) + 'px';
}

This works, and the tooltip shows when clicked, but in my canvas function I also have a click function which toggles an image for a specific circle when clicked. I need both to trigger at the same time when the circle is clicked, now when I click a circle, the image appears, but only after I click a second time, the tooltip appears too, not at the same first click.

Removing the image by clicking a second time also doesn't work untill I click on another circle and then click back on the previously clicked circle.

The strange thing is, when I remove one of the two functions (tooltip click, or image toggle click) it works instant, but together only the image toggle works right away but the tooltip only after a second click. Why is that?

The entire code can be seen here (click the small circles to test): https://codepen.io/twan2020/pen/jOVaWMm

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <base href="//printzelf.nl/new/">
    <title>Image test</title>
    <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/scale-subtle.css"/>
    <link rel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
    <script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
  </head>
  <body>
    <style media="screen">
    .tippy-box {
      width: 100%!important;
      text-align: center;
      background-color: #fff!important;
      color: #fff!important;
      box-shadow: 3px 2px 15px 6px rgb(0 0 0 / 10%);
    }
    .darktext {
      color: #383838;
      font-family: Panton;
      font-size: 15px;
    }
    .tooltippadding {
      padding: 15px;
    }
    body .tippy-arrow {
      color: #fff!important;
    }
    </style>
    <img id="background" src="https://static01.nyt.com/images/2019/05/29/realestate/00skyline-south4/88ce0191bfc249b6aae1b472158cccc4-superJumbo.jpg" alt="" style="display:none;">
    <div class="canvas-container" style="width: 600px; height: 500px; position: relative; user-select: none;">
      <canvas id="c" width="600" height="500" class="lower-canvas" style="border:1px solid red;position: absolute; width: 600px; height: 500px; left: 0px; top: 0px; touch-action: none; user-select: none;"></canvas>
    </div>
  </body>
  <span id="cirkel1" class="carttip inlineflexmenu" style="border-radius:100%;width: 25px;height:25px;z-index:9999999;position:absolute;cursor:pointer;">
    <div class="tooltipcontent darktext tooltippadding" style="position:relative;">
      Testtest
    </div>
  </span>
  <!-- Popper JS -->
  <script src="assets/js/popper.min.js"></script>
  <script src="https://unpkg.com/tippy.js@6"></script>
  <script type="text/javascript" src="assets/js/fabric.js"></script>
  <script type="text/javascript">
  (function() {
    var myImg = document.querySelector("#background");
     var realWidth = myImg.naturalWidth;
     var realHeight = myImg.naturalHeight;
     var source = document.getElementById('background').src;
     var canvas = new fabric.Canvas('c');
     canvas.hoverCursor = 'pointer';
     canvas.selection = false;
     canvas.setDimensions({ width: realWidth, height: realHeight });
     var img = new Image();
     // use a load callback to add image to canvas.
     img.src = 'https://static01.nyt.com/images/2019/05/29/realestate/00skyline-south4/88ce0191bfc249b6aae1b472158cccc4-superJumbo.jpg';
     canvas.setBackgroundImage(source, canvas.renderAll.bind(canvas), {
         backgroundImageOpacity: 0.5,
         backgroundImageStretch: false
     });


     const hotspots = [
      {
        top: 140,
        left: 230,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel1',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 300,
        imgheight: 200,
        imgwidth: 200,
        tooltipid: 'cirkel1',
        imgUrl: 'https://printzelf.nl/new/assets/images/logo_gewoon.png'
      },
      {
        top: 240,
        left: 530,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel2',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 700,
        imgheight: 200,
        imgwidth: 200,
        imgUrl: 'https://i1.wp.com/nypost.com/wp-content/uploads/sites/2/2020/04/pugs-coronavirus.jpg'
      },
      {
        top: 240,
        left: 730,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel2',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 800,
        imgheight: 200,
        imgwidth: 200,
        imgUrl: 'https://i.guim.co.uk/img/media/fe1e34da640c5c56ed16f76ce6f994fa9343d09d/0_174_3408_2046/master/3408.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=0d3f33fb6aa6e0154b7713a00454c83d'
      }
    ];

    const loadedImages = [];

    for (let [idx, props] of hotspots.entries()) {
      let c = new fabric.Circle(props);
      c.class = 'hotspot';
      c.name = 'hotspot-' + idx;
      canvas.add(c);
    }

    fabric.Canvas.prototype.getAbsoluteCoords = function(object) {
      return {
        left: object.left + this._offset.left,
        top: object.top + this._offset.top
      };
    }

    var cirkel1tooltip = document.getElementById('cirkel1'),
    btnWidth = 40,
    btnHeight = 40;

    function positionBtn(obj) {
      var absCoords = canvas.getAbsoluteCoords(obj);
      cirkel1tooltip.style.left = (absCoords.left - btnWidth / 10) + 'px';
      cirkel1tooltip.style.top = (absCoords.top - btnHeight / 10) + 'px';
    }

    for (const ho of canvas.getObjects()) {
      // check for 'hotspot' class
      if (ho.class && ho.class === 'hotspot') {
        ho.on('mousedown', () => {
          // check if image was previously loaded
          if (loadedImages.indexOf(ho.name) < 0) {
            // image is not in the array
            // so it needs to be loaded
            // prepare the image properties
            let imgProps = {
              width: ho.imgwidth,
              height: ho.imgheight,
              left: ho.imgleft,
              top: ho.imgtop,
              scaleX: .25,
              scaleY: .25,
              selectable: false,
              id: 'img-' + ho.name,
              hoverCursor: "default"
            };
            var printzelfImg = new Image();
            printzelfImg.onload = function (img) {
              var printzelf = new fabric.Image(printzelfImg, imgProps);
              canvas.add(printzelf);
            };
            printzelfImg.src = ho.imgUrl;
            // update the `loadedImages` array
            loadedImages.push(ho.name);
          } else {
            // image was previously loaded
            for (const o of canvas.getObjects()) {
              // find the correct image on the canvas
              if (o.id && o.id === 'img-' + ho.name) {
                // toggle the visible property
                o.visible = !o.visible;
                break;
              }
            }
          }
          positionBtn(ho);
        });
      }
    }
  })();
  $( "#cirkel1" ).each(function( i ) {
    tippy(this, {
      theme: 'blue',
      trigger: 'click',
      allowHTML: true,
      placement: 'right',
      animation: 'scale-subtle',
      interactive: true,
      content: function (reference) {
        return reference.querySelector('.tooltipcontent');
      }
    });
  });
  </script>
</html>

Also, is it possible to attach different tooltips to each dot/circle?


Solution

  • The reason why it didn't work is that you have just 1 DIV hotspot and you move this hotspot on mousedown and expect it to trigger the onclick event afterward which doesn't work. The reason why it works on second click is that the hotspot it now there.

    The solution is to have the same amount of DIV as you have hotspot. This allows you to have unique popup message. Currently it displays the same message for each hotspot.

    There is an onShow(instance), and onHide(instance) for the tippy property which allows you to carryout extra functionality when these hotspot are clicked on. In your case you want to load images related to the selected hotspot. This eliminate having two events setup.

    There was also a problem toggling images. I fixed this but I am not 100% certain it is working how you would like this to work.

    Also you had HTML tags outside the <body> tag and HTML content aren't supposed to exist outside body.

    I kept most of your original code as much as I can.

    <html lang="en" dir="ltr">
      <head>
    <meta charset="utf-8">
    <base href="//printzelf.nl/new/">
    <title>Image test</title>
    <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/scale-subtle.css"/>
    <link rel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
    <script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
        <style media="screen">
    .tippy-box {
      width: 100%!important;
      text-align: center;
      background-color: #fff!important;
      color: #fff!important;
      box-shadow: 3px 2px 15px 6px rgb(0 0 0 / 10%);
    }
    .darktext {
      color: #383838;
      font-family: Panton;
      font-size: 15px;
    }
    .tooltippadding {
      padding: 15px;
    }
    body .tippy-arrow {
      color: #fff!important;
    }
    </style>
      </head>
      <body>
    
    <img id="background" src="https://static01.nyt.com/images/2019/05/29/realestate/00skyline-south4/88ce0191bfc249b6aae1b472158cccc4-superJumbo.jpg" alt="" style="display:none;">
    <div class="canvas-container" style="width: 600px; height: 500px; position: relative; user-select: none;">
      <canvas id="c" width="600" height="500" class="lower-canvas" style="border:1px solid red;position: absolute; width: 600px; height: 500px; left: 0px; top: 0px; touch-action: none; user-select: none;"></canvas>
    </div>
    
      <span id="cirkel1" class="carttip inlineflexmenu" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
    <div class="tooltipcontent1 tooltipcontent darktext tooltippadding" style="position:relative;">
      Message 1
    </div>
      </span>
      
      <span id="cirkel2" class="carttip inlineflexmenu" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
    <div class="tooltipcontent2 tooltipcontent darktext tooltippadding" style="position:relative;">
      Message 2
    </div>
      </span>
      
      <span id="cirkel3" class="carttip inlineflexmenu" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
    <div class="tooltipcontent3 tooltipcontent darktext tooltippadding" style="position:relative;">
      Message 3
    </div>
      </span>
      
      
      <!-- Popper JS -->
      <script src="assets/js/popper.min.js"></script>
      <script src="https://unpkg.com/tippy.js@6"></script>
      <script type="text/javascript" src="assets/js/fabric.js"></script>
      <script type="text/javascript">
      (function() {
    var myImg = document.querySelector("#background");
     var realWidth = myImg.naturalWidth;
     var realHeight = myImg.naturalHeight;
     var source = document.getElementById('background').src;
     var canvas = new fabric.Canvas('c');
     canvas.hoverCursor = 'pointer';
     canvas.selection = false;
     canvas.setDimensions({ width: realWidth, height: realHeight });
     var img = new Image();
     // use a load callback to add image to canvas.
     img.src = 'https://static01.nyt.com/images/2019/05/29/realestate/00skyline-south4/88ce0191bfc249b6aae1b472158cccc4-superJumbo.jpg';
     canvas.setBackgroundImage(source, canvas.renderAll.bind(canvas), {
         backgroundImageOpacity: 0.5,
         backgroundImageStretch: false
     });
    
    
     const hotspots = [
      {
        top: 140,
        left: 230,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel1',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 300,
        imgheight: 200,
        imgwidth: 200,
        tooltipid: 'cirkel1',
        imgUrl: 'https://printzelf.nl/new/assets/images/logo_gewoon.png'
      },
      {
        top: 240,
        left: 530,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel2',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 700,
        imgheight: 200,
        imgwidth: 200,
        imgUrl: 'https://i1.wp.com/nypost.com/wp-content/uploads/sites/2/2020/04/pugs-coronavirus.jpg'
      },
      {
        top: 240,
        left: 730,
        radius: 10,
        fill: '#009fe3',
        id: 'cirkel2',
        hoverCursor: 'pointer',
        selectable: false,
        imgtop: 200,
        imgleft: 800,
        imgheight: 200,
        imgwidth: 200,
        imgUrl: 'https://i.guim.co.uk/img/media/fe1e34da640c5c56ed16f76ce6f994fa9343d09d/0_174_3408_2046/master/3408.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=0d3f33fb6aa6e0154b7713a00454c83d'
      }
    ];
    
    const loadedImages = [];
    
    for (let [idx, props] of hotspots.entries()) {
      let c = new fabric.Circle(props);
      c.class = 'hotspot';
      c.name = 'hotspot-' + idx;
      canvas.add(c);
    }
    
    fabric.Canvas.prototype.getAbsoluteCoords = function(object) {
      return {
        left: object.left + this._offset.left,
        top: object.top + this._offset.top
      };
    }
    
    var cirkel1tooltip = document.getElementById('cirkel1'),
    btnWidth = 40,
    btnHeight = 40;
    
    function positionBtn(obj, index) {
      var absCoords = canvas.getAbsoluteCoords(obj);
      var element = document.getElementById('cirkel'+index);
      element.style.left = (absCoords.left - btnWidth / 10) + 'px';
      element.style.top = (absCoords.top - btnHeight / 10) + 'px';
    }
    
    canvas.getObjects().forEach(function(ho, index) {
      positionBtn(ho, index + 1); 
    });
      
    
      
      $( ".carttip" ).each(function( i ) {
    tippy(this, {
      theme: 'blue',
      trigger: 'click',
      allowHTML: true,
      placement: 'right',
      animation: 'scale-subtle',
      interactive: true,
      onShow(instance) {
         canvas.getObjects().forEach(function(ho, index) {
            if (ho.class && ho.class === 'hotspot') {
               if (instance.id == index + 1) {
               // check if image was previously loaded
                  if (loadedImages.indexOf(ho.name) < 0) {
                     // image is not in the array
                     // so it needs to be loaded
                     // prepare the image properties
                     let imgProps = {
                        width: ho.imgwidth,
                        height: ho.imgheight,
                        left: ho.imgleft,
                        top: ho.imgtop,
                        scaleX: .25,
                        scaleY: .25,
                        selectable: false,
                        id: 'img-' + ho.name,
                        hoverCursor: "default",
                        
                     };
                     var printzelfImg = new Image();
                     printzelfImg.onload = function (img) {
                        var printzelf = new fabric.Image(printzelfImg, imgProps);
                        printzelf.trippyHotspotImage = true;
                        canvas.add(printzelf);
                     };
                     printzelfImg.src = ho.imgUrl;
                     // update the `loadedImages` array
                     loadedImages.push(ho.name);
                  } else {
                     for (const o of canvas.getObjects()) {
                        if (o.id && o.id === 'img-' + ho.name) {
                           o.visible = true;
                           break;
                        }
                     }
                     canvas.renderAll();
                  }
               }
            }
         });
      },
      onHide(instance) {
         for (const o of canvas.getObjects()) {
            if (o.trippyHotspotImage) {
               o.visible = false;
            }
         }
         canvas.renderAll();
      },
      content: function (reference) {
        return reference.querySelector('.tooltipcontent' + (i + 1));
      }
    });
      });
    })();
      </script>
      </body>
    
      </html>