skip to Main Content

I have an editor in which the user can create a banner the user can drag the element in any position he/she wants inside a banner, the element has a tooltip, on hover, it should show the tooltip positioned on the side where the space is larger than the rest (top, left, bottom, right) and the tooltip should never go outside the container no matter what.

HTML

<div id="banner-container" class="banner-container">
    <span
        (cdkDragReleased)="onCircleButtonDragEnd($event)"
        id="point-button"
        class="point-button"
        cdkDragBoundary=".banner-container"
        cdkDrag
        [style.left]="banner.bannerElements.x"
        [style.top]="banner.bannerElements.y"
        [attr.data-id]="banner.bannerElements.buttonId"
        [id]="'button-' + banner.bannerElements.buttonId"
    ></span>
    <span
        id="tooltip"
        [style.left]="banner.bannerElements.x"
        [style.top]="banner.bannerElements.y"
        [attr.data-id]="banner.bannerElements.tooltipId"
        [id]="'button-' + banner.bannerElements.tooltipId"
    >
        Szanujemy Twoją prywatność
    </span>
</div>

TS

  banner = {
        buttonId: 11,
        tooltipId: 2,
        x: 0,
        y: 0
    };

onCircleButtonDragEnd(event) {
        const container = event.currentTarget as HTMLElement;
        const containerWidth = container.clientWidth;
        const containerHeight = container.clientHeight;

        this.banner.y =
            ((event.clientX - container.getBoundingClientRect().left) /
                containerWidth) *
            100;
        this.banner.y =
            ((event.clientY - container.getBoundingClientRect().top) /
                containerHeight) *
            100;
    }``

CSS

.point-button {
  cursor: pointer;
  display: block;
  width: 24px;
  height: 24px;
  border: 2px solid rgb(179, 115, 188);
  background-color: rgb(255, 255, 255);
  background-image: none;
  border-radius: 100%;
  position: relative;
  z-index: 1;
  box-sizing: border-box;
}

.point-button:active {
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
    0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.banner-container {
  width: 350px;
  height: 200px;
  max-width: 100%;
  border: dotted #ccc 2px;
}
.tooltip {
  width: fit-content;
  height: 50px;
  border: 2px #ccc solid;
  display: none;
}
.point-button:hover + .tooltip {
  display: block;
}`

**

LIVE DEMO

** : DEMO

enter image description here
enter image description here
enter image description here
enter image description here

2

Answers


  1. Looking at the given demo there is a lot of things to cover:

    1. the $event passed to the onCircleButtonDragEnd is not the MouseEvent object; the mouse event can be accessed via $event.event so when you try to assign the event.currentTarget to the container constant, it returns an error,

    2. you don’t really need to set [style.left] and [style.top] of the point-button because the CdkDrag directive already sets the proper position using the transform: translate3d,

    3. you forgot to add the units to the [style.left] and [style.top] of the tooltip component so even if the values are calculated properly, the styles will not be bound to the element,

    4. if you want the tooltip to float next to the draggable point-button it’d be best to set position: relative for the banner-container and position: absolute for the tooltip, otherwise they may both be misaligned (as one element influences the position of another),

    5. in order to achieve what you want we need 3 things:

      I. the size of the banner-container in order to compute the position of the tooltip,

      II. the size of the tooltip as it influences the desired position,

      III. the size of the point-button.

    We can start solving this by:

    1. getting the mentioned sizes. The most straightforward way to achieve this in Angular is to use the @ViewChild decorator to get the ElementRef references and then get the desired dimensions.

    2. adding the AfterViewInit hook in the component in which:

      I. you save the tooltip dimensions to use in the future,

      II. you set the initial tooltip position – the point-button position is fixed so this should be quite easy (I did it with the setTimeout delayed by 0 so Angular didn’t complain about the value change during the lifecycles check),

      III. you hide the tooltip and set it’s position to absolute. Setting the position: absolute or display: none in CSS may result in the wrong dimensions calculation, that’s why I’d recommend to first display it the normal way and then hide. For the purpose of hiding the element in Angular you can just set its hidden attribute.

    The next steps are just pure maths. As you presented in the pictures the position of the tooltip should be dependent on the point-button position. If you analyze it carefully you will notice that one of the possible ways to do this is to divide the rectangle container into 4 triangles using its diagonals and then set the tooltip position accordingly:

    the container division

    That’s why we can tackle this problem by writing the function computeTheSegment that takes the position (x, y) of the point within the container of size (width, height) and then determines to which segment (T, R, B or L) the point belongs. It can be done by writing the diagonals equations explicitly (these are just the linear equations of the form y = ax + b) or by computing the projection coordinates for both the diagonals d1 and d2 and then comparing them to the original x and y.

    Once we are done we just need to update the tooltip position in the onCircleButtonDragEnd method. We can do this the following way:

    1. extract the circle-button position using the mentioned ElementRef, window.getComputedStyle and the WebKitCSSMatrix (to get the x and y translation from the transform: translate3d property)

    2. add half of the circle-button width to the extracted position to determine the central point of the button,

    3. compute the segment by providing the computed position and the banner-container sizes to the computeTheSegment method,

    4. finally, setting the x and y of the tooltip depending on the segment the circle-button belongs to:

      • for the top and bottom position set the x of the tooltip to be the circle-button position minus half of the tooltip’s width (and accordingly as it comes to the left and right positions and the y coordinate),

      • for the top and bottom position set the y of the tooltip to be next to the circle-button (and accordingly as it comes to the vertical segments),

      • remember that we don’t want the tooltip to overflow from the container, so it’s good to use the Math.max(0, x) function during the computation – if the value is negative it will be replaced by 0.

    I share the demo with you:
    DEMO

    Of course, it can be done in a more elegant way (e.g. with paying attention to the screen resize event which can influence the banner-container size). Nonetheless, I think it’s the fine approach to solve this problem.

    Login or Signup to reply.
  2. You can create a function that does the math and logic of determining the top and left properties of the tooltip position. Then you can easily apply them using [ngStyle]:

    <div #container class="banner-container">
      <span #element class="point-button" cdkDrag
        [cdkDragBoundary]="container"
        (cdkDragReleased)="updateTooltipStyle()"
      ></span>
      <span #tooltip class="tooltip" [ngStyle]="tooltipStyle">
        This is a tooltip :-)
      </span>
    </div>
    
    export class CdkDragDropHorizontalSortingExample implements AfterViewInit {
    
      @ViewChild('container') container!: ElementRef;
      @ViewChild('element') element!: ElementRef;
      @ViewChild('tooltip') tooltip!: ElementRef;
     
      tooltipStyle = {};
    
      ngAfterViewInit() {
        this.updateTooltipStyle();
      }
      
      updateTooltipStyle() {
        const parentRect  = this.container.nativeElement.getBoundingClientRect();
        const elementRect = this.element.nativeElement.getBoundingClientRect();
        const tooltipRect = this.tooltip.nativeElement.getBoundingClientRect();
      
        const spaceLeft   = elementRect.left  - parentRect.left;
        const spaceRight  = parentRect.right  - elementRect.right;
        const spaceTop    = elementRect.top   - parentRect.top;
        const spaceBottom = parentRect.bottom - elementRect.bottom;
    
        let tooltipTop, tooltipLeft;
    
        if (spaceLeft - spaceRight > spaceTop - spaceBottom) {
          tooltipLeft = spaceLeft > spaceRight
            ? elementRect.left - parentRect.left - tooltipRect.width - MARGIN
            : elementRect.right - parentRect.left + MARGIN;
    
          tooltipTop = Math.min(Math.max(0, (elementRect.top - parentRect.top) + (elementRect.height - tooltipRect.height) / 2), parentRect.height - tooltipRect.height);
    
        } else {
          tooltipTop = spaceTop > spaceBottom
            ? (elementRect.top - parentRect.top) - tooltipRect.height - MARGIN
            : (elementRect.bottom - parentRect.top) + MARGIN;
    
          tooltipLeft = Math.min(Math.max(0, (elementRect.left - parentRect.left) + (elementRect.width - tooltipRect.width) / 2), parentRect.width - tooltipRect.width);
        }
    
        this.tooltipStyle = {
          left : `${tooltipLeft}px`,
          top  : `${tooltipTop}px`,
        };
      }
    
    }
    

    Here’s a working StackBlitz demo.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search