Développer des outils interactifs personnalisés avec JointJS

Avantages de JointJS pour les outils interactifs

JointJS offre un cadre de développement d'outils complet et extensible. Il devient possible d'implémenter rapidement des comportements tels que le redimensionnement d'éléments, la connexion de nœuds ou l'édition directe sur le canvas.

Caractéristiques principales

  • API dédiée aux outils : interfaces complètes pour définir des comportements personnalisés
  • Rendu SVG : graphismes vectoriels de qualité et adaptables
  • Architecture événementielle : gestion fine des interactions utilisateur
  • Composants modulaires : outils réutilisables et faciles à maintenir

Création d'un outil personnalisé

1. Dériver une classe d'outil

La première étape consiste à étendre une classe de base fournie par JointJS. L'exemple ci-dessous définit un outil de repositionnement d'ancre situé en bas à droite d'un élément :

import { elementTools } from '@joint/core';

class AnchorTool extends elementTools.Control {
    getPosition(elementView) {
        const rect = elementView.model.size();
        return {
            x: rect.width,
            y: rect.height
        };
    }

    setPosition(elementView, coords) {
        const model = elementView.model;
        const newX = Math.max(coords.x, 20);
        const newY = Math.max(coords.y, 20);
        model.resize(newX, newY);
    }
}

Cet outil hérite de elementTools.Control et redéfinit getPosition et setPosition pour contrôler le déplacement du point d'interaction.

2. Enregistrer et attacher l'outil

Une fois la classe définie, elle doit être enregistrée auprès de JointJS puis ajoutée à un élément graphique :

joint.elementTools.register('anchor', AnchorTool);

const shape = new joint.shapes.standard.Rectangle();
shape.addTools([
    {
        name: 'anchor',
        options: {
            x: '100%',
            y: '100%',
            offset: { x: -10, y: -10 }
        }
    }
]);

3. Répondre aux événements utilisateur

Les outils peuvent écouter les événements du pointeur pour définir un comportement interactif :

class DragTool extends elementTools.Control {
    constructor(options) {
        super(options);
        this.startPoint = null;
        this.on('pointerdown', this.onStart.bind(this));
        this.on('pointermove', this.onDrag.bind(this));
        this.on('pointerup', this.onEnd.bind(this));
    }

    onStart(evt) {
        this.startPoint = { x: evt.clientX, y: evt.clientY };
    }

    onDrag(evt) {
        if (!this.startPoint) return;
        const dx = evt.clientX - this.startPoint.x;
        const dy = evt.clientY - this.startPoint.y;
        this.options.viewport.translate(dx, dy);
    }

    onEnd() {
        this.startPoint = null;
    }
}

Techniques avancées

Combinaison d'outils

Plusieurs outils peuvent être attachés simultanément à un même élément pour former une palette d'actions :

shape.addTools([
    { name: 'resize', options: { position: 'bottom-right' } },
    { name: 'rotate', options: { position: 'top-center' } },
    { name: 'remove', options: { position: 'top-right' } }
]);

Personnalisation visuelle

L'apparence d'un outil peut être ajustée via des styles CSS spécifiques :

.joint-tool.handle {
    fill: #2ecc71;
    stroke: #27ae60;
    stroke-width: 2px;
    cursor: crosshair;
}

.joint-tool.handle:hover {
    fill: #27ae60;
}

Gestion de l'état interne

Pour un outil plus complexe, il est utile de centraliser son état :

class StatefulHandle extends elementTools.Control {
    constructor(options) {
        super(options);
        this.mode = 'idle';
        this.on('pointerdown', () => this.setMode('active'));
        this.on('pointerup', () => this.setMode('idle'));
    }

    setMode(mode) {
        this.mode = mode;
        this.update();
    }

    render() {
        const isActive = this.mode === 'active';
        return this.vel.append(this.vel.tag('circle', {
            r: isActive ? 8 : 6,
            fill: isActive ? '#e74c3c' : '#3498db'
        }));
    }
}

Exemple pratique : outil de connexion personnalisé

Pour créer un outil de connexion entre éléments, les étapes suivantes s'appliquent :

  1. Créer une classe dérivée de linkTools.Tool
  2. Définir la logique d'accroche et le retour visuel
  3. Enregistrer l'outil et l'appliquer aux liens
import { linkTools } from '@joint/core';

class MagnetConnector extends linkTools.Tool {
    onRender() {
        this.vel.append(this.vel.tag('circle', {
            r: 7,
            fill: '#9b59b6',
            stroke: '#8e44ad',
            'stroke-width': 2
        }));
    }
}

Bonnes pratiques

Responsabilité unique

Chaque outil doit se concentrer sur une fonction précise. Séparer le redimensionnement, la rotation et la suppression en classes distinctes facilite la maintenance et la réutilisation.

TypeScript pour plus de robustesse

JointJS fournit des définitions de types complètes qui améliorent la fiabilité du code :

import { elementTools, ElementView, dia } from '@joint/core';

interface HandleOptions extends elementTools.Control.Options {
    color?: string;
}

class TypedHandle extends elementTools.Control<HandleOptions> {
    getPosition(view: ElementView): dia.Point {
        const { width, height } = view.model.size();
        return { x: width / 2, y: height };
    }

    setPosition(view: ElementView, point: dia.Point): void {
        const model = view.model;
        model.resize(
            Math.max(point.x, model.size().width),
            Math.max(point.y, model.size().height)
        );
    }
}

Optimisation des performances

  • Éviter les calculs lourds dans les gestionnaires déclenchés à haute fréquence
  • Utiliser throttle ou debounce pour les événements tels que pointermove
  • Mettre en cache les valeurs coûteuses lors de l'initialisation

JointJS offre donc un cadre souple pour créer des outils interactifs sur mesure. En combinant héritage de classes, gestion d'événements et personnalisation visuelle, il devient possible de proposer une expérience de diagrammation riche et professionnelle.

Étiquettes: JointJS SVG JavaScript diagrammes-interactifs outils-personnalisés

Publié le 14 septembre à 05h20