2022-01-24 19:33:16 +00:00
|
|
|
import constrainNumeric from './helpers/constrainNumeric';
|
|
|
|
|
|
|
|
export enum HoverState {
|
|
|
|
Normal,
|
|
|
|
OverSelectionStart,
|
|
|
|
OverSelectionEnd,
|
|
|
|
OverSelection,
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum EmptySelectionAction {
|
|
|
|
SelectNothing,
|
|
|
|
SelectPrevious,
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Selection {
|
|
|
|
start: number;
|
|
|
|
end: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum SelectionMode {
|
|
|
|
Normal,
|
|
|
|
Selecting,
|
|
|
|
Dragging,
|
|
|
|
ResizingStart,
|
|
|
|
ResizingEnd,
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface State {
|
|
|
|
width: number;
|
|
|
|
emptySelectionAction: EmptySelectionAction;
|
|
|
|
hoverX: number;
|
|
|
|
selection: Selection;
|
|
|
|
origSelection: Selection;
|
|
|
|
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?: Selection;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const stateReducer = (
|
|
|
|
{
|
|
|
|
selection: prevSelection,
|
|
|
|
origSelection,
|
|
|
|
mousedownX: prevMousedownX,
|
|
|
|
mode: prevMode,
|
|
|
|
width,
|
|
|
|
emptySelectionAction,
|
|
|
|
}: State,
|
|
|
|
{ x, type, selection: selectionToSet }: SelectionAction
|
|
|
|
): State => {
|
|
|
|
let mode: SelectionMode;
|
|
|
|
let newSelection: Selection;
|
|
|
|
let mousedownX: number;
|
|
|
|
let cursorClass: string;
|
|
|
|
let hoverState: HoverState;
|
2022-01-29 11:32:39 +00:00
|
|
|
let shouldPublish: boolean | null = null;
|
2022-01-24 19:33:16 +00:00
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'setselection':
|
|
|
|
newSelection = selectionToSet || { start: 0, end: 0 };
|
|
|
|
mousedownX = prevMousedownX;
|
|
|
|
mode = SelectionMode.Normal;
|
|
|
|
cursorClass = 'cursor-auto';
|
|
|
|
hoverState = HoverState.Normal;
|
2022-01-29 11:32:39 +00:00
|
|
|
shouldPublish = false;
|
2022-01-24 19:33:16 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
case 'mousedown':
|
|
|
|
mousedownX = x;
|
|
|
|
cursorClass = 'cursor-auto';
|
|
|
|
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 = { start: x, end: x };
|
|
|
|
mode = SelectionMode.Selecting;
|
|
|
|
cursorClass = 'cursor-col-resize';
|
|
|
|
}
|
|
|
|
|
|
|
|
origSelection = newSelection;
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'mouseup':
|
|
|
|
newSelection = prevSelection;
|
|
|
|
mousedownX = prevMousedownX;
|
|
|
|
mode = SelectionMode.Normal;
|
|
|
|
cursorClass = 'cursor-auto';
|
|
|
|
hoverState = HoverState.Normal;
|
|
|
|
|
|
|
|
if (
|
|
|
|
newSelection.start == newSelection.end &&
|
|
|
|
emptySelectionAction == EmptySelectionAction.SelectNothing
|
|
|
|
) {
|
|
|
|
newSelection = { start: x, end: x };
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'mouseleave':
|
|
|
|
newSelection = prevSelection;
|
|
|
|
mousedownX = prevMousedownX;
|
|
|
|
mode = prevMode;
|
|
|
|
cursorClass = 'cursor-auto';
|
|
|
|
hoverState = HoverState.Normal;
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'mousemove':
|
|
|
|
mousedownX = prevMousedownX;
|
|
|
|
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 = {
|
|
|
|
start: x,
|
|
|
|
end: prevMousedownX,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
newSelection = {
|
|
|
|
start: prevMousedownX,
|
|
|
|
end: x,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SelectionMode.Dragging: {
|
|
|
|
mode = SelectionMode.Dragging;
|
|
|
|
cursorClass = 'cursor-move';
|
|
|
|
|
|
|
|
const diff = x - prevMousedownX;
|
|
|
|
const selectionWidth = origSelection.end - origSelection.start;
|
|
|
|
|
|
|
|
let start = Math.max(0, origSelection.start + diff);
|
|
|
|
let end = start + selectionWidth;
|
|
|
|
if (end > width) {
|
|
|
|
end = width;
|
|
|
|
start = end - selectionWidth;
|
|
|
|
}
|
|
|
|
newSelection = { start: start, end: end };
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SelectionMode.ResizingStart: {
|
|
|
|
mode = SelectionMode.ResizingStart;
|
|
|
|
cursorClass = 'cursor-col-resize';
|
|
|
|
|
|
|
|
const start = constrainNumeric(x, width);
|
|
|
|
if (start > origSelection.end) {
|
|
|
|
newSelection = { start: origSelection.end, end: start };
|
|
|
|
} else {
|
|
|
|
newSelection = { ...origSelection, start: start };
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SelectionMode.ResizingEnd: {
|
|
|
|
mode = SelectionMode.ResizingEnd;
|
|
|
|
cursorClass = 'cursor-col-resize';
|
|
|
|
|
|
|
|
const end = constrainNumeric(x, width);
|
|
|
|
if (end < origSelection.start) {
|
|
|
|
newSelection = {
|
|
|
|
start: end,
|
|
|
|
end: origSelection.start,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
newSelection = { ...origSelection, end: x };
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error();
|
|
|
|
}
|
|
|
|
|
2022-01-29 11:32:39 +00:00
|
|
|
// by default, only trigger the callback if the selection or mode has changed.
|
|
|
|
if (shouldPublish == null) {
|
|
|
|
shouldPublish = newSelection != prevSelection || mode != prevMode;
|
|
|
|
}
|
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
return {
|
|
|
|
width: width,
|
|
|
|
emptySelectionAction: emptySelectionAction,
|
|
|
|
hoverX: x,
|
|
|
|
selection: newSelection,
|
|
|
|
origSelection: origSelection,
|
|
|
|
mousedownX: mousedownX,
|
|
|
|
mode: mode,
|
|
|
|
prevMode: prevMode,
|
|
|
|
cursorClass: cursorClass,
|
|
|
|
hoverState: hoverState,
|
2022-01-29 11:32:39 +00:00
|
|
|
shouldPublish: shouldPublish,
|
2022-01-24 19:33:16 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
// helpers
|
|
|
|
|
|
|
|
const hoverOffset = 10;
|
|
|
|
|
|
|
|
const isHoveringSelectionStart = (x: number, selection: Selection): boolean => {
|
|
|
|
return x > selection.start - hoverOffset && x < selection.start + hoverOffset;
|
|
|
|
};
|
|
|
|
|
|
|
|
const isHoveringSelectionEnd = (x: number, selection: Selection): boolean => {
|
|
|
|
return x > selection.end - hoverOffset && x < selection.end + hoverOffset;
|
|
|
|
};
|
|
|
|
|
|
|
|
const isHoveringSelection = (x: number, selection: Selection): boolean => {
|
|
|
|
return x >= selection.start && x <= selection.end;
|
|
|
|
};
|