1<!-- Copyright (C) 2020 The Android Open Source Project 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14--> 15<template> 16 <div 17 class="draggable-container" 18 :style="{visibility: contentIsLoaded ? 'visible' : 'hidden'}" 19 > 20 <md-card class="draggable-card"> 21 <div class="header" @mousedown="onHeaderMouseDown"> 22 <md-icon class="drag-icon"> 23 drag_indicator 24 </md-icon> 25 <slot name="header" /> 26 </div> 27 <div class="content"> 28 <slot name="main" ref="content"/> 29 <div class="resizer" v-show="resizeable" @mousedown="onResizerMouseDown"/> 30 </div> 31 </md-card> 32 </div> 33</template> 34<script> 35export default { 36 name: "DraggableDiv", 37 // If asyncLoad is enabled must call contentLoaded when content is ready 38 props: ['position', 'asyncLoad', 'resizeable'], 39 data() { 40 return { 41 positions: { 42 clientX: undefined, 43 clientY: undefined, 44 movementX: 0, 45 movementY: 0, 46 }, 47 parentResizeObserver: null, 48 contentIsLoaded: false, 49 extraWidth: 0, 50 extraHeight: 0, 51 } 52 }, 53 methods: { 54 onHeaderMouseDown(e) { 55 e.preventDefault(); 56 57 this.initDragAction(e); 58 }, 59 onResizerMouseDown(e) { 60 e.preventDefault(); 61 62 this.startResize(e); 63 }, 64 initDragAction(e) { 65 this.positions.clientX = e.clientX; 66 this.positions.clientY = e.clientY; 67 document.onmousemove = this.startDrag; 68 document.onmouseup = this.stopDrag; 69 }, 70 startDrag(e) { 71 e.preventDefault(); 72 73 this.positions.movementX = this.positions.clientX - e.clientX; 74 this.positions.movementY = this.positions.clientY - e.clientY; 75 this.positions.clientX = e.clientX; 76 this.positions.clientY = e.clientY; 77 78 const parentHeight = this.$el.parentElement.clientHeight; 79 const parentWidth = this.$el.parentElement.clientWidth; 80 81 const divHeight = this.$el.clientHeight; 82 const divWidth = this.$el.clientWidth; 83 84 let top = this.$el.offsetTop - this.positions.movementY; 85 if (top < 0) { 86 top = 0; 87 } 88 if (top + divHeight > parentHeight) { 89 top = parentHeight - divHeight; 90 } 91 92 let left = this.$el.offsetLeft - this.positions.movementX; 93 if (left < 0) { 94 left = 0; 95 } 96 if (left + divWidth > parentWidth) { 97 left = parentWidth - divWidth; 98 } 99 100 this.$el.style.top = top + 'px'; 101 this.$el.style.left = left + 'px'; 102 }, 103 stopDrag() { 104 document.onmouseup = null; 105 document.onmousemove = null; 106 }, 107 startResize(e) { 108 e.preventDefault(); 109 this.startResizeX = e.clientX; 110 this.startResizeY = e.clientY; 111 document.onmousemove = this.resizing; 112 document.onmouseup = this.stopResize; 113 document.body.style.cursor = "nwse-resize"; 114 }, 115 resizing(e) { 116 let extraWidth = this.extraWidth + (e.clientX - this.startResizeX); 117 if (extraWidth < 0) { 118 extraWidth = 0; 119 } 120 this.$emit('requestExtraWidth', extraWidth); 121 122 let extraHeight = this.extraHeight + (e.clientY - this.startResizeY); 123 if (extraHeight < 0) { 124 extraHeight = 0; 125 } 126 this.$emit('requestExtraHeight', extraHeight); 127 }, 128 stopResize(e) { 129 this.extraWidth += e.clientX - this.startResizeX; 130 if (this.extraWidth < 0) { 131 this.extraWidth = 0; 132 } 133 this.extraHeight += e.clientY - this.startResizeY; 134 if (this.extraHeight < 0) { 135 this.extraHeight = 0; 136 } 137 document.onmousemove = null; 138 document.onmouseup = null; 139 document.body.style.cursor = null; 140 }, 141 onParentResize() { 142 const parentHeight = this.$el.parentElement.clientHeight; 143 const parentWidth = this.$el.parentElement.clientWidth; 144 145 const elHeight = this.$el.clientHeight; 146 const elWidth = this.$el.clientWidth; 147 const rect = this.$el.getBoundingClientRect(); 148 149 const offsetBottom = parentHeight - (rect.y + elHeight); 150 if (offsetBottom < 0) { 151 this.$el.style.top = parseInt(this.$el.style.top) + offsetBottom + 'px'; 152 } 153 154 const offsetRight = parentWidth - (rect.x + elWidth); 155 if (offsetRight < 0) { 156 this.$el.style.left = parseInt(this.$el.style.left) + offsetRight + 'px'; 157 } 158 }, 159 contentLoaded() { 160 // To be called if content is loaded async (eg: video), so that div may 161 // position itself correctly. 162 163 if (this.contentIsLoaded) { 164 return; 165 } 166 167 this.contentIsLoaded = true; 168 const margin = 15; 169 170 switch (this.position) { 171 case 'bottomLeft': 172 this.moveToBottomLeft(margin); 173 break; 174 175 default: 176 throw new Error('Unsupported starting position for DraggableDiv'); 177 } 178 }, 179 moveToBottomLeft(margin) { 180 margin = margin || 0; 181 182 const divHeight = this.$el.clientHeight; 183 const parentHeight = this.$el.parentElement.clientHeight; 184 185 this.$el.style.top = parentHeight - divHeight - margin + 'px'; 186 this.$el.style.left = margin + 'px'; 187 }, 188 }, 189 mounted() { 190 if (!this.asyncLoad) { 191 this.contentLoaded(); 192 } 193 194 // Listen for changes in parent height to avoid element exiting visible view 195 this.parentResizeObserver = new ResizeObserver(this.onParentResize); 196 197 this.parentResizeObserver.observe(this.$el.parentElement); 198 }, 199 destroyed() { 200 this.parentResizeObserver.unobserve(this.$el.parentElement); 201 }, 202} 203</script> 204<style scoped> 205.draggable-container { 206 position: absolute; 207} 208 209.draggable-card { 210 margin: 0; 211} 212 213.header { 214 cursor: grab; 215 padding: 3px; 216} 217 218.resizer { 219 position: absolute; 220 right: 0; 221 bottom: 0; 222 width: 0; 223 height: 0; 224 border-style: solid; 225 border-width: 0 0 15px 15px; 226 border-color: transparent transparent #ffffff transparent; 227 cursor: nwse-resize; 228} 229</style>