| <!-- Copyright (C) 2020 The Android Open Source Project |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --> |
| <template> |
| <div class="wrapper"> |
| <svg |
| width="100%" |
| height="20" |
| class="timeline-svg" |
| :class="{disabled: disabled}" |
| ref="timeline" |
| > |
| <rect |
| :x="`${block.startPos}%`" |
| y="0" |
| :width="`${block.width}%`" |
| :height="pointHeight" |
| :rx="corner" |
| v-for="(block, idx) in timelineBlocks" |
| :key="idx" |
| class="point" |
| /> |
| <rect |
| v-if="selectedWidth >= 0" |
| v-show="showSelection" |
| :x="selectionAreaStart" |
| y="0" |
| :width="selectedWidth" |
| :height="pointHeight" |
| :rx="corner" |
| class="point selection" |
| ref="selectedSection" |
| /> |
| <rect |
| v-else |
| v-show="showSelection" |
| :x="selectionAreaEnd" |
| y="0" |
| :width="-selectedWidth" |
| :height="pointHeight" |
| :rx="corner" |
| class="point selection" |
| ref="selectedSection" |
| /> |
| |
| <rect |
| v-show="showSelection" |
| :x="selectionAreaStart - 2" |
| y="0" |
| :width="4" |
| :height="pointHeight" |
| :rx="corner" |
| class="point selection-edge" |
| ref="leftResizeDragger" |
| /> |
| |
| <rect |
| v-show="showSelection" |
| :x="selectionAreaEnd - 2" |
| y="0" |
| :width="4" |
| :height="pointHeight" |
| :rx="corner" |
| class="point selection-edge" |
| ref="rightResizeDragger" |
| /> |
| </svg> |
| </div> |
| </template> |
| <script> |
| import TimelineMixin from './mixins/Timeline'; |
| |
| export default { |
| name: 'timelineSelection', |
| props: ['startTimestamp', 'endTimestamp', 'cropArea', 'disabled'], |
| data() { |
| return { |
| pointHeight: 15, |
| corner: 2, |
| selectionStartPosition: 0, |
| selectionEndPosition: 0, |
| selecting: false, |
| dragged: false, |
| draggingSelection: false, |
| }; |
| }, |
| mixins: [TimelineMixin], |
| watch: { |
| selectionStartPosition() { |
| // Send crop intent rather than final crop value while we are selecting |
| if ((this.selecting && this.dragged)) { |
| this.emitCropIntent(); |
| return; |
| } |
| |
| this.emitCropDetails(); |
| }, |
| selectionEndPosition() { |
| // Send crop intent rather than final crop value while we are selecting |
| if ((this.selecting && this.dragged)) { |
| this.emitCropIntent(); |
| return; |
| } |
| |
| this.emitCropDetails(); |
| }, |
| }, |
| methods: { |
| /** |
| * Create an object that can be injected and removed from the DOM to change |
| * the cursor style. The object is a mask over the entire screen. It is |
| * done this way as opposed to injecting a style targeting all elements for |
| * performance reasons, otherwise recalculate style would be very slow. |
| * This makes sure that regardless of the cursor style of other elements, |
| * the cursor style will be set to what we want over the entire screen. |
| * @param {string} cursor - The cursor type to apply to the entire page. |
| * @return An object that can be injected and removed from the DOM which |
| * changes the cursor style for the entire page. |
| */ |
| createCursorStyle(cursor) { |
| const cursorMask = document.createElement('div'); |
| cursorMask.style.cursor = cursor; |
| cursorMask.style.height = '100vh'; |
| cursorMask.style.width = '100vw'; |
| cursorMask.style.position = 'fixed'; |
| cursorMask.style.top = '0'; |
| cursorMask.style.left = '0'; |
| cursorMask.style['z-index'] = '10'; |
| |
| return { |
| inject: () => { |
| document.body.appendChild(cursorMask); |
| }, |
| remove: () => { |
| try { |
| document.body.removeChild(cursorMask); |
| } catch (e) {} |
| }, |
| }; |
| }, |
| |
| setupCreateSelectionListeners() { |
| const cursorStyle = this.createCursorStyle('crosshair'); |
| |
| this.timelineSvgMouseDownEventListener = (e) => { |
| e.stopPropagation(); |
| this.selecting = true; |
| this.dragged = false; |
| this.mouseDownX = e.offsetX; |
| this.mouseDownClientX = e.clientX; |
| |
| cursorStyle.inject(); |
| }; |
| |
| this.createSelectionMouseMoveEventListener = (e) => { |
| if (this.selecting) { |
| if (!this.dragged) { |
| this.selectionStartX = this.mouseDownX; |
| } |
| |
| this.dragged = true; |
| const draggedAmount = e.clientX - this.mouseDownClientX; |
| |
| if (draggedAmount >= 0) { |
| this.selectionStartPosition = this.selectionStartX; |
| |
| const endX = this.selectionStartX + draggedAmount; |
| if (endX <= this.$refs.timeline.clientWidth) { |
| this.selectionEndPosition = endX; |
| } else { |
| this.selectionEndPosition = this.$refs.timeline.clientWidth; |
| } |
| |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionEndPosition)); |
| } else { |
| this.selectionEndPosition = this.selectionStartX; |
| |
| const startX = this.selectionStartX + draggedAmount; |
| if (startX >= 0) { |
| this.selectionStartPosition = startX; |
| } else { |
| this.selectionStartPosition = 0; |
| } |
| |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionStartPosition)); |
| } |
| } |
| }; |
| |
| this.createSelectionMouseUpEventListener = (e) => { |
| this.selecting = false; |
| cursorStyle.remove(); |
| this.$emit('resetVideoTimestamp'); |
| if (this.dragged) { |
| // Clear crop intent, we now have a set crop value |
| this.clearCropIntent(); |
| // Notify of final crop value |
| this.emitCropDetails(); |
| } |
| this.dragged = false; |
| }; |
| |
| this.$refs.timeline |
| .addEventListener('mousedown', this.timelineSvgMouseDownEventListener); |
| document |
| .addEventListener('mousemove', this.createSelectionMouseMoveEventListener); |
| document |
| .addEventListener('mouseup', this.createSelectionMouseUpEventListener); |
| }, |
| |
| teardownCreateSelectionListeners() { |
| this.$refs.timeline |
| .removeEventListener('mousedown', this.timelineSvgMouseDownEventListener); |
| document |
| .removeEventListener('mousemove', this.createSelectionMouseMoveEventListener); |
| document |
| .removeEventListener('mouseup', this.createSelectionMouseUpEventListener); |
| }, |
| |
| setupDragSelectionListeners() { |
| const cursorStyle = this.createCursorStyle('move'); |
| |
| this.selectedSectionMouseDownListener = (e) => { |
| e.stopPropagation(); |
| this.draggingSelectionStartX = e.clientX; |
| this.selectionStartPosition = this.selectionAreaStart; |
| this.selectionEndPosition = this.selectionAreaEnd; |
| this.draggingSelectionStartPos = this.selectionAreaStart; |
| this.draggingSelectionEndPos = this.selectionAreaEnd; |
| |
| // Keep this after fetching selectionAreaStart and selectionAreaEnd. |
| this.draggingSelection = true; |
| |
| cursorStyle.inject(); |
| }; |
| |
| this.dragSelectionMouseMoveEventListener = (e) => { |
| if (this.draggingSelection) { |
| const dragAmount = e.clientX - this.draggingSelectionStartX; |
| |
| const newStartPos = this.draggingSelectionStartPos + dragAmount; |
| const newEndPos = this.draggingSelectionEndPos + dragAmount; |
| if (newStartPos >= 0 && newEndPos <= this.$refs.timeline.clientWidth) { |
| this.selectionStartPosition = newStartPos; |
| this.selectionEndPosition = newEndPos; |
| } else { |
| if (newStartPos < 0) { |
| this.selectionStartPosition = 0; |
| this.selectionEndPosition = newEndPos - (newStartPos /* negative overflown amount*/); |
| } else { |
| const overflownAmount = newEndPos - this.$refs.timeline.clientWidth; |
| this.selectionEndPosition = this.$refs.timeline.clientWidth; |
| this.selectionStartPosition = newStartPos - overflownAmount; |
| } |
| } |
| } |
| }; |
| |
| this.dragSelectionMouseUpEventListener = (e) => { |
| this.draggingSelection = false; |
| cursorStyle.remove(); |
| }; |
| |
| this.$refs.selectedSection |
| .addEventListener('mousedown', this.selectedSectionMouseDownListener); |
| document |
| .addEventListener('mousemove', this.dragSelectionMouseMoveEventListener); |
| document |
| .addEventListener('mouseup', this.dragSelectionMouseUpEventListener); |
| }, |
| |
| teardownDragSelectionListeners() { |
| this.$refs.selectedSection |
| .removeEventListener('mousedown', this.selectedSectionMouseDownListener); |
| document |
| .removeEventListener('mousemove', this.dragSelectionMouseMoveEventListener); |
| document |
| .removeEventListener('mouseup', this.dragSelectionMouseUpEventListener); |
| }, |
| |
| setupResizeSelectionListeners() { |
| const cursorStyle = this.createCursorStyle('ew-resize'); |
| |
| this.leftResizeDraggerMouseDownEventListener = (e) => { |
| e.stopPropagation(); |
| this.resizeStartX = e.clientX; |
| this.selectionStartPosition = this.selectionAreaStart; |
| this.selectionEndPosition = this.selectionAreaEnd; |
| this.resizeStartPos = this.selectionAreaStart; |
| this.resizeingLeft = true; |
| |
| cursorStyle.inject(); |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionAreaStart)); |
| }; |
| |
| this.rightResizeDraggerMouseDownEventListener = (e) => { |
| e.stopPropagation(); |
| this.resizeStartX = e.clientX; |
| this.selectionStartPosition = this.selectionAreaStart; |
| this.selectionEndPosition = this.selectionAreaEnd; |
| this.resizeEndPos = this.selectionAreaEnd; |
| this.resizeingRight = true; |
| |
| cursorStyle.inject(); |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionAreaEnd)); |
| }; |
| |
| this.resizeMouseMoveEventListener = (e) => { |
| if (this.resizeingLeft) { |
| const moveAmount = e.clientX - this.resizeStartX; |
| let newStartPos = this.resizeStartPos + moveAmount; |
| if (newStartPos >= this.selectionEndPosition) { |
| newStartPos = this.selectionEndPosition; |
| } |
| if (newStartPos < 0) { |
| newStartPos = 0; |
| } |
| |
| this.selectionStartPosition = newStartPos; |
| |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionStartPosition)); |
| } |
| |
| if (this.resizeingRight) { |
| const moveAmount = e.clientX - this.resizeStartX; |
| let newEndPos = this.resizeEndPos + moveAmount; |
| if (newEndPos <= this.selectionStartPosition) { |
| newEndPos = this.selectionStartPosition; |
| } |
| if (newEndPos > this.$refs.timeline.clientWidth) { |
| newEndPos = this.$refs.timeline.clientWidth; |
| } |
| |
| this.selectionEndPosition = newEndPos; |
| this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionEndPosition)); |
| } |
| }; |
| |
| this.resizeSelectionMouseUpEventListener = (e) => { |
| this.resizeingLeft = false; |
| this.resizeingRight = false; |
| cursorStyle.remove(); |
| this.$emit('resetVideoTimestamp'); |
| }; |
| |
| this.$refs.leftResizeDragger |
| .addEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener); |
| this.$refs.rightResizeDragger |
| .addEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener); |
| document |
| .addEventListener('mousemove', this.resizeMouseMoveEventListener); |
| document |
| .addEventListener('mouseup', this.resizeSelectionMouseUpEventListener); |
| }, |
| |
| teardownResizeSelectionListeners() { |
| this.$refs.leftResizeDragger |
| .removeEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener); |
| this.$refs.rightResizeDragger |
| .removeEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener); |
| document |
| .removeEventListener('mousemove', this.resizeMouseMoveEventListener); |
| document |
| .removeEventListener('mouseup', this.resizeSelectionMouseUpEventListener); |
| }, |
| |
| emitCropDetails() { |
| const width = this.$refs.timeline.clientWidth; |
| this.$emit('crop', { |
| left: this.selectionStartPosition / width, |
| right: this.selectionEndPosition / width, |
| }); |
| }, |
| |
| emitCropIntent() { |
| const width = this.$refs.timeline.clientWidth; |
| this.$emit('cropIntent', { |
| left: this.selectionStartPosition / width, |
| right: this.selectionEndPosition / width |
| }); |
| }, |
| |
| clearCropIntent() { |
| this.$emit('cropIntent', null); |
| } |
| }, |
| computed: { |
| selected() { |
| return this.timeline[this.selectedIndex]; |
| }, |
| selectedWidth() { |
| return this.selectionAreaEnd - this.selectionAreaStart; |
| }, |
| showSelection() { |
| return this.selectionAreaStart || this.selectionAreaEnd; |
| }, |
| selectionAreaStart() { |
| if ((this.selecting && this.dragged) || this.draggingSelection) { |
| return this.selectionStartPosition; |
| } |
| |
| if (this.cropArea && this.$refs.timeline) { |
| return this.cropArea.left * this.$refs.timeline.clientWidth; |
| } |
| |
| return 0; |
| }, |
| selectionAreaEnd() { |
| if ((this.selecting && this.dragged) || this.draggingSelection) { |
| return this.selectionEndPosition; |
| } |
| |
| if (this.cropArea && this.$refs.timeline) { |
| return this.cropArea.right * this.$refs.timeline.clientWidth; |
| } |
| |
| return 0; |
| }, |
| }, |
| mounted() { |
| this.setupCreateSelectionListeners(); |
| this.setupDragSelectionListeners(); |
| this.setupResizeSelectionListeners(); |
| }, |
| beforeDestroy() { |
| this.teardownCreateSelectionListeners(); |
| this.teardownDragSelectionListeners(); |
| this.teardownResizeSelectionListeners(); |
| }, |
| }; |
| </script> |
| <style scoped> |
| .wrapper { |
| padding: 0 15px; |
| } |
| |
| .timeline-svg { |
| cursor: crosshair; |
| } |
| .timeline-svg .point { |
| fill: #BDBDBD; |
| } |
| .timeline-svg .point.selection { |
| fill: rgba(240, 59, 59, 0.596); |
| cursor: move; |
| } |
| |
| .timeline-svg .point.selection-edge { |
| fill: rgba(27, 123, 212, 0.596); |
| cursor: ew-resize; |
| } |
| </style> |