From b47120856a164e12e9457a782f60fc996cbd200c Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 13 Nov 2024 07:57:11 +0000 Subject: [PATCH] chore: refactor SourceLayerItem component --- .../ui/SegmentTimeline/SourceLayerItem.tsx | 1010 +++++++---------- 1 file changed, 427 insertions(+), 583 deletions(-) diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 94b71a6590..3324581799 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -10,7 +10,6 @@ import { SplitsSourceRenderer } from './Renderers/SplitsSourceRenderer' import { LocalLayerItemRenderer } from './Renderers/LocalLayerItemRenderer' import { DEBUG_MODE } from './SegmentTimelineDebugMode' -import { withTranslation, WithTranslation } from 'react-i18next' import { getElementDocumentOffset, OffsetPosition } from '../../utils/positions' import { unprotectString } from '../../lib/tempLib' import RundownViewEventBus, { @@ -18,11 +17,11 @@ import RundownViewEventBus, { HighlightEvent, } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { pieceUiClassNames } from '../../lib/ui/pieceUiClassNames' -import { SourceDurationLabelAlignment } from './Renderers/CustomLayerItemRenderer' import { TransitionSourceRenderer } from './Renderers/TransitionSourceRenderer' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { ReadonlyDeep } from 'type-fest' import { PieceContentStatusObj } from '@sofie-automation/meteor-lib/dist/api/pieceContentStatus' +import { useCallback, useRef, useState, useEffect } from 'react' const LEFT_RIGHT_ANCHOR_SPACER = 15 const MARGINAL_ANCHORED_WIDTH = 5 @@ -82,635 +81,480 @@ export interface ISourceLayerItemProps { /** If source duration of piece's content should be displayed next to any labels */ showDuration?: boolean } -interface ISourceLayerItemState { - /** Whether hover-scrub / inspector is shown */ - showMiniInspector: boolean - /** Element position relative to document top-left */ - elementPosition: OffsetPosition - /** Cursor position relative to element */ - cursorPosition: OffsetPosition - /** Cursor position relative to entire viewport */ - cursorRawPosition: { clientX: number; clientY: number } - /** Timecode under cursor */ - cursorTimePosition: number - /** A reference to this element (&self) */ - itemElement: HTMLDivElement | null - /** Width of the child element anchored to the left side of this element */ - leftAnchoredWidth: number - /** Width of the child element anchored to the right side of this element */ - rightAnchoredWidth: number - /** Set to `true` when the segment is "highlighted" (in focus, generally from a scroll event) */ - highlight: boolean -} -export const SourceLayerItem = withTranslation()( - class SourceLayerItem extends React.Component { - animFrameHandle: number | undefined - - constructor(props: ISourceLayerItemProps & WithTranslation) { - super(props) - this.state = { - showMiniInspector: false, - elementPosition: { - top: 0, - left: 0, - }, - cursorPosition: { - top: 0, - left: 0, - }, - cursorRawPosition: { - clientX: 0, - clientY: 0, - }, - cursorTimePosition: 0, - itemElement: null, - leftAnchoredWidth: 0, - rightAnchoredWidth: 0, - highlight: false, - } - } - setRef = (e: HTMLDivElement) => { - this.setState({ - itemElement: e, - }) - } - - convertTimeToPixels = (time: number) => { - return Math.round(this.props.timeScale * time) - } +export const SourceLayerItem = (props: Readonly): JSX.Element => { + const { + layer, + part, + partStartsAt, + partDuration, + piece, + contentStatus, + timeScale, + isLiveLine, + isTooSmallForText, + onClick, + onDoubleClick, + followLiveLine, + liveLineHistorySize, + scrollLeft, + scrollWidth, + studio, + } = props + + const [highlight, setHighlight] = useState(false) + const [showMiniInspector, setShowMiniInspector] = useState(false) + const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 }) + const [cursorPosition, setCursorPosition] = useState({ top: 0, left: 0 }) + const [cursorTimePosition, setCursorTimePosition] = useState(0) + const [leftAnchoredWidth, setLeftAnchoredWidth] = useState(0) + const [rightAnchoredWidth, setRightAnchoredWidth] = useState(0) + + const state = { + highlight, + showMiniInspector, + elementPosition, + cursorPosition, + cursorTimePosition, + leftAnchoredWidth, + rightAnchoredWidth, + } - private getSourceDurationLabelAlignment = (): SourceDurationLabelAlignment => { - if (this.props.part && this.props.partStartsAt !== undefined && !this.props.isLiveLine) { - const elementWidth = this.getElementAbsoluteWidth() - return this.state.leftAnchoredWidth + this.state.rightAnchoredWidth > elementWidth - 10 ? 'left' : 'right' + const itemElementRef = useRef(null) + const animFrameHandle = useRef(undefined) + const cursorRawPosition = useRef({ clientX: 0, clientY: 0 }) + const setRef = useCallback((e: HTMLDivElement) => { + itemElementRef.current = e + }, []) + + const highlightTimeout = useRef(undefined) + const onHighlight = useCallback( + (e: HighlightEvent) => { + if (e.partId === part.partId && (e.pieceId === piece.instance.piece._id || e.pieceId === piece.instance._id)) { + setHighlight(true) + clearTimeout(highlightTimeout.current) + highlightTimeout.current = setTimeout(() => { + setHighlight(false) + }, 5000) } - return 'right' + }, + [part, piece] + ) + useEffect(() => { + RundownViewEventBus.on(RundownViewEvents.HIGHLIGHT, onHighlight) + return () => { + RundownViewEventBus.off(RundownViewEvents.HIGHLIGHT, onHighlight) + clearTimeout(highlightTimeout.current) } + }, []) - getItemLabelOffsetLeft = (): React.CSSProperties => { - const maxLabelWidth = this.props.piece.maxLabelWidth - - if (this.props.part && this.props.partStartsAt !== undefined) { - // && this.props.piece.renderedInPoint !== undefined && this.props.piece.renderedDuration !== undefined - const piece = this.props.piece - - const inPoint = piece.renderedInPoint || 0 - const duration = Number.isFinite(piece.renderedDuration || 0) - ? piece.renderedDuration || this.props.partDuration || this.props.part.renderedDuration || 0 - : this.props.partDuration || this.props.part.renderedDuration || 0 - - const elementWidth = this.getElementAbsoluteWidth() - - const widthConstrictedMode = - this.props.isTooSmallForText || - (this.state.leftAnchoredWidth > 0 && - this.state.rightAnchoredWidth > 0 && - this.state.leftAnchoredWidth + this.state.rightAnchoredWidth > elementWidth) - - const nextIsTouching = !!piece.cropped - - if (this.props.followLiveLine && this.props.isLiveLine) { - const liveLineHistoryWithMargin = this.props.liveLineHistorySize - 10 - if ( - this.props.scrollLeft + liveLineHistoryWithMargin / this.props.timeScale > - inPoint + this.props.partStartsAt + this.state.leftAnchoredWidth / this.props.timeScale && - this.props.scrollLeft + liveLineHistoryWithMargin / this.props.timeScale < - inPoint + duration + this.props.partStartsAt - ) { - const targetPos = this.convertTimeToPixels(this.props.scrollLeft - inPoint - this.props.partStartsAt) - - return { - maxWidth: - this.state.rightAnchoredWidth > 0 - ? (elementWidth - this.state.rightAnchoredWidth).toString() + 'px' - : maxLabelWidth !== undefined - ? this.convertTimeToPixels(maxLabelWidth).toString() + 'px' - : nextIsTouching - ? '100%' - : 'none', - transform: - 'translate(' + - (widthConstrictedMode - ? targetPos - : Math.min(targetPos, elementWidth - this.state.rightAnchoredWidth - liveLineHistoryWithMargin - 10) - ).toString() + - 'px, 0) ' + - 'translate(' + - liveLineHistoryWithMargin.toString() + - 'px, 0) ' + - 'translate(-100%, 0)', - willChange: 'transform', - } - } else if ( - this.state.rightAnchoredWidth < elementWidth && - this.state.leftAnchoredWidth < elementWidth && - this.props.scrollLeft + liveLineHistoryWithMargin / this.props.timeScale >= - inPoint + duration + this.props.partStartsAt - ) { - const targetPos = this.convertTimeToPixels(this.props.scrollLeft - inPoint - this.props.partStartsAt) - - return { - maxWidth: - this.state.rightAnchoredWidth > 0 - ? (elementWidth - this.state.rightAnchoredWidth).toString() + 'px' - : maxLabelWidth !== undefined - ? this.convertTimeToPixels(maxLabelWidth).toString() + 'px' - : nextIsTouching - ? '100%' - : 'none', - transform: - 'translate(' + - Math.min( - targetPos, - elementWidth - this.state.rightAnchoredWidth - liveLineHistoryWithMargin - 10 - ).toString() + - 'px, 0) ' + - 'translate(' + - liveLineHistoryWithMargin.toString() + - 'px, 0) ' + - 'translate3d(-100%, 0)', - willChange: 'transform', - } - } else { - return { - maxWidth: - this.state.rightAnchoredWidth > 0 - ? (elementWidth - this.state.rightAnchoredWidth - 10).toString() + 'px' - : maxLabelWidth !== undefined - ? this.convertTimeToPixels(maxLabelWidth).toString() + 'px' - : nextIsTouching - ? '100%' - : 'none', - } - } - } else { - if ( - this.props.scrollLeft > inPoint + this.props.partStartsAt && - this.props.scrollLeft < inPoint + duration + this.props.partStartsAt - ) { - const targetPos = this.convertTimeToPixels(this.props.scrollLeft - inPoint - this.props.partStartsAt) - - return { - maxWidth: - this.state.rightAnchoredWidth > 0 - ? (elementWidth - this.state.rightAnchoredWidth - 10).toString() + 'px' - : maxLabelWidth !== undefined - ? this.convertTimeToPixels(maxLabelWidth).toString() + 'px' - : nextIsTouching - ? '100%' - : 'none', - transform: - 'translate(' + - (widthConstrictedMode || this.state.leftAnchoredWidth === 0 || this.state.rightAnchoredWidth === 0 - ? targetPos - : Math.min(targetPos, elementWidth - this.state.leftAnchoredWidth - this.state.rightAnchoredWidth) - ).toString() + - 'px, 0)', - } - } else { - return { - maxWidth: - this.state.rightAnchoredWidth > 0 - ? (elementWidth - this.state.rightAnchoredWidth - 10).toString() + 'px' - : maxLabelWidth !== undefined - ? this.convertTimeToPixels(maxLabelWidth).toString() + 'px' - : nextIsTouching - ? '100%' - : 'none', - } - } - } + const itemClick = useCallback( + (e: React.MouseEvent) => { + // this.props.onFollowLiveLine && this.props.onFollowLiveLine(false, e) + e.preventDefault() + e.stopPropagation() + onClick && onClick(piece, e) + }, + [piece] + ) + const itemDblClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + if (typeof onDoubleClick === 'function') { + onDoubleClick(piece, e) } - return {} + }, + [piece] + ) + const itemMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + const itemMouseUp = useCallback((e: any) => { + const eM = e as MouseEvent + if (eM.ctrlKey === true) { + eM.preventDefault() + eM.stopPropagation() + } + return + }, []) + const toggleMiniInspectorOn = useCallback((e: React.MouseEvent) => toggleMiniInspector(e, true), []) + const toggleMiniInspectorOff = useCallback((e: React.MouseEvent) => toggleMiniInspector(e, false), []) + const updatePos = useCallback(() => { + const elementPos = getElementDocumentOffset(itemElementRef.current) || { + top: 0, + left: 0, + } + const cursorPosition = { + left: cursorRawPosition.current.clientX - elementPos.left, + top: cursorRawPosition.current.clientY - elementPos.top, } - getItemLabelOffsetRight = (): React.CSSProperties => { - if (!this.props.part || this.props.partStartsAt === undefined) return {} + const cursorTimePosition = Math.max(cursorPosition.left, 0) / timeScale - const piece = this.props.piece - const innerPiece = piece.instance.piece + setElementPosition(elementPos) + setCursorPosition(cursorPosition) + setCursorTimePosition(cursorTimePosition) - const inPoint = piece.renderedInPoint || 0 - const duration = - innerPiece.lifespan !== PieceLifespan.WithinPart || piece.renderedDuration === 0 - ? this.props.partDuration - inPoint - : Math.min(piece.renderedDuration || 0, this.props.partDuration - inPoint) - const outPoint = inPoint + duration - - const elementWidth = this.getElementAbsoluteWidth() - - // const widthConstrictedMode = this.state.leftAnchoredWidth > 0 && this.state.rightAnchoredWidth > 0 && ((this.state.leftAnchoredWidth + this.state.rightAnchoredWidth) > this.state.elementWidth) - - if ( - this.props.scrollLeft + this.props.scrollWidth < outPoint + this.props.partStartsAt && - this.props.scrollLeft + this.props.scrollWidth > inPoint + this.props.partStartsAt - ) { - const targetPos = Math.max( - (this.props.scrollLeft + this.props.scrollWidth - outPoint - this.props.partStartsAt) * this.props.timeScale, - (elementWidth - this.state.leftAnchoredWidth - this.state.rightAnchoredWidth - LEFT_RIGHT_ANCHOR_SPACER) * -1 - ) - - return { - transform: 'translate(' + targetPos.toString() + 'px, 0)', - } - } - return {} + animFrameHandle.current = requestAnimationFrame(updatePos) + }, []) + const toggleMiniInspector = useCallback((e: MouseEvent | any, v: boolean) => { + setShowMiniInspector(v) + cursorRawPosition.current = { + clientX: e.clientX, + clientY: e.clientY, } - getItemDuration = (returnInfinite?: boolean): number => { - const piece = this.props.piece - const innerPiece = piece.instance.piece - - const expectedDurationNumber = - typeof innerPiece.enable.duration === 'number' ? innerPiece.enable.duration || 0 : 0 - - let itemDuration: number - if (!returnInfinite) { - itemDuration = Math.min( - piece.renderedDuration || expectedDurationNumber || 0, - this.props.partDuration - (piece.renderedInPoint || 0) - ) - } else { - itemDuration = - this.props.partDuration - (piece.renderedInPoint || 0) < - (piece.renderedDuration || expectedDurationNumber || 0) - ? Number.POSITIVE_INFINITY - : piece.renderedDuration || expectedDurationNumber || 0 - } + if (v) { + animFrameHandle.current = requestAnimationFrame(updatePos) + } else if (animFrameHandle.current !== undefined) { + cancelAnimationFrame(animFrameHandle.current) + } + }, []) + const moveMiniInspector = useCallback((e: MouseEvent | any) => { + cursorRawPosition.current = { + clientX: e.clientX, + clientY: e.clientY, + } + }, []) - if ( - (innerPiece.lifespan !== PieceLifespan.WithinPart || - (innerPiece.enable.start !== undefined && - innerPiece.enable.duration === undefined && - piece.instance.userDuration === undefined)) && - !piece.cropped && - piece.renderedDuration === null && - piece.instance.userDuration === undefined - ) { - if (!returnInfinite) { - itemDuration = this.props.partDuration - (piece.renderedInPoint || 0) - } else { - itemDuration = Number.POSITIVE_INFINITY - } - } + const convertTimeToPixels = (time: number) => { + return Math.round(timeScale * time) + } + const getItemDuration = (returnInfinite?: boolean): number => { + const innerPiece = piece.instance.piece - return itemDuration - } + const expectedDurationNumber = typeof innerPiece.enable.duration === 'number' ? innerPiece.enable.duration || 0 : 0 - getElementAbsoluteWidth(): number { - const itemDuration = this.getItemDuration() - return this.convertTimeToPixels(itemDuration) + let itemDuration: number + if (!returnInfinite) { + itemDuration = Math.min( + piece.renderedDuration || expectedDurationNumber || 0, + partDuration - (piece.renderedInPoint || 0) + ) + } else { + itemDuration = + partDuration - (piece.renderedInPoint || 0) < (piece.renderedDuration || expectedDurationNumber || 0) + ? Number.POSITIVE_INFINITY + : piece.renderedDuration || expectedDurationNumber || 0 } - getElementAbsoluteStyleWidth(): string { - const renderedInPoint = this.props.piece.renderedInPoint - if (renderedInPoint === 0) { - const itemPossiblyInfiniteDuration = this.getItemDuration(true) - if (!Number.isFinite(itemPossiblyInfiniteDuration)) { - return '100%' - } + if ( + (innerPiece.lifespan !== PieceLifespan.WithinPart || + (innerPiece.enable.start !== undefined && + innerPiece.enable.duration === undefined && + piece.instance.userDuration === undefined)) && + !piece.cropped && + piece.renderedDuration === null && + piece.instance.userDuration === undefined + ) { + if (!returnInfinite) { + itemDuration = partDuration - (piece.renderedInPoint || 0) + } else { + itemDuration = Number.POSITIVE_INFINITY } - const itemDuration = this.getItemDuration(false) - return this.convertTimeToPixels(itemDuration).toString() + 'px' } - getItemStyle(): { [key: string]: string } { - const piece = this.props.piece - const innerPiece = piece.instance.piece + return itemDuration + } + const getElementAbsoluteWidth = (): number => { + const itemDuration = getItemDuration() + return convertTimeToPixels(itemDuration) + } - // If this is a live line, take duration verbatim from SegmentLayerItemContainer with a fallback on expectedDuration. - // If not, as-run part "duration" limits renderdDuration which takes priority over MOS-import - // expectedDuration (editorial duration) + const isInsideViewport = RundownUtils.isInsideViewport( + scrollLeft, + scrollWidth, + part, + partStartsAt, + partDuration, + piece + ) + const getItemStyle = (): { [key: string]: string } => { + const innerPiece = piece.instance.piece - // let liveLinePadding = this.props.autoNextPart ? 0 : (this.props.isLiveLine ? this.props.liveLinePadding : 0) + // If this is a live line, take duration verbatim from SegmentLayerItemContainer with a fallback on expectedDuration. + // If not, as-run part "duration" limits renderdDuration which takes priority over MOS-import + // expectedDuration (editorial duration) - if (innerPiece.pieceType === IBlueprintPieceType.OutTransition) { - return { - left: 'auto', - right: '0', - width: this.getElementAbsoluteWidth().toString() + 'px', - } - } + // let liveLinePadding = this.props.autoNextPart ? 0 : (this.props.isLiveLine ? this.props.liveLinePadding : 0) + + if (innerPiece.pieceType === IBlueprintPieceType.OutTransition) { return { - left: this.convertTimeToPixels(piece.renderedInPoint || 0).toString() + 'px', - width: this.getElementAbsoluteStyleWidth(), + left: 'auto', + right: '0', + width: getElementAbsoluteWidth().toString() + 'px', } } - - // TODO(Performance): use ResizeObserver to avoid style recalculations - // checkElementWidth = () => { - // if (this.state.itemElement && this._forceSizingRecheck) { - // this._forceSizingRecheck = false - // const width = getElementWidth(this.state.itemElement) || 0 - // if (this.state.elementWidth !== width) { - // this.setState({ - // elementWidth: width - // }) - // } - // } - // } - - private highlightTimeout: NodeJS.Timeout | undefined - - private onHighlight = (e: HighlightEvent) => { - if ( - e.partId === this.props.part.partId && - (e.pieceId === this.props.piece.instance.piece._id || e.pieceId === this.props.piece.instance._id) - ) { - this.setState({ - highlight: true, - }) - clearTimeout(this.highlightTimeout) - this.highlightTimeout = setTimeout(() => { - this.setState({ - highlight: false, - }) - }, 5000) + return { + left: convertTimeToPixels(piece.renderedInPoint || 0).toString() + 'px', + width: getElementAbsoluteStyleWidth(), + } + } + const getElementAbsoluteStyleWidth = (): string => { + const renderedInPoint = piece.renderedInPoint + if (renderedInPoint === 0) { + const itemPossiblyInfiniteDuration = getItemDuration(true) + if (!Number.isFinite(itemPossiblyInfiniteDuration)) { + return '100%' } } + const itemDuration = getItemDuration(false) + return convertTimeToPixels(itemDuration).toString() + 'px' + } - componentDidMount(): void { - RundownViewEventBus.on(RundownViewEvents.HIGHLIGHT, this.onHighlight) - } + const getItemLabelOffsetLeft = (): React.CSSProperties => { + const maxLabelWidth = piece.maxLabelWidth - componentWillUnmount(): void { - super.componentWillUnmount && super.componentWillUnmount() - RundownViewEventBus.off(RundownViewEvents.HIGHLIGHT, this.onHighlight) - clearTimeout(this.highlightTimeout) - } + if (part && partStartsAt !== undefined) { + // && this.props.piece.renderedInPoint !== undefined && this.props.piece.renderedDuration !== undefined - componentDidUpdate(prevProps: ISourceLayerItemProps, _prevState: ISourceLayerItemState) { - if (this.state.showMiniInspector) { - if (prevProps.scrollLeft !== this.props.scrollLeft) { - const cursorPosition = { - left: this.state.cursorRawPosition.clientX - this.state.elementPosition.left, - top: this.state.cursorRawPosition.clientY - this.state.elementPosition.top, + const inPoint = piece.renderedInPoint || 0 + const duration = Number.isFinite(piece.renderedDuration || 0) + ? piece.renderedDuration || partDuration || part.renderedDuration || 0 + : partDuration || part.renderedDuration || 0 + + const elementWidth = getElementAbsoluteWidth() + + const widthConstrictedMode = + isTooSmallForText || + (leftAnchoredWidth > 0 && rightAnchoredWidth > 0 && leftAnchoredWidth + rightAnchoredWidth > elementWidth) + + const nextIsTouching = !!piece.cropped + + if (followLiveLine && isLiveLine) { + const liveLineHistoryWithMargin = liveLineHistorySize - 10 + if ( + scrollLeft + liveLineHistoryWithMargin / timeScale > inPoint + partStartsAt + leftAnchoredWidth / timeScale && + scrollLeft + liveLineHistoryWithMargin / timeScale < inPoint + duration + partStartsAt + ) { + const targetPos = convertTimeToPixels(scrollLeft - inPoint - partStartsAt) + + return { + maxWidth: + rightAnchoredWidth > 0 + ? (elementWidth - rightAnchoredWidth).toString() + 'px' + : maxLabelWidth !== undefined + ? convertTimeToPixels(maxLabelWidth).toString() + 'px' + : nextIsTouching + ? '100%' + : 'none', + transform: + 'translate(' + + (widthConstrictedMode + ? targetPos + : Math.min(targetPos, elementWidth - rightAnchoredWidth - liveLineHistoryWithMargin - 10) + ).toString() + + 'px, 0) ' + + 'translate(' + + liveLineHistoryWithMargin.toString() + + 'px, 0) ' + + 'translate(-100%, 0)', + willChange: 'transform', + } + } else if ( + rightAnchoredWidth < elementWidth && + leftAnchoredWidth < elementWidth && + scrollLeft + liveLineHistoryWithMargin / timeScale >= inPoint + duration + partStartsAt + ) { + const targetPos = convertTimeToPixels(scrollLeft - inPoint - partStartsAt) + + return { + maxWidth: + rightAnchoredWidth > 0 + ? (elementWidth - rightAnchoredWidth).toString() + 'px' + : maxLabelWidth !== undefined + ? convertTimeToPixels(maxLabelWidth).toString() + 'px' + : nextIsTouching + ? '100%' + : 'none', + transform: + 'translate(' + + Math.min(targetPos, elementWidth - rightAnchoredWidth - liveLineHistoryWithMargin - 10).toString() + + 'px, 0) ' + + 'translate(' + + liveLineHistoryWithMargin.toString() + + 'px, 0) ' + + 'translate3d(-100%, 0)', + willChange: 'transform', + } + } else { + return { + maxWidth: + rightAnchoredWidth > 0 + ? (elementWidth - rightAnchoredWidth - 10).toString() + 'px' + : maxLabelWidth !== undefined + ? convertTimeToPixels(maxLabelWidth).toString() + 'px' + : nextIsTouching + ? '100%' + : 'none', + } + } + } else { + if (scrollLeft > inPoint + partStartsAt && scrollLeft < inPoint + duration + partStartsAt) { + const targetPos = convertTimeToPixels(scrollLeft - inPoint - partStartsAt) + + return { + maxWidth: + rightAnchoredWidth > 0 + ? (elementWidth - rightAnchoredWidth - 10).toString() + 'px' + : maxLabelWidth !== undefined + ? convertTimeToPixels(maxLabelWidth).toString() + 'px' + : nextIsTouching + ? '100%' + : 'none', + transform: + 'translate(' + + (widthConstrictedMode || leftAnchoredWidth === 0 || rightAnchoredWidth === 0 + ? targetPos + : Math.min(targetPos, elementWidth - leftAnchoredWidth - rightAnchoredWidth) + ).toString() + + 'px, 0)', } - const cursorTimePosition = Math.max(cursorPosition.left, 0) / this.props.timeScale - if (this.state.cursorTimePosition !== cursorTimePosition) { - this.setState({ - cursorTimePosition, - }) + } else { + return { + maxWidth: + rightAnchoredWidth > 0 + ? (elementWidth - rightAnchoredWidth - 10).toString() + 'px' + : maxLabelWidth !== undefined + ? convertTimeToPixels(maxLabelWidth).toString() + 'px' + : nextIsTouching + ? '100%' + : 'none', } } } } + return {} + } + const getItemLabelOffsetRight = (): React.CSSProperties => { + if (!part || partStartsAt === undefined) return {} - itemClick = (e: React.MouseEvent) => { - // this.props.onFollowLiveLine && this.props.onFollowLiveLine(false, e) - e.preventDefault() - e.stopPropagation() - this.props.onClick && this.props.onClick(this.props.piece, e) - } - - itemDblClick = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + const innerPiece = piece.instance.piece - if (typeof this.props.onDoubleClick === 'function') { - this.props.onDoubleClick(this.props.piece, e) - } - } + const inPoint = piece.renderedInPoint || 0 + const duration = + innerPiece.lifespan !== PieceLifespan.WithinPart || piece.renderedDuration === 0 + ? partDuration - inPoint + : Math.min(piece.renderedDuration || 0, partDuration - inPoint) + const outPoint = inPoint + duration - itemMouseUp = (e: any) => { - const eM = e as MouseEvent - if (eM.ctrlKey === true) { - eM.preventDefault() - eM.stopPropagation() - } - return - } + const elementWidth = getElementAbsoluteWidth() - toggleMiniInspectorOn = (e: React.MouseEvent) => this.toggleMiniInspector(e, true) - toggleMiniInspectorOff = (e: React.MouseEvent) => this.toggleMiniInspector(e, false) - - toggleMiniInspector = (e: MouseEvent | any, v: boolean) => { - this.setState({ - showMiniInspector: v, - cursorRawPosition: { - clientX: e.clientX, - clientY: e.clientY, - }, - }) - - if (v) { - const updatePos = () => { - const elementPos = getElementDocumentOffset(this.state.itemElement) || { - top: 0, - left: 0, - } - const cursorPosition = { - left: this.state.cursorRawPosition.clientX - elementPos.left, - top: this.state.cursorRawPosition.clientY - elementPos.top, - } + // const widthConstrictedMode = this.state.leftAnchoredWidth > 0 && this.state.rightAnchoredWidth > 0 && ((this.state.leftAnchoredWidth + this.state.rightAnchoredWidth) > this.state.elementWidth) - const cursorTimePosition = Math.max(cursorPosition.left, 0) / this.props.timeScale + if (scrollLeft + scrollWidth < outPoint + partStartsAt && scrollLeft + scrollWidth > inPoint + partStartsAt) { + const targetPos = Math.max( + (scrollLeft + scrollWidth - outPoint - partStartsAt) * timeScale, + (elementWidth - leftAnchoredWidth - rightAnchoredWidth - LEFT_RIGHT_ANCHOR_SPACER) * -1 + ) - this.setState({ - elementPosition: elementPos, - cursorPosition, - cursorTimePosition, - }) - this.animFrameHandle = requestAnimationFrame(updatePos) - } - this.animFrameHandle = requestAnimationFrame(updatePos) - } else if (this.animFrameHandle !== undefined) { - cancelAnimationFrame(this.animFrameHandle) + return { + transform: 'translate(' + targetPos.toString() + 'px, 0)', } } + return {} + } + const setAnchoredElsWidths = (leftAnchoredWidth: number, rightAnchoredWidth: number) => { + // anchored labels will sometimes errorneously report some width. Discard if it's marginal. + setLeftAnchoredWidth(leftAnchoredWidth > MARGINAL_ANCHORED_WIDTH ? leftAnchoredWidth : 0) + setRightAnchoredWidth(rightAnchoredWidth > MARGINAL_ANCHORED_WIDTH ? rightAnchoredWidth : 0) + } - moveMiniInspector = (e: MouseEvent | any) => { - this.setState({ - cursorRawPosition: { - clientX: e.clientX, - clientY: e.clientY, - }, - }) - } - - setAnchoredElsWidths = (leftAnchoredWidth: number, rightAnchoredWidth: number) => { - // anchored labels will sometimes errorneously report some width. Discard if it's marginal. - this.setState({ - leftAnchoredWidth: leftAnchoredWidth > MARGINAL_ANCHORED_WIDTH ? leftAnchoredWidth : 0, - rightAnchoredWidth: rightAnchoredWidth > MARGINAL_ANCHORED_WIDTH ? rightAnchoredWidth : 0, - }) - } - - renderInsideItem(typeClass: string) { - switch (this.props.layer.type) { - case SourceLayerType.SCRIPT: - // case SourceLayerType.MIC: - return ( - - ) - case SourceLayerType.VT: - case SourceLayerType.LIVE_SPEAK: - return ( - - ) - case SourceLayerType.GRAPHICS: - case SourceLayerType.LOWER_THIRD: - case SourceLayerType.STUDIO_SCREEN: - return ( - - ) - case SourceLayerType.SPLITS: - return ( - - ) - - case SourceLayerType.TRANSITION: - // TODOSYNC: TV2 uses other renderers, to be discussed. - - return ( - - ) - case SourceLayerType.LOCAL: - return ( - - ) - default: - return ( - - ) - } + const renderInsideItem = (typeClass: string) => { + const elProps = { + key: unprotectString(piece.instance._id), + typeClass: typeClass, + getItemDuration: getItemDuration, + getItemLabelOffsetLeft: getItemLabelOffsetLeft, + getItemLabelOffsetRight: getItemLabelOffsetRight, + setAnchoredElsWidths: setAnchoredElsWidths, + itemElement: itemElementRef.current, + ...props, + ...state, } - isInsideViewport() { - return RundownUtils.isInsideViewport( - this.props.scrollLeft, - this.props.scrollWidth, - this.props.part, - this.props.partStartsAt, - this.props.partDuration, - this.props.piece - ) + switch (layer.type) { + case SourceLayerType.SCRIPT: + // case SourceLayerType.MIC: + return + case SourceLayerType.VT: + case SourceLayerType.LIVE_SPEAK: + return + case SourceLayerType.GRAPHICS: + case SourceLayerType.LOWER_THIRD: + case SourceLayerType.STUDIO_SCREEN: + return + case SourceLayerType.SPLITS: + return + + case SourceLayerType.TRANSITION: + // TODOSYNC: TV2 uses other renderers, to be discussed. + + return + case SourceLayerType.LOCAL: + return + default: + return } + } - render(): JSX.Element { - if (this.isInsideViewport()) { - const typeClass = RundownUtils.getSourceLayerClassName(this.props.layer.type) - - const piece = this.props.piece - const innerPiece = piece.instance.piece - - const elementWidth = this.getElementAbsoluteWidth() - - return ( -
- {this.renderInsideItem(typeClass)} - {DEBUG_MODE && this.props.studio && ( -
- {innerPiece.enable.start} /{' '} - {RundownUtils.formatTimeToTimecode(this.props.studio.settings, this.props.partDuration).substr(-5)} /{' '} - {piece.renderedDuration - ? RundownUtils.formatTimeToTimecode(this.props.studio.settings, piece.renderedDuration).substr(-5) - : 'X'}{' '} - /{' '} - {typeof innerPiece.enable.duration === 'number' - ? RundownUtils.formatTimeToTimecode(this.props.studio.settings, innerPiece.enable.duration).substr(-5) - : ''} -
- )} + if (isInsideViewport) { + const typeClass = RundownUtils.getSourceLayerClassName(layer.type) + + const innerPiece = piece.instance.piece + + const elementWidth = getElementAbsoluteWidth() + + return ( +
+ {renderInsideItem(typeClass)} + {DEBUG_MODE && studio && ( +
+ {innerPiece.enable.start} / {RundownUtils.formatTimeToTimecode(studio.settings, partDuration).substr(-5)} /{' '} + {piece.renderedDuration + ? RundownUtils.formatTimeToTimecode(studio.settings, piece.renderedDuration).substr(-5) + : 'X'}{' '} + /{' '} + {typeof innerPiece.enable.duration === 'number' + ? RundownUtils.formatTimeToTimecode(studio.settings, innerPiece.enable.duration).substr(-5) + : ''}
- ) - } else { - // render a placeholder - return ( -
- ) - } - } + )} +
+ ) + } else { + // render a placeholder + return ( +
+ ) } -) +}