summaryrefslogtreecommitdiff
path: root/public-src/golden-layout-vue.jsm
blob: 92c36fb4145b6461e15a5c2444cd1b8fa79ce674 (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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import Vue from 'vue';
import LayoutManager from 'golden-layout';

/**
 * `this` will be set to the VueComponentHandler instance.
 */
function onOpen() {
	/* mount the Vue instance *inside* of _container.getElement().
	 * To do that we need to create a temporary element inside of
	 * it for the Vue instance to replace. */
	const tmp = document.createElement('div');
	this._container.getElement()[0].appendChild(tmp);
	this._vueInstance.$mount(tmp);
}

/**
 * `this` will be set to the VueComponentHandler instance.
 */
function onDestroy() {
	this._vueInstance.$destroy();
	this._container.off('open', onOpen, this);
	this._container.off('destroy', onDestroy, this);
}

function deepCopySanitize(obj) {
	if (obj === undefined)
		return undefined;
	return JSON.parse(JSON.stringify(obj));
}

/**
 * A specialised GoldenLayout component that binds GoldenLayout container
 * lifecycle events to Vue components
 */
class VueComponentHandler {
	/**
	 * @param {Function} the Vue component constructor/class that we're wrapping
	 * @param {lm.container.ItemContainer} container as passed by GoldenLayout
	 * @param {Object} state as passed by GoldenLayout
	 */
	constructor(vueComponentClass, container, state) {
		this._container = container;
		/* Get the state directly from the container, rather
		 * than trusting the `state` argument; `state` has
		 * `config.componentName` injected in to it, poluting
		 * our Vue instance's `$data`.
		 *
		 * But, the `state` argument was a deep copy of the
		 * real state; we still need it to be a copy, so that
		 * data-propagation doesn't bypass our $watch.
		 *
		 * See golden-layout/src/js/items/Component.js:lm.items.Component().
		 */
		const realState = deepCopySanitize(container.getState());

		var options = {};
		Object.assign(options, container.layoutManager._vueOptions);
		if (container._config.vueOptions) {
			Object.assign(options, container._config.vueOptions);
		}
		if (realState) {
			options.data = Object.assign(options.data || {}, realState);
		}
		this._vueInstance = new vueComponentClass(options);

		// add Vue -> GoldenLayout event translator
		this._vueInstance.$watch('$data', (newData, oldData) => {
			this._container.setState(deepCopySanitize(newData));
		}, { deep: true });

		// add GoldenLayout -> Vue event translators
		this._container.on('open', onOpen, this);
		this._container.on('destroy', onDestroy, this);
	}
}

LayoutManager.prototype._vueInit = function() {
	if (!this._vueInitialized) {
		this._components["lm-vue-component"] = VueComponentHandler;
		this._vueOptions = {};
		this._vueInitialized = true;
	}
};

/**
 * Register a Vue component for use with this GoldenLayout
 * LayoutManager.
 *
 * The component can then be used just like any other GoldenLayout
 * component.
 *
 *  - The `componentState` config item is bound with the Vue
 *    component's `$data`; the component's private data will be
 *    reflected in `layoutManager.toConfig()`, allowing the component
 *    state to be persisted as part of the layoutManager state.
 *
 * - The `vueOptions` config item becomes the Vue component's options.
 *   This is merged with any global Vue options set with
 *   `.setVueOptions()`, and obviously `componentState` becomes
 *   `vueOptions.data`.  This MUST be a "plain" JSON-encodable object,
 *   and cannot contain complex objects like a Vuex.Store.
 *
 * @public
 * @param {String} name Like `Vue.component()` registration, the
 *                      `name` argument is optional if the component
 *                      itself has a default name.
 * @param {Function|Object} component Like `Vue.component()`
 *                                    registration, this can be a
 *                                    constructor function, or an
 *                                    options object (in which case it
 *                                    will be automatically turned in
 *                                    to a constructor function by
 *                                    passing it to `Vue.extend`).
 * @returns {void}
 */
LayoutManager.prototype.registerVueComponent = function(name, component) {
	this._vueInit();

	if (component === undefined) {
		component = name;
		name = component.name;
	}

	if (!name) {
		throw new Error('Cannot register a component without a name');
	}

	if (!component.name) {
		component.name = name;
	}

	if (typeof component !== "function") {
		component = Vue.extend(component);
	}

	class ThisVueComponentHandler extends VueComponentHandler {
		constructor(container, state) {
			super(component, container, state);
		}
	}

	this.registerComponent(name, ThisVueComponentHandler);
};

/**
 * Set the Vue options.
 *
 * You may be wondering why this is a separate function, instead of
 * simply relying on the `vueOptions` field to the settings object.
 * The settings object *must* be a "plain" JSON-type object; it can't
 * include complex objects, like a Vuex.Store (it would actually stack
 * overflow if we tried putting a Vuex.Store in the settings object).
 *
 * @public
 * @param {Object} options
 *
 * @returns {void}
 */
LayoutManager.prototype.setVueOptions = function(options) {
	this._vueInit();

	this._vueOptions = options;
};