DEV Community

Cover image for Creating a Performant Rectangle Selection Box with Callbacks Using Web Components
Pratik sharma
Pratik sharma

Posted on • Originally published at blog.coolhead.in

Creating a Performant Rectangle Selection Box with Callbacks Using Web Components

In this article, you'll learn how to create a performant rectangle selection box using web components and JavaScript. The objective is to build an interactive tool that allows users to select multiple elements within a defined rectangular area, with a callback function that provides all the elements within the selection.

Let's break down the implementation into small, manageable problems.

1. Getting DOM Elements within a Rectangle

To identify all elements within the selection rectangle, we need to check if the rectangle boundaries intersect with the bounding boxes of the elements on the page.

getElementsWithinSelection(rect) {
  const allElements = Array.from(document.body.querySelectorAll('*'));
  return allElements.filter(element => {
    const elRect = element.getBoundingClientRect();
    return (
      elRect.top < rect.bottom &&
      elRect.bottom > rect.top &&
      elRect.left < rect.right &&
      elRect.right > rect.left
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • getBoundingClientRect() gets the position and dimensions of an element.

  • We filter the elements whose bounding boxes intersect with the given rectangle.

2. Drawing/Rendering the Selection UI

To visually represent the selection area, we'll create an overlay element styled with a transparent background.

<style>
  :host {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 9999;
    pointer-events: none;
  }
  .selection-overlay {
    position: absolute;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 255, 0.2);
    clip-path: polygon(0 0, 0 0, 0 0, 0 0);
    opacity: 0;
  }
</style>
<div class="selection-overlay"></div>
Enter fullscreen mode Exit fullscreen mode

3. Creating a Component Using Web Components

Web components encapsulate the functionality and styling, making the selection tool reusable and modular.

Constructor

constructor() {
  super();
  this.attachShadow({ mode: 'open' });

  this.startX = 0;
  this.startY = 0;
  this.isSelecting = false;

  this.shadowRoot.innerHTML = `
  <style>
    :host {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 9999;
      pointer-events: none;
    }
    .selection-overlay {
      position: absolute;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 255, 0.2);
      clip-path: polygon(0 0, 0 0, 0 0, 0 0);
      opacity: 0;
    }
  </style>
  <div class="selection-overlay"></div>
`;

  this.overlay = this.shadowRoot.querySelector('.selection-overlay');
}
Enter fullscreen mode Exit fullscreen mode

Connecting and Disconnecting Event Listeners

We add mouse event listeners to capture user interactions.

connectedCallback() {
  this.addEventListeners();
}

disconnectedCallback() {
  this.removeEventListeners();
}

addEventListeners() {
  document.addEventListener('mousedown', (event) => this.onMouseDown(event));
  document.addEventListener('mouseup', (event) => this.onMouseUp(event));
  document.addEventListener('mousemove', (event) => this.onMouseMove(event));
}

removeEventListeners() {
  document.removeEventListener('mousedown', this.onMouseDown);
  document.removeEventListener('mouseup', this.onMouseUp);
  document.removeEventListener('mousemove', this.onMouseMove);
}
Enter fullscreen mode Exit fullscreen mode

4. Adding Props/Attributes to Web Components

We can define attributes that control the behavior or appearance of the component. In this case, let's support a customizable background.

static get observedAttributes() {
  return ['background'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'background') {
    this.setBackground(newValue);
  }
}

setBackground(background) {
  this.overlay.style.background = background;
}
Enter fullscreen mode Exit fullscreen mode

5. Propagating Custom Events with Callbacks

When the selection is complete, dispatch a custom event with the selected elements.

this.dispatchEvent(new CustomEvent('selection-complete', {
  detail: { elements: elementsWithinSelection },
  bubbles: true,
  composed: true,
}));
Enter fullscreen mode Exit fullscreen mode

6. Using the Component and Listening for Callbacks

To use the custom selection tool component and handle the callback, add the following code:

<cr-selection id="selection-tool" background="rgba(255, 0, 0, 0.1)"></cr-selection>
Enter fullscreen mode Exit fullscreen mode
const selectionTool = document.getElementById('selection-tool');

// Listen for the selection-complete event
selectionTool.addEventListener('selection-complete', (event) => {
  const selectedElements = event.detail.elements;

  // Log the selected elements
  console.log('Selected Elements:', selectedElements);

  // Highlight the selected elements
  selectedElements.forEach(el => {
    el.style.border = '2px solid red';
  });
});
Enter fullscreen mode Exit fullscreen mode

How It Works:

  1. The component listens for mouse events to define the selection rectangle.

  2. When the selection completes, the selection-complete event fires with the selected elements.

  3. The callback function handles highlighting the selected elements.

Full Javascript Code

class CrSelection extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });

        this.startX = 0;
        this.startY = 0;
        this.isSelecting = false;

        this.shadowRoot.innerHTML = `
        <style>
          :host {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 9999;
            pointer-events: none;
          }
          .selection-overlay {
            position: absolute;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 255, 0.2);
            clip-path: polygon(0 0, 0 0, 0 0, 0 0);
            opacity: 0;
          }
        </style>
        <div class="selection-overlay"></div>
      `;

        this.overlay = this.shadowRoot.querySelector('.selection-overlay');
    }

    static get observedAttributes() {
        return ['background']
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'background') {
            this.setBackground(newValue);
        }
    }

    setBackground(background) {
        this.overlay.style.background = background;
    }



    connectedCallback() {
        this.addEventListeners();
    }

    disconnectedCallback() {
        this.removeEventListeners();
    }

    addEventListeners() {
        document.addEventListener('mousedown', (event) => this.onMouseDown(event));
        document.addEventListener('mouseup', (event) => this.onMouseUp(event));
        document.addEventListener('mousemove', (event) => this.onMouseMove(event));
    }

    removeEventListeners() {
        document.removeEventListener('mousedown', this.onMouseDown);
        document.removeEventListener('mouseup', this.onMouseUp);
        document.removeEventListener('mousemove', this.onMouseMove);
    }

    onMouseDown(event) {
        if (event.button !== 0) return; // Only left-click

        this.startX = event.clientX;
        this.startY = event.clientY;
        this.isSelecting = true;

        this.overlay.style.clipPath = 'none'; // Reset the clip-path
    }

    onMouseMove(event) {
        if (!this.isSelecting) return;

        const currentX = event.clientX;
        const currentY = event.clientY;

        const x1 = Math.min(this.startX, currentX);
        const y1 = Math.min(this.startY, currentY);
        const x2 = Math.max(this.startX, currentX);
        const y2 = Math.max(this.startY, currentY);
        this.overlay.style.opacity = '100'


        this.overlay.style.clipPath = `
            polygon(
                ${x1}px ${y1}px, 
                ${x2}px ${y1}px, 
                ${x2}px ${y2}px, 
                ${x1}px ${y2}px
            )
        `;
    }

    onMouseUp(event) {
        if (!this.isSelecting) return;

        this.isSelecting = false;

        const selectionRect = {
            top: Math.min(this.startY, event.clientY),
            left: Math.min(this.startX, event.clientX),
            right: Math.max(this.startX, event.clientX),
            bottom: Math.max(this.startY, event.clientY),
        };

        const elementsWithinSelection = this.getElementsWithinSelection(selectionRect);

        this.dispatchEvent(new CustomEvent('selection-complete', {
            detail: { elements: elementsWithinSelection },
            bubbles: true,
            composed: true,
        }));

        console.log('elements in selection', elementsWithinSelection);

        this.overlay.style.clipPath = 'none'; // Clear selection area
        this.overlay.style.opacity = '0'
    }

    getElementsWithinSelection(rect) {
        const allElements = Array.from(document.body.querySelectorAll('*'));

        return allElements.filter(element => {
            if (element === this || this.contains(element)) {
                return false;
            }
            const elRect = element.getBoundingClientRect();
            return (
                elRect.top < rect.bottom &&
                elRect.bottom > rect.top &&
                elRect.left < rect.right &&
                elRect.right > rect.left
            );
        });
    }
}

customElements.define('cr-selection', CrSelection);
Enter fullscreen mode Exit fullscreen mode

Codepen

Here the Full Code in Codepen :

Try Click and Dragging in the above Codepen

Conclusion

With this approach, we've built a performant and customizable rectangle selection box using web components. This tool can be extended for various applications such as graphic editors, data selection, or visual analysis tools. The modular design ensures it's reusable and maintainable for future projects.

Top comments (0)