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 :
- Créer une classe dérivée de
linkTools.Tool - Définir la logique d'accroche et le retour visuel
- 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
throttleoudebouncepour les événements tels quepointermove - 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.