267 lines
6.8 KiB
TypeScript
267 lines
6.8 KiB
TypeScript
import constrainNumeric from './helpers/constrainNumeric';
|
|
|
|
export const CanvasLogicalWidth = 2000;
|
|
export const CanvasLogicalHeight = 500;
|
|
|
|
export interface CanvasRange {
|
|
x1: number;
|
|
x2: number;
|
|
}
|
|
|
|
export enum HoverState {
|
|
Normal,
|
|
OverSelectionStart,
|
|
OverSelectionEnd,
|
|
OverSelection,
|
|
}
|
|
|
|
export enum EmptySelectionAction {
|
|
SelectNothing,
|
|
SelectPrevious,
|
|
}
|
|
|
|
export enum SelectionMode {
|
|
Normal,
|
|
Selecting,
|
|
Dragging,
|
|
ResizingStart,
|
|
ResizingEnd,
|
|
}
|
|
|
|
export interface State {
|
|
width: number;
|
|
emptySelectionAction: EmptySelectionAction;
|
|
hoverX: number;
|
|
selection: CanvasRange;
|
|
origSelection: CanvasRange;
|
|
mousedownX: number;
|
|
mode: SelectionMode;
|
|
prevMode: SelectionMode;
|
|
cursorClass: string;
|
|
hoverState: HoverState;
|
|
shouldPublish: boolean;
|
|
}
|
|
|
|
interface SelectionAction {
|
|
type: string;
|
|
x: number;
|
|
// TODO: selection is only used for the setselection SelectionAction. Improve
|
|
// the typing here.
|
|
selection?: CanvasRange;
|
|
}
|
|
|
|
export const stateReducer = (
|
|
{
|
|
selection: prevSelection,
|
|
origSelection,
|
|
mousedownX: prevMousedownX,
|
|
mode: prevMode,
|
|
width,
|
|
emptySelectionAction,
|
|
}: State,
|
|
{ x, type, selection: selectionToSet }: SelectionAction
|
|
): State => {
|
|
let mode: SelectionMode;
|
|
let newSelection: CanvasRange;
|
|
let hoverX: number;
|
|
let mousedownX: number;
|
|
let cursorClass: string;
|
|
let hoverState: HoverState;
|
|
let shouldPublish: boolean | null = null;
|
|
|
|
switch (type) {
|
|
case 'setselection':
|
|
newSelection = selectionToSet || { x1: 0, x2: 0 };
|
|
mousedownX = prevMousedownX;
|
|
mode = SelectionMode.Normal;
|
|
cursorClass = 'cursor-auto';
|
|
hoverX = x;
|
|
hoverState = HoverState.Normal;
|
|
shouldPublish = false;
|
|
|
|
break;
|
|
case 'mousedown':
|
|
mousedownX = x;
|
|
cursorClass = 'cursor-auto';
|
|
hoverX = x;
|
|
hoverState = HoverState.Normal;
|
|
|
|
if (isHoveringSelectionStart(x, prevSelection)) {
|
|
newSelection = prevSelection;
|
|
mode = SelectionMode.ResizingStart;
|
|
} else if (isHoveringSelectionEnd(x, prevSelection)) {
|
|
newSelection = prevSelection;
|
|
mode = SelectionMode.ResizingEnd;
|
|
} else if (isHoveringSelection(x, prevSelection)) {
|
|
newSelection = prevSelection;
|
|
mode = SelectionMode.Dragging;
|
|
cursorClass = 'cursor-move';
|
|
} else {
|
|
newSelection = { x1: x, x2: x };
|
|
mode = SelectionMode.Selecting;
|
|
cursorClass = 'cursor-col-resize';
|
|
}
|
|
|
|
origSelection = newSelection;
|
|
|
|
break;
|
|
case 'mouseup':
|
|
newSelection = prevSelection;
|
|
mousedownX = prevMousedownX;
|
|
mode = SelectionMode.Normal;
|
|
cursorClass = 'cursor-auto';
|
|
hoverX = x;
|
|
hoverState = HoverState.Normal;
|
|
|
|
if (
|
|
newSelection.x1 == newSelection.x2 &&
|
|
emptySelectionAction == EmptySelectionAction.SelectNothing
|
|
) {
|
|
newSelection = { x1: x, x2: x };
|
|
}
|
|
|
|
break;
|
|
case 'mouseleave':
|
|
newSelection = prevSelection;
|
|
mousedownX = prevMousedownX;
|
|
mode = prevMode;
|
|
cursorClass = 'cursor-auto';
|
|
hoverX = 0;
|
|
hoverState = HoverState.Normal;
|
|
|
|
break;
|
|
case 'mousemove':
|
|
mousedownX = prevMousedownX;
|
|
hoverX = x;
|
|
hoverState = HoverState.Normal;
|
|
|
|
switch (prevMode) {
|
|
case SelectionMode.Normal: {
|
|
newSelection = prevSelection;
|
|
mode = SelectionMode.Normal;
|
|
|
|
if (isHoveringSelectionStart(x, prevSelection)) {
|
|
cursorClass = 'cursor-col-resize';
|
|
hoverState = HoverState.OverSelectionStart;
|
|
} else if (isHoveringSelectionEnd(x, prevSelection)) {
|
|
cursorClass = 'cursor-col-resize';
|
|
hoverState = HoverState.OverSelectionEnd;
|
|
} else if (isHoveringSelection(x, prevSelection)) {
|
|
cursorClass = 'cursor-move';
|
|
hoverState = HoverState.OverSelection;
|
|
} else {
|
|
cursorClass = 'cursor-auto';
|
|
hoverState = HoverState.Normal;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SelectionMode.Selecting: {
|
|
cursorClass = 'cursor-col-resize';
|
|
mode = SelectionMode.Selecting;
|
|
if (x < prevMousedownX) {
|
|
newSelection = {
|
|
x1: x,
|
|
x2: prevMousedownX,
|
|
};
|
|
} else {
|
|
newSelection = {
|
|
x1: prevMousedownX,
|
|
x2: x,
|
|
};
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SelectionMode.Dragging: {
|
|
mode = SelectionMode.Dragging;
|
|
cursorClass = 'cursor-move';
|
|
|
|
const diff = x - prevMousedownX;
|
|
const selectionWidth = origSelection.x2 - origSelection.x1;
|
|
|
|
let start = Math.max(0, origSelection.x1 + diff);
|
|
let end = start + selectionWidth;
|
|
if (end > width) {
|
|
end = width;
|
|
start = end - selectionWidth;
|
|
}
|
|
newSelection = { x1: start, x2: end };
|
|
|
|
break;
|
|
}
|
|
case SelectionMode.ResizingStart: {
|
|
mode = SelectionMode.ResizingStart;
|
|
cursorClass = 'cursor-col-resize';
|
|
|
|
const start = constrainNumeric(x, width);
|
|
if (start > origSelection.x2) {
|
|
newSelection = { x1: origSelection.x2, x2: start };
|
|
} else {
|
|
newSelection = { ...origSelection, x1: start };
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SelectionMode.ResizingEnd: {
|
|
mode = SelectionMode.ResizingEnd;
|
|
cursorClass = 'cursor-col-resize';
|
|
|
|
const end = constrainNumeric(x, width);
|
|
if (end < origSelection.x1) {
|
|
newSelection = {
|
|
x1: end,
|
|
x2: origSelection.x1,
|
|
};
|
|
} else {
|
|
newSelection = { ...origSelection, x2: x };
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
default:
|
|
throw new Error();
|
|
}
|
|
|
|
// by default, only trigger the callback if the selection or mode has changed.
|
|
if (shouldPublish == null) {
|
|
shouldPublish = newSelection != prevSelection || mode != prevMode;
|
|
}
|
|
|
|
return {
|
|
width: width,
|
|
emptySelectionAction: emptySelectionAction,
|
|
hoverX: hoverX,
|
|
selection: newSelection,
|
|
origSelection: origSelection,
|
|
mousedownX: mousedownX,
|
|
mode: mode,
|
|
prevMode: prevMode,
|
|
cursorClass: cursorClass,
|
|
hoverState: hoverState,
|
|
shouldPublish: shouldPublish,
|
|
};
|
|
};
|
|
|
|
// helpers
|
|
|
|
const hoverOffset = 10;
|
|
|
|
const isHoveringSelectionStart = (
|
|
x: number,
|
|
selection: CanvasRange
|
|
): boolean => {
|
|
return x > selection.x1 - hoverOffset && x < selection.x1 + hoverOffset;
|
|
};
|
|
|
|
const isHoveringSelectionEnd = (x: number, selection: CanvasRange): boolean => {
|
|
return x > selection.x2 - hoverOffset && x < selection.x2 + hoverOffset;
|
|
};
|
|
|
|
const isHoveringSelection = (x: number, selection: CanvasRange): boolean => {
|
|
return x >= selection.x1 && x <= selection.x2;
|
|
};
|