| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.TimelineRuler = class TimelineRuler extends WI.View |
| { |
| constructor() |
| { |
| super(); |
| |
| this.element.classList.add("timeline-ruler"); |
| |
| this._headerElement = document.createElement("div"); |
| this._headerElement.classList.add("header"); |
| this.element.appendChild(this._headerElement); |
| |
| this._markersElement = document.createElement("div"); |
| this._markersElement.classList.add("markers"); |
| this.element.appendChild(this._markersElement); |
| |
| this._zeroTime = 0; |
| this._startTime = 0; |
| this._endTime = 0; |
| this._duration = NaN; |
| this._secondsPerPixel = 0; |
| this._selectionStartTime = 0; |
| this._selectionEndTime = Number.MAX_VALUE; |
| this._endTimePinned = false; |
| this._snapInterval = 0; |
| this._allowsClippedLabels = false; |
| this._allowsTimeRangeSelection = false; |
| this._minimumSelectionDuration = 0.01; |
| this._formatLabelCallback = null; |
| this._timeRangeSelectionChanged = false; |
| this._enabled = true; |
| |
| this._scannerMarker = null; |
| this._markerElementMap = new Map; |
| this._cachedClientWidth = 0; |
| } |
| |
| // Public |
| |
| get enabled() |
| { |
| return this._enabled; |
| } |
| |
| set enabled(x) |
| { |
| if (this._enabled === x) |
| return; |
| |
| this._enabled = x; |
| this.element.classList.toggle(WI.TreeElementStatusButton.DisabledStyleClassName, !this._enabled); |
| } |
| |
| get allowsClippedLabels() |
| { |
| return this._allowsClippedLabels; |
| } |
| |
| set allowsClippedLabels(x) |
| { |
| x = !!x; |
| |
| if (this._allowsClippedLabels === x) |
| return; |
| |
| this._allowsClippedLabels = x; |
| |
| this.needsLayout(); |
| } |
| |
| set formatLabelCallback(x) |
| { |
| console.assert(typeof x === "function" || !x, x); |
| |
| x = x || null; |
| |
| if (this._formatLabelCallback === x) |
| return; |
| |
| this._formatLabelCallback = x; |
| |
| this.needsLayout(); |
| } |
| |
| get allowsTimeRangeSelection() |
| { |
| return this._allowsTimeRangeSelection; |
| } |
| |
| set allowsTimeRangeSelection(x) |
| { |
| x = !!x; |
| |
| if (this._allowsTimeRangeSelection === x) |
| return; |
| |
| this._allowsTimeRangeSelection = x; |
| |
| if (x) { |
| this._clickEventListener = this._handleClick.bind(this); |
| this._doubleClickEventListener = this._handleDoubleClick.bind(this); |
| this._mouseDownEventListener = this._handleMouseDown.bind(this); |
| this.element.addEventListener("click", this._clickEventListener); |
| this.element.addEventListener("dblclick", this._doubleClickEventListener); |
| this.element.addEventListener("mousedown", this._mouseDownEventListener); |
| |
| this._leftShadedAreaElement = document.createElement("div"); |
| this._leftShadedAreaElement.classList.add("shaded-area"); |
| this._leftShadedAreaElement.classList.add("left"); |
| |
| this._rightShadedAreaElement = document.createElement("div"); |
| this._rightShadedAreaElement.classList.add("shaded-area"); |
| this._rightShadedAreaElement.classList.add("right"); |
| |
| this._leftSelectionHandleElement = document.createElement("div"); |
| this._leftSelectionHandleElement.classList.add("selection-handle"); |
| this._leftSelectionHandleElement.classList.add("left"); |
| this._leftSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this)); |
| |
| this._rightSelectionHandleElement = document.createElement("div"); |
| this._rightSelectionHandleElement.classList.add("selection-handle"); |
| this._rightSelectionHandleElement.classList.add("right"); |
| this._rightSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this)); |
| |
| this._selectionDragElement = document.createElement("div"); |
| this._selectionDragElement.classList.add("selection-drag"); |
| |
| this._needsSelectionLayout(); |
| } else { |
| this.element.removeEventListener("click", this._clickEventListener); |
| this.element.removeEventListener("dblclick", this._doubleClickEventListener); |
| this.element.removeEventListener("mousedown", this._mouseDownEventListener); |
| this._clickEventListener = null; |
| this._doubleClickEventListener = null; |
| this._mouseDownEventListener = null; |
| |
| this._leftShadedAreaElement.remove(); |
| this._rightShadedAreaElement.remove(); |
| this._leftSelectionHandleElement.remove(); |
| this._rightSelectionHandleElement.remove(); |
| this._selectionDragElement.remove(); |
| |
| delete this._leftShadedAreaElement; |
| delete this._rightShadedAreaElement; |
| delete this._leftSelectionHandleElement; |
| delete this._rightSelectionHandleElement; |
| delete this._selectionDragElement; |
| } |
| } |
| |
| get minimumSelectionDuration() |
| { |
| return this._minimumSelectionDuration; |
| } |
| |
| set minimumSelectionDuration(x) |
| { |
| this._minimumSelectionDuration = x; |
| } |
| |
| get zeroTime() |
| { |
| return this._zeroTime; |
| } |
| |
| set zeroTime(x) |
| { |
| x = x || 0; |
| |
| if (this._zeroTime === x) |
| return; |
| |
| if (this.entireRangeSelected) |
| this.selectionStartTime = x; |
| |
| this._zeroTime = x; |
| |
| this.needsLayout(); |
| } |
| |
| get startTime() |
| { |
| return this._startTime; |
| } |
| |
| set startTime(x) |
| { |
| x = x || 0; |
| |
| if (this._startTime === x) |
| return; |
| |
| this._startTime = x; |
| |
| if (!isNaN(this._duration)) |
| this._endTime = this._startTime + this._duration; |
| |
| this._currentDividers = null; |
| |
| this.needsLayout(); |
| } |
| |
| get duration() |
| { |
| if (!isNaN(this._duration)) |
| return this._duration; |
| return this.endTime - this.startTime; |
| } |
| |
| get endTime() |
| { |
| if (!this._endTimePinned && this.layoutPending) |
| this._recalculate(); |
| return this._endTime; |
| } |
| |
| set endTime(x) |
| { |
| x = x || 0; |
| |
| if (this._endTime === x) |
| return; |
| |
| this._endTime = x; |
| this._endTimePinned = true; |
| |
| this.needsLayout(); |
| } |
| |
| get secondsPerPixel() |
| { |
| if (this.layoutPending) |
| this._recalculate(); |
| return this._secondsPerPixel; |
| } |
| |
| set secondsPerPixel(x) |
| { |
| x = x || 0; |
| |
| if (this._secondsPerPixel === x) |
| return; |
| |
| this._secondsPerPixel = x; |
| this._endTimePinned = false; |
| this._currentDividers = null; |
| this._currentSliceTime = 0; |
| |
| this.needsLayout(); |
| } |
| |
| get snapInterval() |
| { |
| return this._snapInterval; |
| } |
| |
| set snapInterval(x) |
| { |
| if (this._snapInterval === x) |
| return; |
| |
| this._snapInterval = x; |
| } |
| |
| get selectionStartTime() |
| { |
| return this._selectionStartTime; |
| } |
| |
| set selectionStartTime(x) |
| { |
| x = this._snapValue(x) || 0; |
| |
| if (this._selectionStartTime === x) |
| return; |
| |
| this._selectionStartTime = x; |
| this._timeRangeSelectionChanged = true; |
| |
| this._needsSelectionLayout(); |
| } |
| |
| get selectionEndTime() |
| { |
| return this._selectionEndTime; |
| } |
| |
| set selectionEndTime(x) |
| { |
| x = this._snapValue(x) || 0; |
| |
| if (this._selectionEndTime === x) |
| return; |
| |
| this._selectionEndTime = x; |
| this._timeRangeSelectionChanged = true; |
| |
| this._needsSelectionLayout(); |
| } |
| |
| get entireRangeSelected() |
| { |
| return this._selectionStartTime === this._zeroTime && this._selectionEndTime === Number.MAX_VALUE; |
| } |
| |
| selectEntireRange() |
| { |
| this.selectionStartTime = this._zeroTime; |
| this.selectionEndTime = Number.MAX_VALUE; |
| } |
| |
| addMarker(marker) |
| { |
| console.assert(marker instanceof WI.TimelineMarker); |
| |
| if (this._markerElementMap.has(marker)) |
| return; |
| |
| marker.addEventListener(WI.TimelineMarker.Event.TimeChanged, this._timelineMarkerTimeChanged, this); |
| |
| let markerTime = marker.time - this._startTime; |
| let markerElement = document.createElement("div"); |
| markerElement.classList.add(marker.type, "marker"); |
| |
| switch (marker.type) { |
| case WI.TimelineMarker.Type.LoadEvent: |
| markerElement.title = WI.UIString("Load \u2014 %s").format(Number.secondsToString(markerTime)); |
| break; |
| case WI.TimelineMarker.Type.DOMContentEvent: |
| markerElement.title = WI.UIString("DOM Content Loaded \u2014 %s").format(Number.secondsToString(markerTime)); |
| break; |
| case WI.TimelineMarker.Type.TimeStamp: |
| if (marker.details) |
| markerElement.title = WI.UIString("%s \u2014 %s").format(marker.details, Number.secondsToString(markerTime)); |
| else |
| markerElement.title = WI.UIString("Timestamp \u2014 %s").format(Number.secondsToString(markerTime)); |
| break; |
| } |
| |
| this._markerElementMap.set(marker, markerElement); |
| |
| this._needsMarkerLayout(); |
| } |
| |
| clearMarkers() |
| { |
| for (let [marker, markerElement] of this._markerElementMap) { |
| marker.removeEventListener(WI.TimelineMarker.Event.TimeChanged, this._timelineMarkerTimeChanged, this); |
| markerElement.remove(); |
| } |
| |
| this._markerElementMap.clear(); |
| |
| this._scannerMarker = null; |
| } |
| |
| elementForMarker(marker) |
| { |
| return this._markerElementMap.get(marker) || null; |
| } |
| |
| showScanner(time) |
| { |
| if (!this._scannerMarker) { |
| this._scannerMarker = new WI.TimelineMarker(time, WI.TimelineMarker.Type.Scanner); |
| this.addMarker(this._scannerMarker); |
| } |
| |
| this._scannerMarker.time = time; |
| } |
| |
| hideScanner() |
| { |
| if (this._scannerMarker) |
| this._scannerMarker.time = -1; |
| } |
| |
| updateLayoutIfNeeded(layoutReason) |
| { |
| // If a layout is pending we can let the base class handle it and return, since that will update |
| // markers and the selection at the same time. |
| if (this.layoutPending) { |
| super.updateLayoutIfNeeded(layoutReason); |
| return; |
| } |
| |
| let visibleWidth = this._recalculate(); |
| if (visibleWidth <= 0) |
| return; |
| |
| if (this._scheduledMarkerLayoutUpdateIdentifier) |
| this._updateMarkers(visibleWidth, this.duration); |
| |
| if (this._scheduledSelectionLayoutUpdateIdentifier) |
| this._updateSelection(visibleWidth, this.duration); |
| } |
| |
| needsLayout(layoutReason) |
| { |
| if (this.layoutPending) |
| return; |
| |
| if (this._scheduledMarkerLayoutUpdateIdentifier) { |
| cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier); |
| this._scheduledMarkerLayoutUpdateIdentifier = undefined; |
| } |
| |
| if (this._scheduledSelectionLayoutUpdateIdentifier) { |
| cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier); |
| this._scheduledSelectionLayoutUpdateIdentifier = undefined; |
| } |
| |
| super.needsLayout(layoutReason); |
| } |
| |
| // Protected |
| |
| layout() |
| { |
| let visibleWidth = this._recalculate(); |
| if (visibleWidth <= 0) |
| return; |
| |
| let duration = this.duration; |
| let pixelsPerSecond = visibleWidth / duration; |
| |
| // Calculate a divider count based on the maximum allowed divider density. |
| let dividerCount = Math.round(visibleWidth / WI.TimelineRuler.MinimumDividerSpacing); |
| let sliceTime; |
| if (this._endTimePinned || !this._currentSliceTime) { |
| // Calculate the slice time based on the rough divider count and the time span. |
| sliceTime = duration / dividerCount; |
| |
| // Snap the slice time to a nearest number (e.g. 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, etc.) |
| sliceTime = Math.pow(10, Math.ceil(Math.log(sliceTime) / Math.LN10)); |
| if (sliceTime * pixelsPerSecond >= 5 * WI.TimelineRuler.MinimumDividerSpacing) |
| sliceTime = sliceTime / 5; |
| if (sliceTime * pixelsPerSecond >= 2 * WI.TimelineRuler.MinimumDividerSpacing) |
| sliceTime = sliceTime / 2; |
| |
| this._currentSliceTime = sliceTime; |
| } else { |
| // Reuse the last slice time since the time duration does not scale to fit when the end time isn't pinned. |
| sliceTime = this._currentSliceTime; |
| } |
| |
| // Calculate the divider count now based on the final slice time. |
| dividerCount = Math.floor(visibleWidth * this.secondsPerPixel / sliceTime); |
| |
| let firstDividerTime = (Math.ceil((this._startTime - this._zeroTime) / sliceTime) * sliceTime) + this._zeroTime; |
| let lastDividerTime = firstDividerTime + sliceTime * dividerCount; |
| |
| // Make an extra divider in case the last one is partially visible. |
| if (!this._endTimePinned) |
| ++dividerCount; |
| |
| let dividerData = { |
| count: dividerCount, |
| firstTime: firstDividerTime, |
| lastTime: lastDividerTime, |
| }; |
| |
| if (Object.shallowEqual(dividerData, this._currentDividers)) { |
| this._updateMarkers(visibleWidth, duration); |
| this._updateSelection(visibleWidth, duration); |
| return; |
| } |
| |
| this._currentDividers = dividerData; |
| |
| let markerDividers = this._markersElement.querySelectorAll("." + WI.TimelineRuler.DividerElementStyleClassName); |
| let dividerElement = this._headerElement.firstChild; |
| |
| for (var i = 0; i <= dividerCount; ++i) { |
| if (!dividerElement) { |
| dividerElement = document.createElement("div"); |
| dividerElement.className = WI.TimelineRuler.DividerElementStyleClassName; |
| this._headerElement.appendChild(dividerElement); |
| |
| let labelElement = document.createElement("div"); |
| labelElement.className = WI.TimelineRuler.DividerLabelElementStyleClassName; |
| dividerElement.appendChild(labelElement); |
| } |
| |
| let markerDividerElement = markerDividers[i]; |
| if (!markerDividerElement) { |
| markerDividerElement = document.createElement("div"); |
| markerDividerElement.className = WI.TimelineRuler.DividerElementStyleClassName; |
| this._markersElement.appendChild(markerDividerElement); |
| } |
| |
| let dividerTime = firstDividerTime + (sliceTime * i); |
| let newPosition = (dividerTime - this._startTime) / duration; |
| |
| if (!this._allowsClippedLabels) { |
| // Don't allow dividers under 0% where they will be completely hidden. |
| if (newPosition < 0) |
| continue; |
| |
| // When over 100% it is time to stop making/updating dividers. |
| if (newPosition > 1) |
| break; |
| |
| // Don't allow the left-most divider spacing to be so tight it clips. |
| if ((newPosition * visibleWidth) < WI.TimelineRuler.MinimumLeftDividerSpacing) |
| continue; |
| } |
| |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._updatePositionOfElement(dividerElement, newPosition, visibleWidth, property); |
| this._updatePositionOfElement(markerDividerElement, newPosition, visibleWidth, property); |
| |
| console.assert(dividerElement.firstChild.classList.contains(WI.TimelineRuler.DividerLabelElementStyleClassName)); |
| |
| dividerElement.firstChild.textContent = isNaN(dividerTime) ? "" : this._formatDividerLabelText(dividerTime - this._zeroTime); |
| dividerElement = dividerElement.nextSibling; |
| } |
| |
| // Remove extra dividers. |
| while (dividerElement) { |
| let nextDividerElement = dividerElement.nextSibling; |
| dividerElement.remove(); |
| dividerElement = nextDividerElement; |
| } |
| |
| for (; i < markerDividers.length; ++i) |
| markerDividers[i].remove(); |
| |
| this._updateMarkers(visibleWidth, duration); |
| this._updateSelection(visibleWidth, duration); |
| } |
| |
| sizeDidChange() |
| { |
| this._cachedClientWidth = this.element.clientWidth; |
| } |
| |
| // Private |
| |
| _needsMarkerLayout() |
| { |
| // If layout is scheduled, abort since markers will be updated when layout happens. |
| if (this.layoutPending) |
| return; |
| |
| if (this._scheduledMarkerLayoutUpdateIdentifier) |
| return; |
| |
| this._scheduledMarkerLayoutUpdateIdentifier = requestAnimationFrame(() => { |
| this._scheduledMarkerLayoutUpdateIdentifier = undefined; |
| |
| let visibleWidth = this._cachedClientWidth; |
| if (visibleWidth <= 0) |
| return; |
| |
| this._updateMarkers(visibleWidth, this.duration); |
| }); |
| } |
| |
| _needsSelectionLayout() |
| { |
| if (!this._allowsTimeRangeSelection) |
| return; |
| |
| // If layout is scheduled, abort since the selection will be updated when layout happens. |
| if (this.layoutPending) |
| return; |
| |
| if (this._scheduledSelectionLayoutUpdateIdentifier) |
| return; |
| |
| this._scheduledSelectionLayoutUpdateIdentifier = requestAnimationFrame(() => { |
| this._scheduledSelectionLayoutUpdateIdentifier = undefined; |
| |
| let visibleWidth = this._cachedClientWidth; |
| if (visibleWidth <= 0) |
| return; |
| |
| this._updateSelection(visibleWidth, this.duration); |
| }); |
| } |
| |
| _recalculate() |
| { |
| let visibleWidth = this._cachedClientWidth; |
| if (visibleWidth <= 0) |
| return 0; |
| |
| let duration; |
| if (this._endTimePinned) |
| duration = this._endTime - this._startTime; |
| else |
| duration = visibleWidth * this._secondsPerPixel; |
| |
| this._secondsPerPixel = duration / visibleWidth; |
| |
| if (!this._endTimePinned) |
| this._endTime = this._startTime + (visibleWidth * this._secondsPerPixel); |
| |
| return visibleWidth; |
| } |
| |
| _updatePositionOfElement(element, newPosition, visibleWidth, property) |
| { |
| newPosition *= this._endTimePinned ? 100 : visibleWidth; |
| |
| let newPositionAprox = Math.round(newPosition * 100); |
| let currentPositionAprox = Math.round(parseFloat(element.style[property]) * 100); |
| if (currentPositionAprox !== newPositionAprox) |
| element.style[property] = (newPositionAprox / 100) + (this._endTimePinned ? "%" : "px"); |
| } |
| |
| _updateMarkers(visibleWidth, duration) |
| { |
| if (this._scheduledMarkerLayoutUpdateIdentifier) { |
| cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier); |
| this._scheduledMarkerLayoutUpdateIdentifier = undefined; |
| } |
| |
| for (let [marker, markerElement] of this._markerElementMap) { |
| if (marker.time < 0) { |
| markerElement.remove(); |
| continue; |
| } |
| |
| let newPosition = (marker.time - this._startTime) / duration; |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._updatePositionOfElement(markerElement, newPosition, visibleWidth, property); |
| |
| if (!markerElement.parentNode) |
| this._markersElement.appendChild(markerElement); |
| } |
| } |
| |
| _updateSelection(visibleWidth, duration) |
| { |
| if (this._scheduledSelectionLayoutUpdateIdentifier) { |
| cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier); |
| this._scheduledSelectionLayoutUpdateIdentifier = undefined; |
| } |
| |
| this.element.classList.toggle("allows-time-range-selection", this._allowsTimeRangeSelection); |
| |
| if (!this._allowsTimeRangeSelection) |
| return; |
| |
| this.element.classList.toggle("selection-hidden", this.entireRangeSelected); |
| |
| if (this.entireRangeSelected) { |
| this._dispatchTimeRangeSelectionChangedEvent(); |
| return; |
| } |
| |
| let startTimeClamped = this._selectionStartTime < this._startTime || this._selectionStartTime > this._endTime; |
| let endTimeClamped = this._selectionEndTime < this._startTime || this._selectionEndTime > this._endTime; |
| |
| this.element.classList.toggle("both-handles-clamped", startTimeClamped && endTimeClamped); |
| |
| let formattedStartTimeText = this._formatDividerLabelText(this._selectionStartTime - this._zeroTime); |
| let formattedEndTimeText = this._formatDividerLabelText(this._selectionEndTime - this._zeroTime); |
| |
| let startProperty = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| let newStartPosition = Number.constrain((this._selectionStartTime - this._startTime) / duration, 0, 1); |
| this._updatePositionOfElement(this._leftShadedAreaElement, newStartPosition, visibleWidth, "width"); |
| this._updatePositionOfElement(this._leftSelectionHandleElement, newStartPosition, visibleWidth, startProperty); |
| this._updatePositionOfElement(this._selectionDragElement, newStartPosition, visibleWidth, startProperty); |
| |
| this._leftSelectionHandleElement.classList.toggle("clamped", startTimeClamped); |
| this._leftSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionStartTime < this._startTime); |
| this._leftSelectionHandleElement.title = formattedStartTimeText; |
| |
| let endProperty = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "left" : "right"; |
| let newEndPosition = 1 - Number.constrain((this._selectionEndTime - this._startTime) / duration, 0, 1); |
| this._updatePositionOfElement(this._rightShadedAreaElement, newEndPosition, visibleWidth, "width"); |
| this._updatePositionOfElement(this._rightSelectionHandleElement, newEndPosition, visibleWidth, endProperty); |
| this._updatePositionOfElement(this._selectionDragElement, newEndPosition, visibleWidth, endProperty); |
| |
| this._rightSelectionHandleElement.classList.toggle("clamped", endTimeClamped); |
| this._rightSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionEndTime > this._endTime); |
| this._rightSelectionHandleElement.title = formattedEndTimeText; |
| |
| if (!this._selectionDragElement.parentNode) { |
| this.element.appendChild(this._selectionDragElement); |
| this.element.appendChild(this._leftShadedAreaElement); |
| this.element.appendChild(this._leftSelectionHandleElement); |
| this.element.appendChild(this._rightShadedAreaElement); |
| this.element.appendChild(this._rightSelectionHandleElement); |
| } |
| |
| this._dispatchTimeRangeSelectionChangedEvent(); |
| } |
| |
| _formatDividerLabelText(value) |
| { |
| if (this._formatLabelCallback) |
| return this._formatLabelCallback(value); |
| |
| return Number.secondsToString(value, true); |
| } |
| |
| _snapValue(value) |
| { |
| if (!value || !this.snapInterval) |
| return value; |
| |
| return Math.round(value / this.snapInterval) * this.snapInterval; |
| } |
| |
| _dispatchTimeRangeSelectionChangedEvent() |
| { |
| if (!this._timeRangeSelectionChanged) |
| return; |
| |
| this._timeRangeSelectionChanged = false; |
| |
| this.dispatchEventToListeners(WI.TimelineRuler.Event.TimeRangeSelectionChanged); |
| } |
| |
| _timelineMarkerTimeChanged() |
| { |
| this._needsMarkerLayout(); |
| } |
| |
| _shouldIgnoreMicroMovement(event) |
| { |
| if (this._mousePassedMicroMovementTest) |
| return false; |
| |
| let pixels = Math.abs(event.pageX - this._mouseStartX); |
| if (pixels <= 4) |
| return true; |
| |
| this._mousePassedMicroMovementTest = true; |
| return false; |
| } |
| |
| _handleClick(event) |
| { |
| if (!this._enabled) |
| return; |
| |
| if (this._mouseMoved) |
| return; |
| |
| for (let newTarget of document.elementsFromPoint(event.pageX, event.pageY)) { |
| if (!newTarget || typeof newTarget.click !== "function") |
| continue; |
| if (this.element.contains(newTarget)) |
| continue; |
| |
| // Clone the event to dispatch it on the new element. |
| let newEvent = new event.constructor(event.type, event); |
| newTarget.dispatchEvent(newEvent); |
| if (newEvent.__timelineRecordClickEventHandled) |
| event.stop(); |
| return; |
| } |
| } |
| |
| _handleDoubleClick(event) |
| { |
| if (this.entireRangeSelected) |
| return; |
| |
| this.selectEntireRange(); |
| } |
| |
| _handleMouseDown(event) |
| { |
| // Only handle left mouse clicks. |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| |
| this._selectionIsMove = event.target === this._selectionDragElement; |
| this._rulerBoundingClientRect = this.element.getBoundingClientRect(); |
| |
| if (this._selectionIsMove) { |
| this._lastMousePosition = event.pageX; |
| var selectionDragElementRect = this._selectionDragElement.getBoundingClientRect(); |
| this._moveSelectionMaximumLeftOffset = this._rulerBoundingClientRect.left + (event.pageX - selectionDragElementRect.left); |
| this._moveSelectionMaximumRightOffset = this._rulerBoundingClientRect.right - (selectionDragElementRect.right - event.pageX); |
| } else { |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| this._mouseDownPosition = this._rulerBoundingClientRect.right - event.pageX; |
| else |
| this._mouseDownPosition = event.pageX - this._rulerBoundingClientRect.left; |
| } |
| |
| this._mouseMoved = false; |
| |
| this._mousePassedMicroMovementTest = false; |
| this._mouseStartX = event.pageX; |
| |
| this._mouseMoveEventListener = this._handleMouseMove.bind(this); |
| this._mouseUpEventListener = this._handleMouseUp.bind(this); |
| |
| // Register these listeners on the document so we can track the mouse if it leaves the ruler. |
| document.addEventListener("mousemove", this._mouseMoveEventListener); |
| document.addEventListener("mouseup", this._mouseUpEventListener); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleMouseMove(event) |
| { |
| console.assert(event.button === 0); |
| |
| if (this._shouldIgnoreMicroMovement(event)) |
| return; |
| |
| this._mouseMoved = true; |
| |
| let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL; |
| |
| let currentMousePosition; |
| if (this._selectionIsMove) { |
| currentMousePosition = Math.max(this._moveSelectionMaximumLeftOffset, Math.min(this._moveSelectionMaximumRightOffset, event.pageX)); |
| |
| let positionDelta = 0; |
| if (isRTL) |
| positionDelta = this._lastMousePosition - currentMousePosition; |
| else |
| positionDelta = currentMousePosition - this._lastMousePosition; |
| |
| let offsetTime = positionDelta * this.secondsPerPixel; |
| let selectionDuration = this.selectionEndTime - this.selectionStartTime; |
| let oldSelectionStartTime = this.selectionStartTime; |
| |
| this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime + offsetTime, this.endTime - selectionDuration)); |
| this.selectionEndTime = this.selectionStartTime + selectionDuration; |
| |
| if (this.snapInterval) { |
| // When snapping we need to check the mouse position delta relative to the last snap, rather than the |
| // last mouse move. If a snap occurs we adjust for the amount the cursor drifted, so that the mouse |
| // position relative to the selection remains constant. |
| let snapOffset = this.selectionStartTime - oldSelectionStartTime; |
| if (!snapOffset) |
| return; |
| |
| let positionDrift = (offsetTime - snapOffset * this.snapInterval) / this.secondsPerPixel; |
| currentMousePosition -= positionDrift; |
| } |
| |
| this._lastMousePosition = currentMousePosition; |
| } else { |
| if (isRTL) |
| currentMousePosition = this._rulerBoundingClientRect.right - event.pageX; |
| else |
| currentMousePosition = event.pageX - this._rulerBoundingClientRect.left; |
| |
| this.selectionStartTime = Math.max(this.startTime, this.startTime + (Math.min(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel)); |
| this.selectionEndTime = Math.min(this.startTime + (Math.max(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel), this.endTime); |
| |
| // Turn on col-resize cursor style once dragging begins, rather than on the initial mouse down. |
| this.element.classList.add(WI.TimelineRuler.ResizingSelectionStyleClassName); |
| } |
| |
| this._updateSelection(this._cachedClientWidth, this.duration); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleMouseUp(event) |
| { |
| console.assert(event.button === 0); |
| |
| if (!this._selectionIsMove) { |
| this.element.classList.remove(WI.TimelineRuler.ResizingSelectionStyleClassName); |
| |
| if (this.selectionEndTime - this.selectionStartTime < this.minimumSelectionDuration) { |
| // The section is smaller than allowed, grow in the direction of the drag to meet the minumum. |
| let currentMousePosition = 0; |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| currentMousePosition = this._rulerBoundingClientRect.right - event.pageX; |
| else |
| currentMousePosition = event.pageX - this._rulerBoundingClientRect.left; |
| |
| if (currentMousePosition > this._mouseDownPosition) { |
| this.selectionEndTime = Math.min(this.selectionStartTime + this.minimumSelectionDuration, this.endTime); |
| this.selectionStartTime = this.selectionEndTime - this.minimumSelectionDuration; |
| } else { |
| this.selectionStartTime = Math.max(this.startTime, this.selectionEndTime - this.minimumSelectionDuration); |
| this.selectionEndTime = this.selectionStartTime + this.minimumSelectionDuration; |
| } |
| } |
| } |
| |
| this._dispatchTimeRangeSelectionChangedEvent(); |
| |
| document.removeEventListener("mousemove", this._mouseMoveEventListener); |
| document.removeEventListener("mouseup", this._mouseUpEventListener); |
| |
| delete this._mouseMoveEventListener; |
| delete this._mouseUpEventListener; |
| delete this._mouseDownPosition; |
| delete this._lastMousePosition; |
| delete this._selectionIsMove; |
| delete this._rulerBoundingClientRect; |
| delete this._moveSelectionMaximumLeftOffset; |
| delete this._moveSelectionMaximumRightOffset; |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleSelectionHandleMouseDown(event) |
| { |
| // Only handle left mouse clicks. |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| |
| this._dragHandleIsStartTime = event.target === this._leftSelectionHandleElement; |
| |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| this._mouseDownPosition = this.element.totalOffsetRight - event.pageX; |
| else |
| this._mouseDownPosition = event.pageX - this.element.totalOffsetLeft; |
| |
| this._selectionHandleMouseMoveEventListener = this._handleSelectionHandleMouseMove.bind(this); |
| this._selectionHandleMouseUpEventListener = this._handleSelectionHandleMouseUp.bind(this); |
| |
| // Register these listeners on the document so we can track the mouse if it leaves the ruler. |
| document.addEventListener("mousemove", this._selectionHandleMouseMoveEventListener); |
| document.addEventListener("mouseup", this._selectionHandleMouseUpEventListener); |
| |
| this.element.classList.add(WI.TimelineRuler.ResizingSelectionStyleClassName); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleSelectionHandleMouseMove(event) |
| { |
| console.assert(event.button === 0); |
| |
| let currentMousePosition = 0; |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| currentMousePosition = this.element.totalOffsetRight - event.pageX; |
| else |
| currentMousePosition = event.pageX - this.element.totalOffsetLeft; |
| |
| let currentTime = this.startTime + (currentMousePosition * this.secondsPerPixel); |
| if (this.snapInterval) |
| currentTime = this._snapValue(currentTime); |
| |
| if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) { |
| // Resize the selection on both sides when the Option keys is held down. |
| if (this._dragHandleIsStartTime) { |
| let timeDifference = currentTime - this.selectionStartTime; |
| this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration)); |
| this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, this.selectionEndTime - timeDifference), this.endTime); |
| } else { |
| let timeDifference = currentTime - this.selectionEndTime; |
| this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime); |
| this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime - timeDifference, this.selectionEndTime - this.minimumSelectionDuration)); |
| } |
| } else { |
| // Resize the selection on side being dragged. |
| if (this._dragHandleIsStartTime) |
| this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration)); |
| else |
| this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime); |
| } |
| |
| this._updateSelection(this._cachedClientWidth, this.duration); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleSelectionHandleMouseUp(event) |
| { |
| console.assert(event.button === 0); |
| |
| this.element.classList.remove(WI.TimelineRuler.ResizingSelectionStyleClassName); |
| |
| document.removeEventListener("mousemove", this._selectionHandleMouseMoveEventListener); |
| document.removeEventListener("mouseup", this._selectionHandleMouseUpEventListener); |
| |
| delete this._selectionHandleMouseMoveEventListener; |
| delete this._selectionHandleMouseUpEventListener; |
| delete this._dragHandleIsStartTime; |
| delete this._mouseDownPosition; |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| }; |
| |
| WI.TimelineRuler.MinimumLeftDividerSpacing = 48; |
| WI.TimelineRuler.MinimumDividerSpacing = 64; |
| |
| WI.TimelineRuler.ResizingSelectionStyleClassName = "resizing-selection"; |
| WI.TimelineRuler.DividerElementStyleClassName = "divider"; |
| WI.TimelineRuler.DividerLabelElementStyleClassName = "label"; |
| |
| WI.TimelineRuler.Event = { |
| TimeRangeSelectionChanged: "time-ruler-time-range-selection-changed" |
| }; |