labo-components
Version:
310 lines (268 loc) • 10.8 kB
JSX
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import IDUtil from "../../../../util/IDUtil";
import { getEventPath } from "../../../../util/EventPath";
import { MSAnnotationUtil } from "../../AnnotationClient";
const LEFT = "left";
const RIGHT = "right";
const BOTH = "both";
// A wrapper for editable section
class SectionEdit extends React.PureComponent {
constructor(props) {
super(props);
this.direction = LEFT;
this.lastX = 0;
this.lastUserLayer = null;
this.originalLayer = null;
this.selection = null;
this.dragging = false;
this.state = {
dragging: "",
};
this.ref = React.createRef();
}
componentWillUnmount() {
this.stopDrag();
}
/**
* Dragging section
*/
startDragSection = (e) => {
// shift key + left mouse button
if (e.shiftKey && e.button == 0) {
e.stopPropagation();
this.direction = BOTH;
this.startDrag(e.pageX, e.pageY);
}
};
/**
* Dragging timing left/right paddles to modify start/end position
*/
startDragLeft = (e) => {
if (!e.button == 0) {
return;
}
e.stopPropagation();
this.direction = e.shiftKey ? BOTH : LEFT;
this.startDrag(e.pageX, e.pageY);
};
startDragRight = (e) => {
if (!e.button == 0) {
return;
}
e.stopPropagation();
this.direction = e.shiftKey ? BOTH : RIGHT;
this.startDrag(e.pageX, e.pageY);
};
loadSelection() {
// get selection
this.selection = MSAnnotationUtil.extractSelectionFromTarget(
this.props.segment.target
);
if (!this.selection) {
console.error(
"Could not find selection for target",
this.props.segment.target
);
}
}
onWindowLeave = (e) => {
if (e.toElement == null && e.relatedTarget == null) {
this.stopDrag(null);
}
};
startDrag = (x, y) => {
if (this.dragging) {
return;
}
this.dragging = true;
this.lastX = x;
this.setState({ dragging: this.direction });
this.props.annotationClient.setActiveAnnotation(this.props.segment);
this.loadSelection();
// for dragging between layers
this.originalLayer =
this.ref.current && this.ref.current.parentNode
? this.ref.current.parentNode.parentNode
: null;
this.lastUserLayer = this.originalLayer;
document.addEventListener("mousemove", this.onDrag);
document.addEventListener("mouseup", this.stopDrag);
document.addEventListener("mouseleave", this.onWindowLeave);
};
stopDrag = (e) => {
if (!this.dragging) {
return;
}
this.dragging = false;
//this.direction = "";
this.setState({ dragging: "" });
document.removeEventListener("mousemove", this.onDrag);
document.removeEventListener("mouseup", this.stopDrag);
document.removeEventListener("mouseleave", this.onWindowLeave);
// Update section layer if the last hovered user layer differs from current user layer
// The layer id is obtained directly from the layer attribute of the last user layer DOM element
if (
this.ref.current &&
this.lastUserLayer &&
this.lastUserLayer != this.ref.current.parentNode &&
this.lastUserLayer.attributes.layer
) {
this.props.segment.target.layerId = parseInt(
this.lastUserLayer.attributes.layer.value
);
this.props.selectTimelineLayer(this.props.segment.target.layerId);
}
// Save changes
this.props.updateSegment(this.props.segment, this.selection);
};
onDrag = (e) => {
e.stopPropagation();
const dx = (e.pageX - this.lastX) / this.props.getPixelsPerSecond();
const movePlayPos = e.ctrlKey || e.metaKey;
switch (this.direction) {
case BOTH:
// use getEventPath to make it compatible to Chrome's e.path property
const path = getEventPath(e);
// Move segment between layers of type .tl-layer-header.user-segment
// Here we directly manipulate the DOM, to show layer shift, but
// prevent making changes to the data, which would result in the drag action to
// be halted.
if (this.ref.current && path) {
// check if we are hovering another user layer
const userLayer = path.find(
(p) =>
p.classList &&
p.classList.contains("bg__tl-layer") &&
p.classList.contains("user-segment")
);
// call may not be triggered by current ref
const isCurrentRef = path.some(
(p) => p == this.ref.current
);
// we hit a new user layer, so change section layer
if (
!isCurrentRef &&
userLayer &&
this.lastUserLayer !== userLayer
) {
// snap newTop to new layer
const newTop =
userLayer.offsetTop - this.originalLayer.offsetTop;
this.ref.current.parentNode.style.marginTop =
newTop + "px";
this.lastUserLayer = userLayer;
// update active timeline layer to indicate drop target
userLayer.attributes.layer &&
this.props.selectTimelineLayer(
parseInt(userLayer.attributes.layer.value)
);
}
}
// move selection
this.selection.start = this.props.withinRange(
this.selection.start + dx
);
this.selection.end = this.props.withinRange(
this.selection.end + dx
);
// Optionally, move the player position
movePlayPos && this.props.updatePlayerPos(this.selection.start);
break;
case LEFT:
this.selection.start = this.props.withinRange(
e.shiftKey
? this.selection.start + dx
: this.snapToSegments(
this.selection.start + dx,
this.props.segment.target.layerId
)
);
// Prevent negative selection duration
if (this.selection.start > this.selection.end) {
const end = this.selection.end;
this.selection.end = this.selection.start;
this.selection.start = end;
this.direction = RIGHT;
this.setState({ dragging: this.direction });
}
// Optionally, move the player position
movePlayPos && this.props.updatePlayerPos(this.selection.start);
break;
case RIGHT:
this.selection.end = this.props.withinRange(
e.shiftKey
? this.selection.end + dx
: this.snapToSegments(
this.selection.end + dx,
this.props.segment.target.layerId
)
);
// Prevent negative selections duration
if (this.selection.start > this.selection.end) {
const start = this.selection.start;
this.selection.start = this.selection.end;
this.selection.end = start;
this.direction = LEFT;
this.setState({ dragging: this.direction });
}
// Optionally, move the player position
movePlayPos && this.props.updatePlayerPos(this.selection.end);
break;
}
this.lastX = e.pageX;
// Directly set the new values to the current selector object of the segment
// Final values will be saved to the annotation API when the mouse is released
this.props.segment.target.selector.refinedBy.start = this.selection.start;
this.props.segment.target.selector.refinedBy.end = this.selection.end;
// Force an update of the timeline layer
this.props.updateAnnotationLayer(
this.props.segment.target.layerId || 0
);
};
snapToSegments(x, layerId = null) {
return this.props.snapToSegments(
x,
null, // no segments, so snap to all
this.props.segment,
layerId
);
}
render() {
const { annotationClient, segment } = this.props;
return (
<div
ref={this.ref}
className={classNames(
IDUtil.cssClassName("tl-section-edit"),
{
dragging: this.state.dragging,
active:
annotationClient.activeAnnotation &&
annotationClient.activeAnnotation.id == segment.id,
},
this.state.dragging ? "dragging-" + this.state.dragging : ""
)}
onMouseDown={this.startDragSection}
>
<div className="left" onMouseDown={this.startDragLeft} />
{this.props.children}
<div className="right" onMouseDown={this.startDragRight} />
</div>
);
}
}
SectionEdit.propTypes = {
getPixelsPerSecond: PropTypes.func.isRequired,
updateAnnotationLayer: PropTypes.func.isRequired,
updatePlayerPos: PropTypes.func.isRequired,
updateSegment: PropTypes.func.isRequired,
snapToSegments: PropTypes.func.isRequired,
withinRange: PropTypes.func.isRequired,
selectTimelineLayer: PropTypes.func.isRequired,
mediaObject: PropTypes.object.isRequired,
segment: PropTypes.object.isRequired,
annotationClient: PropTypes.object.isRequired,
};
export default SectionEdit;