Angular 2 Move and Nudgeable Directive
Article Purpose
The purpose of this article is to provide an Angular 2 attribute directive that empowers any HTML
element to:
- Move via drag
- Move via arrow key nudge
- Clamp its position to its parent's bounds
I creatively call it MoveAndNudgableDirective
. Here are two samples of it in action:
Background
In experimenting with Angular 2 recently I re-created Buffer's image editing app Pablo (first example above). In working on the <canvas>
editor portion I designed the MoveAndNudgableDirective
directive. It allowed me to ensure both editable text components and custom graphic components (or any components for that matter) could easily be composed with the same behavior.
Though I stopped at my specific use case, this directive's code could be expanded upon and may be useful for others creating a wide range of web-based editor tooling. As a result I'm sharing it. Here are just a few ideas of its expansion:
- Employ ctrl/cmd + click for multi-select and shared movement.
- Employ shift + arrow keys for multiplied movement a la Photoshop's 10px nudge.
- Leverage
EventEmitter
to trigger actions of all sorts. Here are some ideas: - When an element is clamped to an edge you could reveal a UI that allows alignment to its corners or direct center.
- When an element comes in contact with another element they could impact each others movement.
- When an element comes in contact with another element a subtle delay could trigger a sequence of depth sorting where continuing movement would "select" the current depth sort.
- Get creative :)
Code
Below you'll find the code for this directive and below that you'll find the GitHub link for grabbing the example so you can download, manipulate, and interact with it yourself.
import { Directive, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import * as _ from 'lodash';
@Directive({
selector: '[move-and-nudgeable]',
host: {
'(mousedown)': 'onMouseDown($event)',
'(document: mousemove)': 'onMouseMove($event)',
'(document: mouseup)': 'onMouseUp($event)',
'(keydown.ArrowUp)': 'onNudge($event)',
'(keydown.ArrowRight)': 'onNudge($event)',
'(keydown.ArrowDown)': 'onNudge($event)',
'(keydown.ArrowLeft)': 'onNudge($event)',
'(keyup.ArrowUp)': 'onNudge($event)',
'(keyup.ArrowRight)': 'onNudge($event)',
'(keyup.ArrowDown)': 'onNudge($event)',
'(keyup.ArrowLeft)': 'onNudge($event)'
}
})
export class MoveAndNudgeableDirective {
//element refs
private el: HTMLElement;
private parent: HTMLElement;
//movable flag
private isMovable: boolean = false;
//position model helper
private pos: any = { x: 0, y: 0, clampX: 0, clampY: 0 };
//arrow key store
private keys: Array = [37, 38, 39, 40];
constructor(el: ElementRef) {
//el ref
this.el = el.nativeElement;
//tab index allows div to accept key events
this.el.tabIndex = 1;
//default key values
this.keys[37] = this.keys[38] = this.keys[39] = this.keys[40] = 0;
}
public update() {
//update ref due to ngIf
this.parent = this.el.parentElement;
//update clamp settings
this.pos.clampX = parseInt(this.parent.style.width.replace('px', '')) - parseInt(this.el.style.width.replace('px', ''));
this.pos.clampY = parseInt(this.parent.style.height.replace('px', '')) - parseInt(this.el.style.height.replace('px', ''));
//update position
this.updatePosition();
}
private updatePosition(x: number = 0, y: number = 0) {
//update data
this.pos.x += x;
this.pos.y += y;
//clamp data
this.pos.x = _.clamp(this.pos.x, 0, this.pos.clampX);
this.pos.y = _.clamp(this.pos.y, 0, this.pos.clampY);
//update view
this.el.style.left = this.pos.x + 'px';
this.el.style.top = this.pos.y + 'px';
}
private onMouseDown($event) {
//ensure fresh size calculation accounted for
this.update();
//update drag flag
this.isMovable = true;
}
private onMouseMove($event) {
//exit condition
if (!this.isMovable) { return; }
//update position
this.updatePosition($event.movementX, $event.movementY);
}
private onMouseUp($event) {
//update drag flag
this.isMovable = false;
}
private onNudge($event) {
//update keys
this.keys[$event.keyCode] = $event.type === 'keydown' ? 1 : 0;
//update move targets accommodating multiple keys
let x = 0 - this.keys[37] + this.keys[39];
let y = 0 - this.keys[38] + this.keys[40];
//update position
this.updatePosition(x, y);
}
}
Hit me up on Twitter @derekknox if you have any thoughts or improvement ideas.
MoveAndNudgableDirective on Github