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>