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
);
});
}
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>
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');
}
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);
}
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;
}
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,
}));
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>
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';
});
});
How It Works:
The component listens for mouse events to define the selection rectangle.
When the selection completes, the
selection-complete
event fires with the selected elements.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);
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)