Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Drag Meta Provider #637

Merged
merged 10 commits into from
Oct 6, 2017
Merged
170 changes: 170 additions & 0 deletions src/meta/Drag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import global from '@dojo/shim/global';
import { assign } from '@dojo/shim/object';
import WeakMap from '@dojo/shim/WeakMap';
import { Base } from './Base';

export interface DragResults {
/**
* The movement of pointer during the duration of the drag state
*/
delta: Position;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we declare the interface before we use it? (I thought it was an from an import)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was favouring alphabetisation over use before declaration... one form of clarity causes another issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so.


/**
* Is the DOM node currently in a drag state
*/
isDragging: boolean;
}

interface NodeData {
dragResults: DragResults;
invalidate: () => void;
last: Position;
start: Position;
}

export interface Position {
x: number;
y: number;
}

/**
* A frozen empty result object, frozen to ensure that no one downstream modifies it
*/
const emptyResults = Object.freeze({
delta: Object.freeze({ x: 0, y: 0 }),
isDragging: false
});

/**
* Return the x/y position for an event
* @param e The MouseEvent or TouchEvent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now the PointerEvent?

*/
function getPosition(e: PointerEvent): Position {
return {
x: e.pageX,
y: e.pageY
};
}

/**
* Return the delta position between two positions
* @param start The first posistion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posistion -> position

* @param current The second posistion
*/
function getDelta(start: Position, current: Position): Position {
return {
x: current.x - start.x,
y: current.y - start.y
};
}

class DragController {
private _nodeMap = new WeakMap<HTMLElement, NodeData>();
private _dragging: HTMLElement | undefined = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be private _dragging?: HTMLElement;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be, but considering I was explicitly assigning undefined, the property will always be on the instance. Felt more accurate to not be optional.


private _getData(target: HTMLElement): { state: NodeData, target: HTMLElement } | undefined {
if (this._nodeMap.has(target)) {
return { state: this._nodeMap.get(target)!, target };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be so good if TS could know that we are in a block narrowed by this._nodeMap.has(target)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not surprised there’s an issue. That would be nice.

}
if (target.parentElement) {
return this._getData(target.parentElement);
}
}

private _onDragStart = (e: PointerEvent) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why private _onDragStart = (e: PointerEvent) => { over private _onDragStart(e: PointerEvent) {?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we have event over e?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the lambda, is because the handler needs to be bound to the instance. When used as a handler, the this scope is the window I believe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha re scope. Over having a separate variable that binds scope to the method (we have used both patterns)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I like this better for class methods where it effects the code readability less, IMO. Using the bound variable makes a bit of indirection when reading the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, for private methods.

const data = this._getData(e.target as HTMLElement);
if (data) {
const { state, target } = data;
this._dragging = target;
state.dragResults.isDragging = true;
state.last = state.start = getPosition(e);
state.dragResults.delta = { x: 0, y: 0 };
state.invalidate();
} // else, we are ignoring the event
}

private _onDrag = (e: PointerEvent) => {
const { _dragging } = this;
if (!_dragging) {
return;
}
// state cannot be unset, using ! operator
const state = this._nodeMap.get(_dragging)!;
state.last = getPosition(e);
state.dragResults.delta = getDelta(state.start, state.last);
state.invalidate();
}

private _onDragStop = (e: PointerEvent) => {
const { _dragging } = this;
if (!_dragging) {
return;
}
// state cannot be unset, using ! operator
const state = this._nodeMap.get(_dragging)!;
state.last = getPosition(e);
state.dragResults = {
delta: getDelta(state.start, state.last),
isDragging: false
};
state.invalidate();
this._dragging = undefined;
}

constructor() {
const win: Window = global.window;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't want to mask the global. We have tended to now do that, using doc or win when using a reference to a potential global.

win.addEventListener('pointerdown', this._onDragStart);
win.addEventListener('pointermove', this._onDrag, true);
win.addEventListener('pointerup', this._onDragStop, true);
}

public get(node: HTMLElement, invalidate: () => void): DragResults {
const { _nodeMap } = this;
// first time we see a node, we will initialize its state
if (!_nodeMap.has(node)) {
_nodeMap.set(node, {
dragResults: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use emptyResults?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apart from it being frozen... could we just deep assign the emptyResults when we return them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, empty results is valid here, I will use that

delta: { x: 0, y: 0 },
isDragging: false
},
invalidate,
last: { x: 0, y: 0 },
start: { x: 0, y: 0 }
});
return emptyResults;
}

const state = _nodeMap.get(node)!;
// we are offering up an accurate delta, so we need to take the last event position and move it to the start so
// that our deltas are calculated from the last time they are read
state.start = state.last;
// shallow "clone" the results, so no downstream manipulation can occur
const dragResults = assign({}, state.dragResults);

// reset the delta after we have read any last delta while not dragging
if (!dragResults.isDragging && dragResults.delta.x !== 0 && dragResults.delta.y !== 0) {
// future reads of the delta will be blank
state.dragResults.delta = { x: 0, y: 0 };
}

return dragResults;
}
}

const controller = new DragController();

export default class Drag extends Base {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally we do a named export and a default export across widget-core.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no problem

private boundInvalidate = this.invalidate.bind(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_boundInvalidate

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


public get(key: string): Readonly<DragResults> {
const node = this.getNode(key);

// if we don't have a reference to the node yet, return an empty set of results
if (!node) {
return emptyResults;
}

// otherwise we will ask the controller for our results
return controller.get(node, this.boundInvalidate);
}
}
Loading