summaryrefslogtreecommitdiffstats
path: root/src/mixins/vueAtReparenter.js
blob: e64bb28680aaccf62c3bdf3e3ac6662123d2b16d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
 *
 * @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import Vue from 'vue'

/**
 * Mixin to reparent the panel of the vue-at component to a specific element.
 *
 * By default the panel of the vue-at component is a child of the root element
 * of the component. In some cases this may not be desirable (for example, if
 * a parent element uses "overflow: hidden" and causes the panel to be
 * partially hidden), so this mixin reparents the panel to a specific element
 * when it is shown.
 *
 * Components using this mixin require a reference called "at" to the vue-at
 * component. The desired parent element can be specified using the
 * "atWhoPanelParentSelector" property.
 */
export default {

	data: function() {
		return {
			/**
			 * The selector for the HTML element to reparent the vue-at panel to.
			 */
			atWhoPanelParentSelector: 'body',
			/**
			 * Extra CSS classes to be set in the vue-at panel.
			 */
			atWhoPanelExtraClasses: '',
			at: null,
			atWhoPanelElement: null,
			originalWrapElement: null,
		}
	},

	computed: {
		/**
		 * Returns the "atwho" property of the vue-at component.
		 *
		 * The "atwho" property is an object when the panel is open and null
		 * when the panel is closed.
		 *
		 * @returns {Object} the "atwho" property of the vue-at component.
		 */
		atwho() {
			if (!this.at) {
				return null
			}

			return this.at.atwho
		},

		/**
		 * Returns a list of CSS clases from the space separated string
		 * "atWhoPanelExtraClasses".
		 *
		 * @returns {Array} the list of CSS classes
		 */
		atWhoPanelExtraClassesList() {
			return this.atWhoPanelExtraClasses.split(' ').filter(cssClass => cssClass !== '')
		},
	},

	watch: {
		/**
		 * Reparents the panel of the vue-at component when shown.
		 *
		 * Besides reparenting the panel its position needs to be adjusted to
		 * the new parent. The panel is initially a child of the "wrap" element
		 * of vue-at and vue-at calculates the position of the panel based on
		 * that element. Fortunately the reference to that element is not used
		 * for anything else, so it can be modified while the panel is open to
		 * point to the new parent.
		 *
		 * @param {Object} atwho current value of atwho
		 * @param {Object} atwhoOld previous value of atwho
		 */
		atwho(atwho, atwhoOld) {
			// Only check whether the object existed or not; its properties are
			// not relevant.
			if ((atwho && atwhoOld) || (!atwho && !atwhoOld)) {
				return
			}

			if (atwho) {
				// Panel will be opened in next tick; defer moving it to the
				// proper parent until that happens
				Vue.nextTick(function() {
					this.atWhoPanelElement = this.at.$refs.wrap.querySelector('.atwho-panel')

					if (this.atWhoPanelExtraClassesList.length > 0) {
						this.atWhoPanelElement.classList.add(...this.atWhoPanelExtraClassesList)
					}

					this.originalWrapElement = this.at.$refs.wrap
					this.at.$refs.wrap = window.document.querySelector(this.atWhoPanelParentSelector)

					const atWhoPanelParentSelector = window.document.querySelector(this.atWhoPanelParentSelector)
					atWhoPanelParentSelector.appendChild(this.atWhoPanelElement)

					// The position of the panel will be automatically adjusted
					// due to the reactivity, but that will happen in next tick.
					// To prevent a flicker due to the change of the panel
					// position the style is explicitly adjusted now.
					const { top, left } = this.at._computedWatchers.style.get()
					this.atWhoPanelElement.style.top = top
					this.atWhoPanelElement.style.left = left
				}.bind(this))
			} else {
				this.at.$refs.wrap = this.originalWrapElement
				this.originalWrapElement = null

				// Panel will be closed in next tick; move it back to the
				// expected parent before that happens.
				this.at.$refs.wrap.appendChild(this.atWhoPanelElement)
			}
		},
	},

	mounted() {
		// $refs is not reactive and its contents are set after the initial
		// render.
		this.at = this.$refs.at
	},

}