pxt-ev3/sim/visuals/util.ts
Sam El-Husseini b69af383a6
Better mouse event handling (#552)
* Use learnings from touch events in Blockly and apply to the Ev3 sim. Register move and up events on the document rather than the individual element allowing users greater flexibility where they move their mouse once it's down.

* Add mouse leave events
2018-05-02 13:54:06 -07:00

151 lines
5.6 KiB
TypeScript

namespace pxsim.visuals {
export interface LinearGradientDefinition {
stops: LinearGradientStop[];
}
export interface LinearGradientStop {
offset: string | number;
color: string;
}
export type TouchCallback = (event: MouseEvent | TouchEvent | PointerEvent) => void;
export function touchEvents(e: SVGElement | SVGElement[], move?: TouchCallback, down?: TouchCallback, up?: TouchCallback) {
if (Array.isArray(e)) {
e.forEach(el => bindEvents(el, move, down, up));
}
else {
bindEvents(e, move, down, up);
}
}
function bindEvents(e: SVGElement, move?: TouchCallback, down?: TouchCallback, up?: TouchCallback) {
const moveEvent = move ? (ev: MouseEvent) => {
move.call(this, ev);
ev.preventDefault();
ev.stopPropagation();
} : undefined;
const enterEvent = move ? (ev: MouseEvent) => {
if (ev.buttons != 1) {
// cancel all events when we re-enter without a button down
upEvent(ev);
}
} : undefined;
const upEvent = up ? (ev: MouseEvent) => {
up.call(this, ev);
ev.preventDefault();
ev.stopPropagation();
// Unregister document up and move events
if ((window as any).PointerEvent) {
if (moveEvent) document.removeEventListener("pointermove", moveEvent);
if (upEvent) document.removeEventListener("pointerup", upEvent);
if (upEvent) document.removeEventListener("pointercancel", upEvent);
if (moveEvent) document.removeEventListener("pointerenter", enterEvent);
} else {
if (moveEvent) document.removeEventListener("mousemove", moveEvent);
if (upEvent) document.removeEventListener("mouseup", upEvent);
if (moveEvent) document.removeEventListener("mouseenter", enterEvent);
if (pxsim.svg.isTouchEnabled()) {
if (moveEvent) document.removeEventListener("touchmove", moveEvent);
if (upEvent) document.removeEventListener("touchend", upEvent);
if (upEvent) document.removeEventListener("touchcancel", upEvent);
}
}
} : undefined;
const downEvent = down ? (ev: MouseEvent) => {
down.call(this, ev);
ev.preventDefault();
ev.stopPropagation();
// Register document up and move events
if ((window as any).PointerEvent) {
if (moveEvent) document.addEventListener("pointermove", moveEvent);
if (upEvent) document.addEventListener("pointerup", upEvent);
if (upEvent) document.addEventListener("pointercancel", upEvent);
if (moveEvent) document.addEventListener("pointerenter", enterEvent);
} else {
if (moveEvent) document.addEventListener("mousemove", moveEvent);
if (upEvent) document.addEventListener("mouseup", upEvent);
if (moveEvent) document.addEventListener("mouseenter", enterEvent);
if (pxsim.svg.isTouchEnabled()) {
if (moveEvent) document.addEventListener("touchmove", moveEvent);
if (upEvent) document.addEventListener("touchend", upEvent);
if (upEvent) document.addEventListener("touchcancel", upEvent);
}
}
} : undefined;
if ((window as any).PointerEvent) {
if (downEvent) e.addEventListener("pointerdown", downEvent);
}
else {
if (downEvent) e.addEventListener("mousedown", downEvent);
if (pxsim.svg.isTouchEnabled()) {
if (downEvent) e.addEventListener("touchstart", downEvent);
}
}
}
export function createGradient(id: string, opts: LinearGradientDefinition) {
const g = svg.elt("linearGradient") as SVGLinearGradientElement;
g.setAttribute("id", id);
opts.stops.forEach(stop => {
let offset: string;
if (typeof stop.offset === "number") {
offset = stop.offset + "%"
}
else {
offset = stop.offset as string;
}
svg.child(g, "stop", { offset, "stop-color": stop.color });
});
return g;
}
export function updateGradient(gradient: SVGLinearGradientElement, opts: LinearGradientDefinition) {
let j = 0;
forEachElement(gradient.childNodes, (e, i) => {
if (i < opts.stops.length) {
const stop = opts.stops[i];
e.setAttribute("offset", offsetString(stop.offset));
e.setAttribute("stop-color", stop.color);
}
else {
gradient.removeChild(e);
}
j = i + 1;
});
for (; j < opts.stops.length; j++) {
const stop = opts.stops[j];
svg.child(gradient, "stop", { offset: offsetString(stop.offset), "stop-color": stop.color });
}
}
export function forEachElement(nodes: NodeList, cb: (e: Element, i: number) => void) {
let index = 0;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
cb(node as Element, index);
++index;
}
}
}
function offsetString(offset: string | number) {
return (typeof offset === "number") ? offset + "%" : offset;
}
}