blob: 8829009110753e173743b242bcac785d08d4ba61 [file] [log] [blame]
<!-- 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>