CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-pigeon-maps

ReactJS maps without external dependencies - performance-first React-centric extendable map engine

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

draggable.mddocs/

Draggable Elements

System for making map elements draggable with comprehensive touch and mouse support. The Draggable component wraps other elements and provides smooth drag interactions with geographic coordinate updates.

Capabilities

Draggable Component

Wrapper component that makes child elements draggable on the map with mouse and touch support.

/**
 * Makes child elements draggable on the map with touch and mouse support
 * @param props - Draggable configuration and event handlers
 * @returns JSX.Element representing the draggable container
 */
function Draggable(props: DraggableProps): JSX.Element;

interface DraggableProps extends PigeonProps {
  // Styling
  className?: string;
  style?: React.CSSProperties;
  
  // Content
  children?: React.ReactNode;
  
  // Drag event handlers
  onDragStart?: (anchor: Point) => void;
  onDragMove?: (anchor: Point) => void;
  onDragEnd?: (anchor: Point) => void;
}

Usage Examples:

import React, { useState } from "react";
import { Map, Draggable, Marker } from "pigeon-maps";

// Basic draggable marker
function DraggableMarker() {
  const [position, setPosition] = useState([50.879, 4.6997]);

  return (
    <Map height={400} center={position} zoom={11}>
      <Draggable
        anchor={position}
        onDragEnd={(newPosition) => {
          setPosition(newPosition);
        }}
      >
        <Marker anchor={position} />
      </Draggable>
    </Map>
  );
}

// Draggable custom content
function DraggableContent() {
  const [position, setPosition] = useState([50.879, 4.6997]);

  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      <Draggable
        anchor={position}
        onDragStart={(anchor) => {
          console.log('Started dragging at:', anchor);
        }}
        onDragMove={(anchor) => {
          console.log('Dragging to:', anchor);
        }}
        onDragEnd={(anchor) => {
          console.log('Drag ended at:', anchor);
          setPosition(anchor);
        }}
      >
        <div style={{
          background: 'white',
          border: '2px solid #333',
          borderRadius: '8px',
          padding: '12px',
          cursor: 'grab'
        }}>
          Drag me!
        </div>
      </Draggable>
    </Map>
  );
}

Multiple Draggable Elements

function MultipleDraggables() {
  const [markers, setMarkers] = useState([
    { id: 1, position: [50.879, 4.6997], name: 'Marker 1' },
    { id: 2, position: [50.885, 4.7050], name: 'Marker 2' },
    { id: 3, position: [50.875, 4.6900], name: 'Marker 3' }
  ]);

  const updateMarkerPosition = (id, newPosition) => {
    setMarkers(markers.map(marker => 
      marker.id === id 
        ? { ...marker, position: newPosition }
        : marker
    ));
  };

  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      {markers.map(marker => (
        <Draggable
          key={marker.id}
          anchor={marker.position}
          onDragEnd={(newPosition) => {
            updateMarkerPosition(marker.id, newPosition);
          }}
        >
          <Marker anchor={marker.position} payload={marker} />
        </Draggable>
      ))}
    </Map>
  );
}

Drag Event Handling

Event Lifecycle

The draggable component provides three event handlers for the complete drag lifecycle:

/**
 * Called when drag operation begins
 * @param anchor - Geographic coordinates where drag started
 */
onDragStart?: (anchor: Point) => void;

/**
 * Called continuously during drag operation
 * @param anchor - Current geographic coordinates during drag
 */
onDragMove?: (anchor: Point) => void;

/**
 * Called when drag operation ends
 * @param anchor - Final geographic coordinates where drag ended
 */
onDragEnd?: (anchor: Point) => void;

Coordinate Updates

All drag events provide geographic coordinates (Point as [lat, lng]) rather than pixel coordinates, automatically handling the conversion from screen pixels to map coordinates.

function TrackingDraggable() {
  const [dragPath, setDragPath] = useState([]);
  const [isDragging, setIsDragging] = useState(false);

  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      <Draggable
        anchor={[50.879, 4.6997]}
        onDragStart={(anchor) => {
          setIsDragging(true);
          setDragPath([anchor]);
        }}
        onDragMove={(anchor) => {
          setDragPath(path => [...path, anchor]);
        }}
        onDragEnd={(anchor) => {
          setIsDragging(false);
          console.log('Drag path:', dragPath);
        }}
      >
        <div style={{
          background: isDragging ? 'red' : 'blue',
          width: 20,
          height: 20,
          borderRadius: '50%'
        }} />
      </Draggable>
    </Map>
  );
}

Input Handling

Mouse Support

  • Mouse Down: Initiates drag when clicking on the draggable element
  • Mouse Move: Updates position during drag
  • Mouse Up: Completes drag operation

Touch Support

  • Touch Start: Initiates drag when touching the draggable element
  • Touch Move: Updates position during drag
  • Touch End: Completes drag operation

Event Prevention

The draggable component automatically:

  • Prevents default browser behaviors during drag
  • Stops event propagation to prevent map interactions
  • Handles both mouse and touch events simultaneously

Styling and Visual Feedback

Cursor States

// Automatic cursor styling
cursor: isDragging ? 'grabbing' : 'grab'

The component automatically updates the cursor to provide visual feedback:

  • grab: When hoverable but not dragging
  • grabbing: During active drag operation

Custom Styling

function StyledDraggable() {
  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      <Draggable
        anchor={[50.879, 4.6997]}
        style={{
          transform: 'scale(1.1)', // Make slightly larger
          transition: 'transform 0.2s' // Smooth scaling
        }}
        className="custom-draggable"
      >
        <div>Styled draggable content</div>
      </Draggable>
    </Map>
  );
}

CSS Classes

// Default CSS class
className="pigeon-drag-block"

// Custom additional classes
className="pigeon-drag-block my-custom-class"

The component automatically adds the pigeon-drag-block class, which:

  • Prevents map drag interactions when interacting with the draggable element
  • Provides a hook for custom CSS styling

Advanced Usage

Constrained Dragging

function ConstrainedDraggable() {
  const [position, setPosition] = useState([50.879, 4.6997]);
  const bounds = {
    north: 50.89,
    south: 50.87,
    east: 4.71,
    west: 4.69
  };

  const constrainPosition = (newPosition) => {
    const [lat, lng] = newPosition;
    return [
      Math.max(bounds.south, Math.min(bounds.north, lat)),
      Math.max(bounds.west, Math.min(bounds.east, lng))
    ];
  };

  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      <Draggable
        anchor={position}
        onDragEnd={(newPosition) => {
          const constrainedPosition = constrainPosition(newPosition);
          setPosition(constrainedPosition);
        }}
      >
        <Marker anchor={position} />
      </Draggable>
    </Map>
  );
}

Snapping to Grid

function SnappingDraggable() {
  const [position, setPosition] = useState([50.879, 4.6997]);
  const gridSize = 0.01; // Grid spacing in degrees

  const snapToGrid = (position) => {
    const [lat, lng] = position;
    return [
      Math.round(lat / gridSize) * gridSize,
      Math.round(lng / gridSize) * gridSize
    ];
  };

  return (
    <Map height={400} center={[50.879, 4.6997]} zoom={11}>
      <Draggable
        anchor={position}
        onDragEnd={(newPosition) => {
          const snappedPosition = snapToGrid(newPosition);
          setPosition(snappedPosition);
        }}
      >
        <Marker anchor={position} />
      </Draggable>
    </Map>
  );
}

Performance Considerations

  • Drag events are throttled to prevent excessive updates
  • Only the final position is typically persisted to state
  • Use onDragMove sparingly for performance-critical applications
  • Consider debouncing external API calls triggered by drag events
  • The component uses React refs to minimize re-renders during drag operations

Integration with Map Controls

The Draggable component integrates seamlessly with map settings:

  • Respects mouseEvents and touchEvents map props
  • Works with all zoom levels and map transformations
  • Automatically handles coordinate system conversions
  • Compatible with all other overlay types

Install with Tessl CLI

npx tessl i tessl/npm-pigeon-maps

docs

controls.md

draggable.md

geojson.md

index.md

map.md

markers-overlays.md

providers.md

tile.json