├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── demo ├── app.vue ├── demo-stack.vue ├── dist │ ├── app.js │ ├── favicon.ico │ └── index.html ├── favicon.ico ├── index.ejs ├── index.ts ├── layout-route.vue ├── p-head.vue ├── router.ts ├── tree.vue ├── tsconfig.json └── webpack.config.js ├── dts-bundle-plugin.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── @types │ └── vue-resize-directive.d.ts ├── colors.ts ├── gl-component.vue ├── gl-dstack.ts ├── gl-group.vue ├── gl-groups.ts ├── golden.vue ├── index.ts ├── roles │ ├── child.ts │ ├── container.ts │ ├── index.ts │ ├── item.ts │ └── link.ts ├── router │ ├── gl-component-route.ts │ ├── gl-container-route.vue │ ├── gl-route-base.ts │ ├── gl-route.vue │ ├── gl-router.vue │ └── utils.ts ├── tsconfig.json └── utils.ts └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | *.json -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:vue/essential" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "parser": "@typescript-eslint/parser", 16 | "ecmaVersion": 2018, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "legacyDecorators": true 20 | } 21 | }, 22 | "plugins": [ 23 | "vue", 24 | "@typescript-eslint" 25 | ], 26 | "rules": { 27 | "no-console": "off", 28 | "no-unused-vars": "warn" 29 | } 30 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /dist 4 | .vscode 5 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | demo 4 | test 5 | .* 6 | yalc.lock 7 | package-lock.* 8 | *.lock 9 | webpack.* 10 | coverage 11 | doc 12 | .eslint* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.10] - 2020-07-18 9 | 10 | ### Fixed 11 | 12 | - adding return value for the goldenContainer data mixin 13 | 14 | ## [2.0.9] - 2020-06-23 15 | 16 | ### Fixed 17 | 18 | - issue#59 19 | 20 | ## [2.0.8] - 2020-06-15 21 | 22 | ### Changed 23 | 24 | - Component' model becomes a generic `state.sync` that can be used on any GL object 25 | 26 | ## [2.0.7] - 2020-06-14 27 | 28 | ### Added 29 | 30 | - Component' model 31 | 32 | ## [2.0.5] - 2020-02-28 33 | 34 | ### Changed 35 | 36 | - Used packages upgrade 37 | 38 | ## [2.0.4] - 2020-02-28 39 | 40 | ### Fixed 41 | 42 | - issue #57 43 | 44 | ## [2.0.3] - 2020-02-18 45 | 46 | ### Fixed 47 | 48 | - issue #55 49 | 50 | ## [2.0.2] - 2019-11-08 51 | 52 | ### Fixed 53 | 54 | - Obsolete code removal 55 | - issue-51 56 | - `forwardEvent` calls 57 | 58 | ## [2.0.1] - 2019-10.xx 59 | 60 | ### Added 61 | 62 | - `vueComponent.focus()` 63 | 64 | ### Changed 65 | 66 | - Lots of code clean-up 67 | - Fix moving random tab in `gl-router` 68 | - Documentation rewriting 69 | - Selecting a tab in a `d-stack` now `focus()` it even if it is not in the d-stack anymore 70 | 71 | ## [2.0.0] - 2019-10.xx 72 | 73 | ### Added 74 | 75 | - Cross-windows synchronisation: one `vue` tree controls all the windows 76 | - Custom containers: allows to define custom controls who contain a part of the layout 77 | - Custom containers as route components 78 | - Branch coloring to retrieve which master goes with which slave easily 79 | 80 | ### Changed 81 | 82 | - Named template are obsolete, components are to be defined inline 83 | 84 | ## [1.5.14] - 2019-02-15 85 | 86 | ### Added 87 | 88 | - `inter-window` property 89 | 90 | ### Fixed 91 | 92 | - Templates now can access to the `VueComponent`' data 93 | - `golden-layout` and `gl-items` are not observed anymore 94 | - Security vulnerability fix 95 | 96 | ## [1.5.13] - 2019-01-15 97 | 98 | ### Fixed 99 | 100 | - Routes and custom components have a `glComponent` class 101 | 102 | ## [1.5.12] - 2019-01-09 103 | 104 | ### Fixed 105 | 106 | - Security vulnerability fix 107 | 108 | ## [1.5.10] - 2018-11-28 109 | 110 | ### Fixed 111 | 112 | - The `state` as a `Promise` is indeed a pretty neat solution. Tested and a bit corrected 113 | - Weird bug corrected with the saved state of popups (arraay-like objects not array ... ?!?) 114 | 115 | ## [1.5.9] - 2018-11-24 116 | 117 | The branch `feature/dstack` has been merged with the `gl-dstack` functionality (implemented by the `gl-router`) 118 | 119 | ### Added 120 | 121 | - The control `gl-dstack` that behaves like a `gl-stack` but remains opened in the main window when poped-out 122 | - The control `gl-route` allows the user to use the routing hacks implemented here 123 | - The golden-layout `state` can be given as a Promise 124 | - The property `reorder-enabled` that can avoid a tab to be dragged 125 | 126 | ## [1.5.8] - 2018-10-30 127 | 128 | The library has become stable enough for us to take care to keep a stable master branch. Let's get this libray from "hobby" level to "serious" one. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # vue-golden-layout 3 | 4 | [![npm](https://img.shields.io/npm/v/vue-golden-layout.svg)](https://www.npmjs.com/package/vue-golden-layout) 5 | [![Not Maintained](https://img.shields.io/badge/Maintenance%20Level-Not%20Maintained-yellow.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) 6 | 7 | _DEPRECATED_: The project is not maintained anymore. A new version for Vue3 has been begun here : https://github.com/emedware/v3-gl-ext 8 | 9 | 10 | Integration of the golden-layout to Vue 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm i -S vue-golden-layout 16 | ``` 17 | 18 | ### Fast example 19 | 20 | ```html 21 | 22 | 23 | 24 |

Component 1

25 |
26 | 27 | 28 |

Component 2

29 |
30 | 31 |

Component 3

32 |
33 |
34 |
35 |
36 | ``` 37 | 38 | Note: each component who is not rendered in a stack will indeed be rendered in a golden-layout singleton stack. 39 | 40 | ### Bigger example 41 | 42 | > Demo available [here](https://rawcdn.githack.com/emedware/vue-golden-layout/master/demo/dist/index.html) 43 | 44 | A more complex exemple is in the project when git-cloned. 45 | In order to test, the static sample application can be compiled like this: 46 | 47 | ```sh 48 | npm install 49 | npm run demo 50 | ``` 51 | 52 | You can now browse `http://localhost:9000` 53 | 54 | The example can be found in the sources under the '/demo' folder 55 | 56 | ## Usage 57 | 58 | ```javascript 59 | import vgl from 'vue-golden-layout' 60 | Vue.use(vgl); 61 | ``` 62 | 63 | In case of incompatibility with bundlers, `vue-golden-layout` can be bundeled by the sources. 64 | The sources entry point is in `vue-golden-layout/src/index.ts` 65 | 66 | ```javascript 67 | import vgl from 'vue-golden-layout/src' 68 | Vue.use(vgl); 69 | ``` 70 | 71 | ### Don't forget in order to make it work 72 | 73 | - Include a golden-layout theme CSS. 74 | 75 | ```typescript 76 | import 'golden-layout/src/css/goldenlayout-light-theme.css' 77 | ``` 78 | 79 | Available themes are `light`, `dark`, `soda`, `translucent`. 80 | 81 | `goldenlayout-base.css` is already integrated to the library. 82 | 83 | ## Structure 84 | 85 | Elements like ``, `` and `` can be represented in a tree - they respectively stand for a golden-layout row, column and stack. 86 | 87 | ### Components 88 | 89 | Component are described *by extension* - namely, by giving their content using the data from the defining component. 90 | 91 | ```html 92 | 93 |

Heydoo

94 | Price: {{priceLess}} 95 |
96 | ``` 97 | 98 | ## Saving/restoring states 99 | 100 | > TL;DR: The state is the model of the golden-layout object 101 | 102 | The `golden-layout` has a *property* and an *event* named `state`. 103 | 104 | - The event is triggered when the state has changed (even deeply, like a deep watch). 105 | - The property is used [**at mount**](https://github.com/emedware/vue-golden-layout/issues/20#issuecomment-433828678) to initialise the configuration. After that, any change will have no effect. 106 | - The `state` property can be a `Promise`, then the golden-layout will be rendered only when the `Promise` has been resolved. 107 | 108 | Notes: 109 | 110 | - The property `state` can be given minified or not 111 | - The event `state` gives indeed the minified version of the config, and the expanded version as a second argument. 112 | - It is also the `v-model` of the `golden-layout` 113 | - In order to reload a state, the Vue object structure must corresp to the state it be applied to 114 | - If there is a miss-match between the Vue object structure and the state, the `golden-layout` object `creation-error` event will be raised 115 | 116 | ### Sub-object states 117 | 118 | Every `` can have a `:state.sync` property (`Dictionary`) that will be saved along his other properties in the golden-layout state. 119 | This is a good place for example for custom containers to store locally what is needed to be persisted. 120 | 121 | ## Components events and properties 122 | 123 | ### Events 124 | 125 | #### Layout' events 126 | 127 | Straight forwards from golden-layout, refer to their doc 128 | 129 | ```javascript 130 | itemCreated 131 | stackCreated 132 | rowCreated 133 | tabCreated 134 | columnCreated 135 | componentCreated 136 | selectionChanged 137 | windowOpened 138 | windowClosed 139 | itemDestroyed 140 | initialised 141 | activeContentItemChanged 142 | ``` 143 | 144 | #### Contained objects' events 145 | 146 | Straight forwards from golden-layout, refer to their doc 147 | 148 | ```javascript 149 | stateChanged 150 | titleChanged 151 | activeContentItemChanged 152 | beforeItemDestroyed 153 | itemDestroyed 154 | itemCreated 155 | ``` 156 | 157 | #### Components' events 158 | 159 | Straight forwards from golden-layout, refer to their doc 160 | 161 | ```javascript 162 | open 163 | destroy 164 | close 165 | tab 166 | hide 167 | show 168 | resize 169 | ``` 170 | 171 | ### Properties 172 | 173 | #### Layout' properties 174 | 175 | ```typescript 176 | @Prop({default: true}) hasHeaders: boolean 177 | @Prop({default: true}) reorderEnabled: boolean 178 | @Prop({default: false}) selectionEnabled: boolean 179 | @Prop({default: true}) popoutWholeStack: boolean 180 | @Prop({default: true}) closePopoutsOnUnload: boolean 181 | @Prop({default: true}) showPopoutIcon: boolean 182 | @Prop({default: true}) showMaximiseIcon: boolean 183 | @Prop({default: true}) showCloseIcon: boolean 184 | @Prop({default: 5}) borderWidth: number 185 | @Prop({default: 10}) minItemHeight: number 186 | @Prop({default: 10}) minItemWidth: number 187 | @Prop({default: 20}) headerHeight: number 188 | @Prop({default: 300}) dragProxyWidth: number 189 | @Prop({default: 200}) dragProxyHeight: number 190 | ``` 191 | 192 | ##### `popup-timeout` 193 | 194 | (default: 5 = 5 seconds) 195 | 196 | When the state change, an event is fired and provides the new state. Unfortunately, when something is poped-out, querying the state will raise an exception if the pop-out' golden-layout is not loaded. Hence, the first call to `GoldenLayout.toConfig()` will for sure raise an exception. 197 | 198 | The policy chosen here is to then wait a bit and try again. In order to avoid infinite exception+try-again, a time-out is still specified. 199 | 200 | Therefore: 201 | 202 | - Changing this value to higher will not postpone the event fireing, it will just allow more time for the pop-out to load before raising an exception 203 | - This can be useful to increase in applications where the main page has some long loading process before displaying the golden-layout 204 | 205 | #### Contained objects' properties 206 | 207 | - `title: string`: Used for tab title 208 | - `tabId: string`: Used as the `v-model` of a `gl-stack` or `gl-dstack` to determine/set the active tab 209 | - `width: number` 210 | - `height: number` 211 | - `closable: boolean` 212 | - `reorderEnabled: boolean` 213 | - `hidden: boolean` 214 | 215 | #### Contained objects' methods 216 | 217 | - `show()` and `hide()` respectively show and hide the element 218 | - `focus()` brings the element in front recursively, making sure all tabs are right for them to be visible (also brings the window in front if needed) 219 | - `delete()` delete the vue-object and the gl-object 220 | - `nodePath` is the unique path to this node from the golden-layout root (can change). 221 | The golden-layout object has a method `getSubChild(path: string)` that returns this vue-object (useful between page reload) 222 | 223 | #### Containers 224 | 225 | Containers have an additional `color-group: boolean` property defaulted to `false`. 226 | A container for which this property is set to `true` will see all his descendants have a color assigned to their tabs. 227 | 228 | This is meant to be used when the same component can be used twice on different objects, to follow in the pop-outs which is the descendant of which. 229 | 230 | Note: by default, routes that are `glCustomContainer` have a `color-group` set to `true` 231 | 232 | ## Specific components 233 | 234 | Some components have been programmed as an extension, even if they are not part of golden-layout *proprio sensu*. 235 | 236 | ### gl-dstack 237 | 238 | *Duplicatable stacks* are stacks that should always remain in the main window as their content is modified programatically. These stacks, when poped-out, *remain* in the main screen while their content is poped-out. 239 | Components defined in it that are not `closable` nor `reorder-enabled` will *stay* in the stack in the main window. 240 | 241 | ### gl-router 242 | 243 | The router is a `glContainer` that aims to sublimate the `` 244 | It lets users manage their routes in tabs, open them in a split screen or even popped-out in another browser window on another physical display. 245 | 246 | The main usage is ``. Any options of `router-view` still has to be implemented. 247 | 248 | Note: `gl-router` is a `gl-dstack`. 249 | 250 | #### gl-router' slots 251 | 252 | A default content to render all routes can be provided as the `route` slot template - with or without scope : if a scope is queried, it will be the route object. 253 | If this content is provided, it should contain a `
` tag that will be filled with the loaded route component. 254 | 255 | Note: the provided template will be ignored when maximised/popped-out. 256 | 257 | All the components in the default slot will be added as tabs in the router. 258 | 259 | #### gl-router' properties 260 | 261 | ##### `titler` 262 | 263 | Allows to specify a function that takes a route object in parameter and gives the string that will be used as tab title. 264 | If none is specified, the default is to take `$route.meta.title` - this means that routes have to be defined with a title in their meta-data. 265 | 266 | ##### `empty-route` 267 | 268 | Specify the URL to use when the user closes all the tabs (`"/"` by default) 269 | 270 | ### gl-route 271 | 272 | `gl-route`s are components displaying a route. They are meant to be used in a gl-router but only have to be used in a golden-layout container. 273 | 274 | They can take a `name` and/or a `path`, and their `closable` and `reorder-enabled` properties are false by default. They can be forced a `title` but their container' `titler` will be used if not. 275 | 276 | Note: all the elements inside them will have a `this.$route` pointing to the given route, not the actual URL. 277 | 278 | ## glCustomContainers 279 | 280 | Users can define components who describe a part of the layout. In order to do this, instead of extending `Vue`, the component has to extend `glCustomContainer`. 281 | 282 | ```js 283 | var comp = Vue.extend({...}); 284 | // becomes 285 | var vgl = require('vue-golden-layout') 286 | var comp = vgl.glCustomContainer.extend({...}); 287 | ``` 288 | 289 | ```ts 290 | @Component 291 | export class MyComp extends Vue { 292 | ... 293 | } 294 | // becomes 295 | import { glCustomContainer } from 'vue-golden-layout' 296 | 297 | @Component 298 | export class MyComp extends glCustomContainer { 299 | ... 300 | } 301 | ``` 302 | 303 | The template' root must therefore be a proper golden-layout child (row, col, stack, ...) 304 | 305 | These components can be used as route components. 306 | 307 | ## Low-level functionalities 308 | 309 | ### Global components 310 | 311 | Some golden-layout global component can be given before any instanciation (while declaring classes) by calling this function: 312 | 313 | ```typescript 314 | import { registerGlobalComponent } from 'vue-golden-layout' 315 | // registerGlobalComponent(name: string, comp: (gl: goldenLayout, container: any, state: any)=> void) 316 | ``` 317 | 318 | `(container: any, state: any)=> void` is the signature of a gloden-layout component and they are created per golden-layout instances 319 | 320 | ### `isSubWindow` 321 | 322 | ```typescript 323 | import { isSubWindow } from 'vue-golden-layout' 324 | ``` 325 | 326 | The main application component will be created in any pop-out that is opened. The `` node will generate an empty HTML content, so nothing in it will be rendered. Though, if needed, this value is `true` when the component is generated in a pop-out which indicate that the component won't even be rendered and should take no action. 327 | 328 | ### CSS 329 | 330 | The elements with the `glComponent` CSS class are the ones directly included in the `
` controlled and sized by golden-layout and answers to this class to fit in the layout child container, that can be overridden 331 | 332 | ```css 333 | .glComponent { 334 | width: 100%; 335 | height: 100%; 336 | overflow: auto; 337 | } 338 | ``` 339 | 340 | ### Objects linking 341 | 342 | Golden-layout and Vue both have objects representing their internal state. A `glRow` is associated with a `ContentItem`. 343 | 344 | Each vue object has a `glObject` property and, vice versa, each golden-layout object has a `vueObject` property linking to each another. 345 | 346 | #### Virtual vs actual tree 347 | 348 | Vue objects (rows, components, stacks, ...) all have a `$parent` that retrieve their Vue' parent. Also their children might be retrieved with `$children`. 349 | 350 | Though, the user might change the order of things and who contain what. To retrieve the golden-layout-wise hierarchy, we can use `glParent` as well as `glChildren` on the vue objects to retrieve vue objects. 351 | -------------------------------------------------------------------------------- /demo/app.vue: -------------------------------------------------------------------------------- 1 | 20 | 39 | -------------------------------------------------------------------------------- /demo/demo-stack.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /demo/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emedware/vue-golden-layout/d5bc001fd1a7d35960f8446b5461ba09cdaef5a2/demo/dist/favicon.ico -------------------------------------------------------------------------------- /demo/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-golden-layout-demo 6 | 7 | 8 | Open devTools 9 | 10 | -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emedware/vue-golden-layout/d5bc001fd1a7d35960f8446b5461ba09cdaef5a2/demo/favicon.ico -------------------------------------------------------------------------------- /demo/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | Open devTools 9 | 10 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import vgl from 'vue-golden-layout' 3 | import 'golden-layout/src/css/goldenlayout-light-theme.css' 4 | Vue.use(vgl) 5 | 6 | import App from './app.vue' 7 | import router from './router' 8 | 9 | // If you want to debug the loading code of a popup, set this to true. 10 | // It will cause popups to wait for devtools to be opened before actually loading 11 | var debuggingPopoutLoading = false, 12 | waiter = debuggingPopoutLoading && /[?&]gl-window=/.test(window.location.search) ? 13 | new Promise(function(resolve, _) { 14 | // https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/7814241#7814241 15 | var element = new Image(); 16 | Object.defineProperty(element, 'id', { 17 | get:function() { 18 | setTimeout(resolve, 500); 19 | throw new Error("Dev tools checker"); 20 | } 21 | }); 22 | console.dir(element); 23 | }) : 24 | Promise.resolve(); 25 | 26 | async function load() { 27 | await waiter; 28 | 29 | new App({router, el: 'app'}); 30 | } 31 | load(); -------------------------------------------------------------------------------- /demo/layout-route.vue: -------------------------------------------------------------------------------- 1 | 19 | 30 | -------------------------------------------------------------------------------- /demo/p-head.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | -------------------------------------------------------------------------------- /demo/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter, { RouteConfig } from 'vue-router'; 3 | export const letters = 'abcdef'; 4 | import layoutRoute from './layout-route.vue'; 5 | const routes: RouteConfig[] = [{ 6 | name: 'spec-lr', 7 | path: '/lr', 8 | component: layoutRoute, 9 | meta: {title: 'Layout route'} 10 | }]; 11 | for(let l of letters) { 12 | let L = l.toUpperCase(); 13 | routes.push({ 14 | name: `r-${l}`, 15 | path: `/${l}`, 16 | component: {template: `

test-${L}

`}, 17 | meta: {title: `${L}-test`} 18 | }); 19 | } 20 | 21 | export default new VueRouter({ 22 | mode: 'hash', 23 | routes 24 | }) 25 | Vue.use(VueRouter); -------------------------------------------------------------------------------- /demo/tree.vue: -------------------------------------------------------------------------------- 1 | 9 | 14 | 28 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "sourceMap": true, 5 | "inlineSources": true, 6 | "noImplicitAny": false, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "lib": [ 12 | "dom", 13 | "es5", 14 | "es6" 15 | ], 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["../node_modules/@types"], 18 | "baseUrl": "./" 19 | }, 20 | "exclude": [ 21 | "**/*.spec.ts" 22 | ] 23 | } -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"), 2 | path = require("path"), 3 | HtmlWebpackPlugin = require('html-webpack-plugin'), 4 | VueLoader = require('vue-loader'); 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'eval', 8 | entry: { 9 | app: [path.resolve(__dirname, './index.ts'), 'webpack-dev-server-status-bar'] 10 | }, 11 | output: { 12 | filename: '[name].js', 13 | path: path.resolve(__dirname, "dist"), 14 | chunkFilename: "[chunkhash].js" 15 | }, 16 | plugins: [ 17 | new HtmlWebpackPlugin({ 18 | template: path.resolve(__dirname, './index.ejs'), 19 | favicon: path.resolve(__dirname, './favicon.ico'), 20 | title: 'vue-golden-layout-demo' 21 | }), 22 | new VueLoader.VueLoaderPlugin() 23 | ], 24 | module: { 25 | rules: [{ 26 | test: /\.tsx?$/, 27 | exclude: /node_modules/, 28 | loader: 'ts-loader', 29 | options: { 30 | appendTsSuffixTo: [/\.vue$/] 31 | } 32 | }, { 33 | test: /\.css$/, 34 | loader: "style-loader!css-loader" 35 | }, { 36 | enforce: 'pre', 37 | test: /\.tsx?$/, 38 | exclude: /node_modules/, 39 | use: "source-map-loader" 40 | }, { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: { 44 | loaders: { 45 | ts: 'ts-loader' 46 | } 47 | } 48 | }] 49 | }, 50 | resolve: { 51 | alias: { 52 | vue: 'vue/dist/vue.esm.js', //route are only given a template and need to be compiled client-side 53 | 'vue-golden-layout': path.resolve(__dirname, '../src/index.ts') 54 | }, 55 | extensions: [".tsx", ".ts", ".js", '.html', '.vue'] 56 | }, 57 | devServer: { 58 | contentBase: path.join(__dirname, 'dist'), 59 | compress: true, 60 | port: 9000, 61 | overlay: true, 62 | historyApiFallback: true, 63 | stats: { 64 | colors: true 65 | } 66 | } 67 | }; -------------------------------------------------------------------------------- /dts-bundle-plugin.js: -------------------------------------------------------------------------------- 1 | var { resolve } = require("path"), 2 | DtsBundle = require("dts-bundle"), 3 | { copyFileSync, readdirSync, rmdirSync } = require("fs"); 4 | 5 | module.exports = DtsBundlePlugin; 6 | function DtsBundlePlugin(options, fileEq) { 7 | Object.assign(this, {options, fileEq}); 8 | } 9 | DtsBundlePlugin.prototype.apply = function(compiler) { 10 | var {options, fileEq} = this, baseDir = resolve(__dirname, options.baseDir||''); 11 | //compiler.plugin('done', function() { 12 | compiler.hooks.afterEmit.tap('dts-bundle', function() { 13 | if(fileEq) for(let dst in fileEq) { 14 | let src = fileEq[dst]; 15 | dst = resolve(baseDir, dst); 16 | src = resolve(baseDir, src); 17 | copyFileSync(src, dst); 18 | } 19 | DtsBundle.bundle(options); 20 | // RemoveSource does not remove empty directories 21 | if(options.removeSource) { 22 | var gen = readdirSync(baseDir, {withFileTypes: true}); 23 | gen.filter(dirent=> dirent.isDirectory()) 24 | //ignore error if directory not empty - that's the point 25 | .map(dirent=> rmdirSync(resolve(baseDir, dirent.name))); 26 | } 27 | }); 28 | }; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-golden-layout", 3 | "version": "2.1.2", 4 | "description": "Integration of the golden-layout to Vue", 5 | "main": "dist/vue-golden-layout.js", 6 | "ts:main": "src/index.ts", 7 | "typings": "dist/vue-golden-layout.d.ts", 8 | "typescript": { 9 | "definition": "build/dist/vue-golden-layout.d.ts" 10 | }, 11 | "sideEffects": false, 12 | "keywords": [ 13 | "vue", 14 | "golden-layout", 15 | "golden", 16 | "layout", 17 | "integration" 18 | ], 19 | "directories": { 20 | "example": "demo", 21 | "lib": "dist" 22 | }, 23 | "scripts": { 24 | "prepack": "webpack", 25 | "build": "webpack --config demo/webpack.config.js", 26 | "demo": "webpack serve --config demo/webpack.config.js" 27 | }, 28 | "repository": "github:emedware/vue-golden-layout.git", 29 | "author": "emedware", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "@types/core-js": "^2.5.3", 33 | "@types/jquery": "^3.3.33", 34 | "@types/node": "^18.14.6", 35 | "acorn-es7-plugin": "^1.1.7", 36 | "ajv": "^6.12.6", 37 | "babel-core": "^6.26.3", 38 | "core-js": "^3.6.5", 39 | "css-loader": "^5.0.0", 40 | "dts-bundle": "^0.7.3", 41 | "golden-layout": "^1.5.9", 42 | "html-webpack-plugin": "^4.5.0", 43 | "lodash": ">=4.17.15", 44 | "source-map-loader": "^1.1.2", 45 | "style-loader": "^2.0.0", 46 | "ts-loader": "^9.1.1", 47 | "typescript": "^4.0.5", 48 | "vue": "^2.6.12", 49 | "vue-class-component": "^7.2.3", 50 | "vue-loader": "^15.9.4", 51 | "vue-property-decorator": "^9.1.2", 52 | "vue-router": "^3.1.6", 53 | "vue-storage-decorator": "^1.0.7", 54 | "vue-template-compiler": "^2.6.12", 55 | "vue-template-es2015-compiler": "^1.9.1", 56 | "webpack": "^4.44.2", 57 | "webpack-cli": "^4.1.0", 58 | "webpack-dev-server": "^4.11.1", 59 | "webpack-dev-server-status-bar": "^1.1.2", 60 | "webpack-node-externals": "^3.0.0" 61 | }, 62 | "dependencies": { 63 | "jquery": "^3.5.0", 64 | "vue-resize-directive": "^1.2.0" 65 | }, 66 | "peerDependencies": { 67 | "golden-layout": "^1.5.9", 68 | "jquery": "^3.5.0", 69 | "vue": "^2.6.12" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/emedware/vue-golden-layout/issues" 73 | }, 74 | "homepage": "https://github.com/emedware/vue-golden-layout" 75 | } 76 | -------------------------------------------------------------------------------- /src/@types/vue-resize-directive.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vue-resize-directive" { 2 | import { DirectiveOptions } from 'vue' 3 | export default DirectiveOptions; 4 | } -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | var palette: string[] = [ 2 | '#ffe0b7', '#fca570', '#fff089', '#f8f644', 3 | '#d5dc1d', '#c4f129', '#d0ffea', '#97edca', 4 | '#f1f2ff', '#c9d4fd', '#f6d896', '#fcf7be', 5 | '#ecebe7', '#e3cddf', '#dceaee', '#f8e398' 6 | ]; 7 | var allocations = new Array(palette.length).fill(false); 8 | 9 | const fallBack = '#fff'; 10 | 11 | export function allocateColor(): string { 12 | var rv = allocations.findIndex(a=>!a); 13 | if(!~rv) return fallBack; 14 | allocations[rv] = true; 15 | return palette[rv]; 16 | } 17 | 18 | export function freeColor(c: string): void { 19 | if(c===fallBack) return; 20 | var ndx = palette.indexOf(c); 21 | console.assert(~ndx, 'Specified color exists in palette'); 22 | allocations[ndx] = false; 23 | } -------------------------------------------------------------------------------- /src/gl-component.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | -------------------------------------------------------------------------------- /src/gl-dstack.ts: -------------------------------------------------------------------------------- 1 | import { Watch, Component, Prop, Emit, Model } from 'vue-property-decorator' 2 | import { glRow } from './gl-groups' 3 | import Vue from 'vue' 4 | import * as $ from 'jquery' 5 | import { goldenChild } from './roles' 6 | 7 | @Component 8 | export default class glDstack extends glRow { 9 | //TODO: `closable` should be forced true for the row, and forwarded to the created stack 10 | @Prop({default: false}) closable: boolean 11 | 12 | @Model('tab-change') activeTab: string 13 | // eslint-disable-next-line no-unused-vars 14 | @Emit() tabChange(tabId: string) { } 15 | @Watch('activeTab', {immediate: true}) progTabChange(tabId: any) { 16 | if('undefined'!== typeof tabId && null!== tabId) { 17 | for(let child of this.$children) 18 | if(child.givenTabId === tabId) { 19 | child.focus(); 20 | break; 21 | } 22 | } 23 | } 24 | 25 | get glChildrenTarget() { return this.stack; } 26 | content: any[] 27 | getChildConfig(): any { 28 | var config = (glRow).extendOptions.methods.getChildConfig.apply(this); //super is a @Component 29 | this.content = config.content.filter((x: any) => !x.isClosable && !x.reorderEnabled); 30 | config.content = [{ 31 | type: 'stack', 32 | content: config.content.slice(0) 33 | }]; 34 | return config; 35 | } 36 | initialState() { 37 | this.initStack(this.stack); 38 | } 39 | get activeContentItemChanged() { 40 | return (()=> { 41 | var vueObject = this.stack.getActiveContentItem().vueObject; 42 | if(vueObject) 43 | this.tabChange(vueObject.givenTabId); 44 | }).bind(this); 45 | } 46 | initStack(stack: any) { 47 | stack.on('activeContentItemChanged', this.activeContentItemChanged); 48 | stack.on('beforePopOut', (stack: any)=> { 49 | stack.contentItems 50 | .filter((x: any)=> !x.config.isClosable && !x.config.reorderEnabled) 51 | .forEach((comp: any, index: number)=> { 52 | stack.removeChild(comp); 53 | if(index < stack.config.activeItemIndex) 54 | --stack.config.activeItemIndex; 55 | }); 56 | }); 57 | stack.on('poppedOut', (bw: any)=> bw.on('beforePopIn', ()=> { 58 | // TODO: store the d-stack nodePath in the window config to pop-in in the right d-stack even after page reload 59 | var bwGl = bw.getGlInstance(), 60 | childConfig = $.extend(true, {}, bwGl.toConfig()).content[0], 61 | stack = this.stack; 62 | for(let item of childConfig.content) 63 | stack.addChild(item); 64 | bwGl.root.contentItems = []; 65 | })); 66 | stack.on('itemCreated', (event: any)=> { 67 | this.addAnchor(event.origin); 68 | }); 69 | } 70 | addAnchor(item: any) { 71 | if(item.parent === this.stack && !item.config.isClosable && !item.config.reorderEnabled) 72 | setTimeout(()=> { 73 | var tab = item.tab; 74 | if(tab) tab.element.append(''); 75 | }); 76 | } 77 | cachedStack: any = null 78 | get stack() { 79 | var ci = this.glObject , rv: any; 80 | if(!ci) return null; 81 | if(this.cachedStack && this.cachedStack.vueObject.glObject) 82 | return this.cachedStack; 83 | rv = ci.contentItems.find((x: any) => x.isStack); 84 | if(!rv) { 85 | ci.addChild({ 86 | type: 'stack', 87 | content: this.content.slice(0) 88 | }, 0); 89 | rv = ci.contentItems[0]; 90 | for(let item of rv.contentItems) 91 | this.addAnchor(item); 92 | this.initStack(rv); 93 | this.activeContentItemChanged(); 94 | } 95 | rv.on('destroyed', ()=> Vue.nextTick(()=> { 96 | this.cachedStack = null; 97 | this.stack; 98 | })); 99 | return this.cachedStack = rv; 100 | } 101 | 102 | @Watch('stack.vueObject.glObject') 103 | observe(obj: any) { 104 | //stacks created by the users are created without an activeItemIndex 105 | //set `activeItemIndex` observed 106 | if(obj) { 107 | var config = obj.config, aii = config.activeItemIndex; 108 | delete config.activeItemIndex; 109 | this.$set(config, 'activeItemIndex', aii); 110 | } 111 | } 112 | async created() { 113 | await this.layout.glo; 114 | this.stack; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/gl-group.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | -------------------------------------------------------------------------------- /src/gl-groups.ts: -------------------------------------------------------------------------------- 1 | import { Component, Model, Watch, Emit } from 'vue-property-decorator' 2 | import group from './gl-group.vue' 3 | 4 | // We have to re-define : ts is lost with Vue files 5 | export class glGroup extends group { 6 | closable: boolean 7 | config: any 8 | } 9 | @Component 10 | export class glRow extends glGroup { 11 | getChildConfig(): any { return { 12 | isClosable: this.closable, 13 | type: 'row', 14 | ...this.config 15 | }; } 16 | } 17 | @Component 18 | export class glCol extends glGroup { 19 | getChildConfig(): any { return { 20 | isClosable: this.closable, 21 | type: 'column', 22 | ...this.config 23 | }; } 24 | } 25 | @Component 26 | export class glStack extends glGroup { 27 | type = 'stack' 28 | @Model('tab-change') activeTab: string 29 | @Emit() tabChange(tabId: string) { } 30 | @Watch('activeTab') progTabChange(tabId: any) { 31 | for(var child of this.glChildren) 32 | if(child.givenTabId === tabId) 33 | (this).glObject.setActiveContentItem((child).container.parent); 34 | } 35 | async watchActiveIndex() { 36 | await this.layout.glo; 37 | 38 | if(this.glObject) this.glObject.on('activeContentItemChanged', (item: any)=> { 39 | var v = this.glObject.config.activeItemIndex; 40 | if('number'=== typeof v) 41 | this.tabChange(item.vueObject.givenTabId); 42 | }); 43 | } 44 | getChildConfig(): any { 45 | this.watchActiveIndex(); 46 | return { //we can use $children as it is on-load : when the user still didn't interract w/ the layout 47 | activeItemIndex: Math.max(0, (this.$children).findIndex((c: any) => c.givenTabId === this.activeTab)), 48 | isClosable: this.closable, 49 | type: 'stack', 50 | ...this.config}; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/golden.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | 355 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'golden-layout/src/css/goldenlayout-base.css' 2 | import goldenLayout from './golden.vue' 3 | export { registerGlobalComponent } from './golden.vue' 4 | import glComponent from './gl-component.vue' 5 | import { glRow, glCol, glStack } from './gl-groups' 6 | import glDstack from './gl-dstack' 7 | import glRouter from './router/gl-router.vue' 8 | import glRoute from './router/gl-route.vue' 9 | export { glCustomContainer } from './roles' 10 | export { isSubWindow } from './utils'; 11 | import Vue, { VueConstructor } from 'vue' 12 | 13 | var components : any = { 14 | goldenLayout, glComponent, 15 | glRow, glCol, glStack, glDstack, 16 | glRouter, glRoute 17 | }; 18 | 19 | export { 20 | goldenLayout, glComponent, 21 | glRow, glCol, glStack, glDstack, 22 | glRouter, glRoute 23 | } 24 | 25 | /** 26 | * Vue plugin installation 27 | */ 28 | export default { 29 | install(vue : VueConstructor) { 30 | for(let i in components) 31 | vue.component(i, components[i]); 32 | } 33 | }; -------------------------------------------------------------------------------- /src/roles/child.ts: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Watch, Inject, PropSync } from 'vue-property-decorator' 2 | import { Dictionary, xInstanceOf, statusChange } from '../utils' 3 | import { goldenContainer, goldenItem } from "./index" 4 | import goldenLayout from '../golden' 5 | 6 | @Component 7 | export class goldenChild extends goldenItem { 8 | @Inject() layout: goldenLayout 9 | @Inject({from: 'groupColor'}) belongGroupColor: string 10 | @Prop() width: number 11 | @Prop() height: number 12 | @Watch('width') reWidth(w:number) { this.container && this.container.setSize(w, false); } 13 | @Watch('height') reHeight(h:number) { this.container && this.container.setSize(false, h); } 14 | $parent: goldenContainer 15 | getChildConfig(): any { return null; } 16 | get glParent() { return this.glObject.parent.vueObject; } 17 | /** 18 | * Gets the Vue container that is not a component definition and therefore actually contains this 19 | */ 20 | get vueParent(): goldenContainer { 21 | return this.$parent.parentMe; 22 | } 23 | 24 | get definedVueComponent(): goldenContainer { 25 | return this.$parent.definedVueComponent; 26 | } 27 | container: any = null; 28 | 29 | @Prop() tabId: string 30 | get givenTabId() { return this.givenProp('tabId'); } 31 | @Prop() title: string 32 | @Watch('title') setTitle(title: any): void { 33 | if(this.container) this.container.setTitle(title); 34 | } 35 | 36 | givenProp(prop: string): any { 37 | var itr: any = this; 38 | while(!itr[prop] && xInstanceOf(itr.$parent, 'glCustomContainer')) 39 | itr = itr.$parent; 40 | return itr[prop]; 41 | } 42 | 43 | rootProp(prop: string): any { 44 | var itr: any = this, rv = itr[prop]; 45 | while(xInstanceOf(itr.$parent, 'glCustomContainer')) { 46 | itr = itr.$parent; 47 | if(prop in itr) rv = itr[prop]; 48 | } 49 | return rv; 50 | } 51 | 52 | get tabColor(): string|null { 53 | return this.belongGroupColor; 54 | } 55 | 56 | hide() { this.container && this.container.hide(); } 57 | show() { this.container && this.container.show(); } 58 | shouldFocus: boolean 59 | focus() { 60 | var brwsr = this.childMe.glObject, doc; 61 | if(brwsr) { 62 | // TODO: cfr layout.selectionEnabled 63 | this.show(); 64 | for(; !brwsr.isRoot; brwsr = brwsr.parent) { 65 | if(brwsr.parent.isStack) 66 | brwsr.parent.setActiveContentItem(brwsr); 67 | } 68 | doc = brwsr.layoutManager.container[0].ownerDocument; 69 | (doc.defaultView || doc.parentWindow).focus(); 70 | } else 71 | this.shouldFocus = true; 72 | } 73 | @Watch('glObject') glObjectSet(v:boolean) { 74 | if(!v) this.delete(); 75 | else if(this.shouldFocus) { 76 | this.shouldFocus = false; 77 | this.focus(); 78 | } 79 | } 80 | 81 | @Prop({default: false}) hidden: boolean 82 | 83 | @Watch('container') 84 | @Watch('hidden') 85 | setContainer() { 86 | if(this.glObject) { 87 | var parent = this.glObject.parent; 88 | this.container && ( 89 | !parent.isStack || 90 | parent.contentItems[parent.config.activeItemIndex] === this.glObject 91 | ) && this.container[this.hidden ? "hide" : "show"](); 92 | } 93 | } 94 | 95 | @Prop({default: true}) closable: boolean 96 | @Prop({default: true}) reorderEnabled: boolean 97 | _isDestroyed?: boolean 98 | delete() { 99 | if(!statusChange.unloading && !this._isDestroyed) { // If unloading, it might persist corrupted data 100 | this.$parent.computeChildrenPath() 101 | //this.$emit('destroy', this); //Already emited as a forward-event. 102 | this.$destroy(); 103 | } 104 | } 105 | created() { 106 | if(!this.vueParent.addGlChild) 107 | throw new Error('gl-child can only appear directly in a golden-layout container'); 108 | } 109 | 110 | // Don't remove: goldenItem is weirdly inherited in popouts 111 | get childMe(): goldenChild { return this; } 112 | // Defined when this is a pop-out mirror component 113 | get nodePath() { 114 | return this.vueParent.childPath(this.childMe); 115 | } 116 | 117 | // State object available to vue objects 118 | @PropSync('state', {default: ()=> ({})}) syncedState: Dictionary 119 | @Watch('state', {deep: true}) innerStateChanged() { 120 | if(this.glObject) 121 | this.glObject.emitBubblingEvent('stateChanged'); 122 | } 123 | 124 | mounted() { 125 | var dimensions: any = {}; 126 | if(undefined!== this.width) dimensions.width = this.width; 127 | if(undefined!== this.height) dimensions.height = this.height; 128 | let childConfig: any = this.getChildConfig(); 129 | if(childConfig) //glCustomContainer shouldn't mount as their child is already mounted in the vueParent 130 | this.vueParent.addGlChild({ 131 | ...dimensions, 132 | ...childConfig, 133 | isClosable: this.rootProp('closable'), 134 | reorderEnabled: this.rootProp('reorderEnabled'), 135 | title: childConfig.title||this.givenProp('title'), 136 | vue: this.nodePath, 137 | componentState: this.syncedState 138 | }, this); 139 | } 140 | destroyed() { 141 | if(this.glObject && this.glObject.parent && ~this.glObject.parent.contentItems.indexOf(this.glObject)) 142 | this.glObject.parent.removeChild(this.glObject); 143 | } 144 | } -------------------------------------------------------------------------------- /src/roles/container.ts: -------------------------------------------------------------------------------- 1 | import { Component, Provide, Prop } from 'vue-property-decorator' 2 | import { goldenChild, goldenItem } from "./index" 3 | import { allocateColor, freeColor } from '../colors' 4 | import { genericTemplate } from '../golden.vue' 5 | 6 | @Component({mixins: [{ 7 | data(vm: any) { 8 | if(vm.colorGroup) 9 | vm.groupColor = allocateColor(); 10 | else if(vm.belongGroupColor) 11 | vm.groupColor = vm.belongGroupColor; 12 | return {} 13 | } 14 | }]}) 15 | export class goldenContainer extends goldenItem { 16 | @Provide() groupColor: string|null 17 | @Prop({default: false}) colorGroup: boolean 18 | get definedVueComponent(): goldenContainer { throw 'Not overriden'; } 19 | config: any = { 20 | content: [] 21 | } 22 | // Hack to force child-path re-computation 23 | watchComputeChildrenPath: number = 0 24 | computeChildrenPath() { ++this.watchComputeChildrenPath; } 25 | childPath(comp: goldenChild): string { 26 | this.computeChildrenPath(); 27 | var rv = this.childMe.nodePath?`${this.childMe.nodePath}.`:''; 28 | var ndx = this.vueChildren().indexOf(comp); 29 | console.assert(!!~ndx, 'Child exists'); 30 | return rv+ndx; 31 | } 32 | getChild(path: string): goldenChild { 33 | var nrs = path.split('.'); 34 | var ndx_string : string | undefined = nrs.shift(); 35 | 36 | if (ndx_string === undefined) { 37 | throw "Invalid operation"; 38 | } 39 | let ndx= parseInt(ndx_string); 40 | var next = this.vueChild(ndx); 41 | console.assert(next !== undefined && next !== null, "Vue structure correspond to loaded GL configuration"); 42 | return nrs.length ? (next).getChild(nrs.join('.')) : next; 43 | } 44 | //In order to be overriden 45 | get glChildrenTarget() { return this.glObject; } 46 | 47 | addGlChild(child : any, comp : any) { 48 | if(comp && 'component'=== child.type) { 49 | if(!child.componentName) 50 | child.componentName = genericTemplate; 51 | if(!child.componentState) 52 | child.componentState = {}; 53 | } 54 | var ci = this.glChildrenTarget; 55 | if(ci) 56 | ci.addChild(child); 57 | else 58 | this.config.content.push(child); 59 | } 60 | get glChildren(): goldenChild[] { 61 | return this.glObject.contentItems.map((x : any)=> x.vueObject); 62 | } 63 | vueChild(child: number): goldenChild { 64 | return (this.$children[child]).childMe; 65 | } 66 | /** 67 | * Get the list of Vue children and not their definition abstract component 68 | */ 69 | vueChildren(): goldenChild[] { 70 | return this.$children.map(comp=> (comp).childMe).filter(x=> x instanceof goldenItem); 71 | } 72 | destroyed() { 73 | if(this.groupColor) 74 | freeColor(this.groupColor); 75 | } 76 | } -------------------------------------------------------------------------------- /src/roles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './item' 2 | export * from './container' 3 | export * from './child' 4 | export * from './link' 5 | 6 | -------------------------------------------------------------------------------- /src/roles/item.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import goldenLayout from "../golden" 3 | import { goldenContainer, goldenChild } from "./index" 4 | 5 | export class goldenItem extends Vue { 6 | glObject: any = null 7 | layout: goldenLayout 8 | // To be overriden 9 | get childMe() { return this; } 10 | get parentMe() { return this; } 11 | get nodePath(): string { return ''; } 12 | } -------------------------------------------------------------------------------- /src/roles/link.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue-property-decorator' 2 | import { goldenContainer } from './container' 3 | import { goldenChild } from "./child" 4 | import { goldenItem } from "./item" 5 | import goldenLayout from "../golden" 6 | 7 | @Component({mixins: [goldenContainer]}) 8 | export class goldenLink extends goldenChild implements goldenContainer { 9 | // declaration of goldenContainer properties 10 | destroyed: ()=> void 11 | config: any 12 | layout: goldenLayout 13 | childPath:(comp: goldenChild)=> string 14 | getChild: (path: string)=> goldenChild 15 | //readonly glChildrenTarget: any 16 | get glChildrenTarget() { return this.glObject; } 17 | addGlChild: (child : any, comp : any)=> void 18 | readonly glChildren: goldenChild[] 19 | vueChild: (child: number)=> goldenChild 20 | vueChildren: ()=> goldenChild[] 21 | groupColor: string|null 22 | colorGroup: boolean 23 | 24 | watchComputeChildrenPath: number = 0 25 | computeChildrenPath: ()=> void 26 | 27 | get tabColor(): string|null { 28 | return this.belongGroupColor || this.groupColor; 29 | } 30 | } 31 | 32 | @Component 33 | export class glCustomContainer extends goldenLink { 34 | constructor() { 35 | super(); 36 | this.destructor = this.delete.bind(this); 37 | } 38 | get definedVueComponent(): goldenContainer { return this; } 39 | cachedChildMe: goldenChild 40 | destructor: any 41 | get childMe() { 42 | var sub = this.$children[0], rv = sub && sub.childMe; 43 | 44 | if(this.cachedChildMe) this.cachedChildMe.$off('destroy', this.destructor); 45 | if(rv) rv.$on('destroy', this.destructor); 46 | return rv; 47 | } 48 | get parentMe() { 49 | return this.vueParent; 50 | } 51 | getChildConfig(): any { return null; } 52 | } -------------------------------------------------------------------------------- /src/router/gl-component-route.ts: -------------------------------------------------------------------------------- 1 | import { Watch, Component, Prop, Emit, Provide, Inject } from 'vue-property-decorator' 2 | import { goldenChild } from '../roles' 3 | import { defaultTitler, RouteComponentName } from './utils' 4 | import glRouteBase from './gl-route-base' 5 | import Vue from 'vue' 6 | @Component 7 | export default class glComponentRoute extends glRouteBase { 8 | @Prop() component: typeof Vue 9 | getChildConfig(): any { 10 | return { 11 | type: 'component', 12 | title: this.compTitle, 13 | isClosable: this.closable, 14 | reorderEnabled: this.reorderEnabled, 15 | componentName: RouteComponentName, 16 | componentState: this.location 17 | }; 18 | } 19 | render(v : any) {} 20 | } 21 | -------------------------------------------------------------------------------- /src/router/gl-container-route.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/router/gl-route-base.ts: -------------------------------------------------------------------------------- 1 | import { Watch, Component, Prop, Inject } from 'vue-property-decorator' 2 | import { goldenLink } from '../roles' 3 | import { defaultTitler } from './utils' 4 | import VueRouter, { Location, Route } from 'vue-router'; 5 | 6 | // TODO 2 route-base created for each route 7 | @Component 8 | export default class glRouteBase extends goldenLink { 9 | $router: VueRouter 10 | $route: Route 11 | @Prop() path: string 12 | @Prop() name: string 13 | @Prop({default: false}) closable: boolean 14 | @Prop({default: false}) reorderEnabled: boolean 15 | @Inject() titler: (route: any)=> string 16 | @Inject() _glRouter: any 17 | @Prop() title: string 18 | get compTitle(): string { 19 | return this.title || 20 | (this.titler.bind(this)||defaultTitler)(this.$router.resolve(this.location).route); 21 | } 22 | 23 | get location(): Location { 24 | console.assert(!!this.name || !!this.path, 'At least one route specification - `name` or `path` is given.'); 25 | if(!this.syncedState.path && !this.syncedState.name) { 26 | // TODO Find out why this is necessary 27 | this.syncedState.name = this.name; 28 | this.syncedState.path = this.path; 29 | } 30 | var rv: Location = {}; 31 | if(this.name) rv.name = this.name; 32 | if(this.path) rv.path = this.path; 33 | return rv; 34 | } 35 | 36 | @Watch('compTitle') setTitle(title: any) { 37 | if(this.container) this.container.setTitle(title); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/router/gl-route.vue: -------------------------------------------------------------------------------- 1 | 12 | 14 | -------------------------------------------------------------------------------- /src/router/gl-router.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/router/utils.ts: -------------------------------------------------------------------------------- 1 | import { Route } from 'vue-router' 2 | import goldenLayout, { registerGlobalComponent } from '../golden.vue' 3 | import { Dictionary, xInstanceOf } from '../utils' 4 | import { goldenItem } from '../roles' 5 | import Vue, { ComponentOptions, Component, AsyncComponent, VueConstructor } from 'vue' 6 | 7 | export function defaultTitler(route: Route): string { 8 | //The last case is to warn the programmer who would have forgotten that detail 9 | return route ? ((route.meta && route.meta.title) || 'set $route.meta.title') : ''; 10 | } 11 | 12 | export const RouteComponentName = '$router-route'; 13 | 14 | function freezeValue(object: Dictionary, path: string, value?: any) { 15 | const props = path.split('.'), 16 | forced = props.pop()!; 17 | for(let property of props) 18 | Object.defineProperty(object, property, { 19 | value: object = Object.create(object[property]), 20 | writable: false 21 | }); 22 | Object.defineProperty(object, forced, { 23 | value, 24 | writable: false 25 | }); 26 | } 27 | 28 | export function freezeRoute(component: Vue, route: Route) { 29 | //Simulate a _routerRoot object so that all children have a $route object set to this route object 30 | var routerRoot = (component)._routerRoot = Object.create((component)._routerRoot); 31 | freezeValue(routerRoot, '_route', route); 32 | freezeValue(routerRoot, '_router.history.current', route); 33 | } 34 | interface RouterSpec { 35 | template: any 36 | parent: any 37 | } 38 | function routeParent(parent: any, route: Route): RouterSpec { 39 | var template: any; 40 | if(parent._glRouter) 41 | template = parent.$scopedSlots.route ? 42 | parent.$scopedSlots.route(route) : 43 | parent.$slots.route; 44 | return {template, parent}; 45 | } 46 | 47 | type ComponentSpec = Component | AsyncComponent; 48 | 49 | async function vueComponent(comp: ComponentSpec|string, namedComponents: Dictionary): Promise { 50 | var component: ComponentSpec = 'string'=== typeof comp?namedComponents[comp]:comp; 51 | function componentIsVueConstructor() { return (component).prototype instanceof Vue; } 52 | console.assert(`Component registered : "${comp}".`); 53 | if('function'=== typeof component && !componentIsVueConstructor) 54 | //AsyncComponentFactory | FunctionalComponentOptions> 55 | component = (<()=> ComponentSpec>component)(); 56 | if(component instanceof Promise) 57 | component = await (>component); 58 | return componentIsVueConstructor() ? 59 | component : 60 | Vue.extend(>component); 61 | } 62 | 63 | function createRouteComponent(comp: VueConstructor, routerSpec: RouterSpec, route: Route) : Vue { 64 | const {parent, template} = routerSpec; 65 | var itr; 66 | for(itr = comp; itr && itr != goldenItem; itr = (itr).super); 67 | if(itr) { 68 | return new comp({ 69 | parent, 70 | propsData: route 71 | }); 72 | } 73 | const component = template ? new Vue({ 74 | render(ce) { 75 | // `instanceof Array` fails in popouts: `template` is a `window.opener.Array` then 76 | return xInstanceOf(template, 'Array') ? 77 | ce('div', {class: 'glComponent'}, template) : 78 | template; 79 | }, 80 | mounted() { 81 | new comp({ 82 | el: component.$el.querySelector('main') || undefined, 83 | parent: component 84 | }); 85 | }, 86 | parent 87 | }) : new comp({parent}); 88 | return component; 89 | } 90 | 91 | function renderInContainer(container: any, component: Vue) { 92 | //TODO: document why we don't use simply component.$mount(container.getElement()); 93 | var el = document.createElement('div'); 94 | container.getElement().append(el); 95 | component.$mount(el); 96 | } 97 | 98 | export async function getRouteComponent(gl: goldenLayout, router: any, path: string) { 99 | var route = gl.$router.resolve({path}).route, 100 | compSpec = gl.$router.getMatchedComponents({path})[0], 101 | component: Vue; 102 | 103 | console.assert(compSpec, `Path resolves to a component: ${path}`); 104 | component = createRouteComponent( 105 | await vueComponent(compSpec!, gl.$options.components || {}), 106 | routeParent(router, route), route); 107 | //freezeRoute(component, route); 108 | return component; 109 | } 110 | 111 | async function renderRoute(gl: goldenLayout, container: any, state: any) { 112 | var parent = container.parent, _glRouter = parent.vueObject._glRouter; 113 | if(_glRouter) parent = _glRouter; 114 | else { 115 | while(!parent.vueObject || !parent.vueObject._isVue) parent = parent.parent; 116 | parent = parent.vueObject; 117 | } 118 | renderInContainer(container, 119 | await getRouteComponent(gl, parent, state.path)); 120 | } 121 | 122 | export function UsingRoutes(target: any) { //This function should be called once and only once if we are using the router 123 | registerGlobalComponent(RouteComponentName, renderRoute); 124 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dist", 4 | "sourceMap": true, 5 | "declaration": true, 6 | "strict": true, 7 | "strictPropertyInitialization":false, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "target": "es5", 13 | "lib": [ 14 | "dom", 15 | "es5", 16 | "es6" 17 | ], 18 | "allowSyntheticDefaultImports": true, 19 | "typeRoots": ["../node_modules/@types"], 20 | "baseUrl": "./" 21 | }, 22 | "exclude": [ 23 | "**/*.spec.ts" 24 | ] 25 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const isSubWindow = /[?&]gl-window=/.test(window.location.search); 2 | import { goldenChild } from "./roles"; 3 | export type Dictionary = {[key: string]: T} 4 | import * as GoldenLayout from 'golden-layout' 5 | import * as $ from 'jquery' 6 | 7 | export function newSemaphore(): Semaphore { 8 | var access, rv = new Promise(function(resolve, reject) { 9 | access = {resolve, reject}; 10 | }); 11 | Object.assign(rv, access); 12 | return >rv; 13 | } 14 | 15 | export interface Semaphore extends Promise { 16 | resolve: (arg?: T)=> void; 17 | reject: (arg?: T)=> void; 18 | } 19 | 20 | 21 | const lm = (GoldenLayout).__lm; 22 | 23 | /** 24 | * Equivalent of `obj instanceof name` but accepting cross-windows classes. 25 | * @example 26 | * A popup and the main window both have an `Array` class defined - and they are different 27 | * Therefore `x instanceof Array` will return false if the Array class is from the other window 28 | */ 29 | export function xInstanceOf(obj: any, name: string) { 30 | var browser = obj.constructor; 31 | while(browser.name !== name && browser.super) 32 | browser = browser.super; 33 | return browser.name === name; 34 | } 35 | 36 | export function localWindow(obj: any): any { 37 | if(!obj || 'object'!= typeof obj) 38 | return obj; 39 | var rv: Dictionary = xInstanceOf(obj, 'Array') ? [] : {}; 40 | for(let i in obj) rv[i] = localWindow(obj[i]); 41 | return rv; 42 | } 43 | 44 | export const statusChange = { 45 | poppingOut: false, 46 | poppingIn: false, 47 | unloading: false 48 | }; 49 | 50 | // hook `createPopout` to give objects instead of destroying then on-destroy 51 | const oldCreatePopout = lm.LayoutManager.prototype.createPopout; 52 | lm.LayoutManager.prototype.createPopout = function(item: any) { 53 | var rv: any; 54 | statusChange.poppingOut = true; 55 | try { 56 | item.emit && item.emit('beforePopOut', item); 57 | if(!(item.contentItems || item[0].content).length) return null; 58 | rv = oldCreatePopout.apply(this, arguments); 59 | } finally { 60 | statusChange.poppingOut = false; 61 | } 62 | item.emit && item.emit('poppedOut', rv); 63 | if(item[0]) item = item[0]; 64 | var rootPaths: Dictionary = {}, gl = this.vueObject; 65 | function ref(path: string) { rootPaths[path] = gl.getChild(path); } 66 | if(item.content) { //config 67 | if(item.vue) ref(item.vue); 68 | for(let i=0; item.content[i]; ++i) 69 | ref(item.content[i].vue); 70 | } else { //item 71 | var obj = item.vueObject; 72 | if(obj && obj.nodePath) rootPaths[obj.nodePath] = obj; 73 | for(let sub of item.contentItems) { 74 | obj = sub.vueObject; 75 | rootPaths[obj.nodePath] = obj; 76 | } 77 | } 78 | var ppWindow = rv.getWindow(); 79 | ppWindow.poppedoutVue = { 80 | layout: gl, 81 | path: rootPaths 82 | }; 83 | ppWindow.addEventListener('beforeunload', ()=> { 84 | if(!rv.poppedIn) for(let p in rootPaths) rootPaths[p].delete(); 85 | }); 86 | rv.on('initialised', ()=> { 87 | var ppGl = rv.getGlInstance(), emptyCheck: ReturnType|null = null; 88 | //Automatically closes the window when there is no more tabs 89 | ppGl.on('itemDestroyed', ()=> { 90 | if(!emptyCheck) emptyCheck = setTimeout(()=> { 91 | emptyCheck = null; 92 | if(!ppGl.root.contentItems.length) 93 | ppWindow.close(); 94 | }); 95 | }); 96 | }); 97 | return rv; 98 | } 99 | 100 | const bp = lm.controls.BrowserPopout.prototype; 101 | 102 | // hook `createPopout` to give objects instead of destroying then on-destroy 103 | const oldPopIn = bp.popIn; 104 | bp.popIn = function() { 105 | var rv; 106 | statusChange.poppingIn = true; 107 | // GL bug-fix: poping-in empty window 108 | try { 109 | this.emit('beforePopIn'); 110 | this.poppedIn = true; 111 | rv = this.getGlInstance().root.contentItems.length ? 112 | oldPopIn.apply(this, arguments) : 113 | this.close(); 114 | } finally { 115 | statusChange.poppingIn = false; 116 | } 117 | return rv; 118 | } 119 | 120 | window.addEventListener('beforeunload', ()=> { statusChange.unloading = true; }); 121 | 122 | /** 123 | * Determine if the user is dradding a tab 124 | */ 125 | export function isDragging(): boolean { 126 | return $('body').hasClass('lm_dragging'); 127 | } 128 | function enumerable(e: boolean) { 129 | //https://stackoverflow.com/questions/40930251/how-to-create-a-typescript-enumerablefalse-decorator-for-a-property 130 | const rv: { 131 | (target: any, name: string): void; 132 | (target: any, name: string, desc: PropertyDescriptor): PropertyDescriptor; 133 | } = (target: any, name: string, desc?: any) => { 134 | if(desc) { 135 | desc.enumerable = e; 136 | return desc; 137 | } 138 | Object.defineProperty(target, name, { 139 | set(value) { 140 | Object.defineProperty(this, name, { 141 | value, enumerable: e, writable: true, configurable: true, 142 | }); 143 | }, 144 | enumerable: e, 145 | configurable: true, 146 | }); 147 | }; 148 | return rv; 149 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"), 2 | externals = require("webpack-node-externals"), 3 | DtsBundlePlugin = require("./dts-bundle-plugin"), 4 | VueLoader = require("vue-loader"); 5 | 6 | module.exports = { 7 | mode: "development", //This is meant to be bundled afterward anyway 8 | context: path.resolve(__dirname, "src"), 9 | entry: { 10 | "vue-golden-layout": "./index.ts", 11 | }, 12 | output: { 13 | filename: "[name].js", 14 | path: path.resolve(__dirname, "dist"), 15 | libraryTarget: "umd", 16 | library: "vue-golden-layout", 17 | umdNamedDefine: true 18 | }, 19 | plugins: [ 20 | new DtsBundlePlugin({ 21 | name: "vue-golden-layout", 22 | main: "dist/index.d.ts", 23 | baseDir: 'dist', 24 | out: "vue-golden-layout.d.ts", 25 | removeSource: true 26 | }, { 27 | "golden.d.ts": "golden.vue.d.ts", 28 | "utils.js.d.ts": "utils.d.ts" 29 | }), 30 | new VueLoader.VueLoaderPlugin() 31 | ], 32 | externals: [ 33 | externals() 34 | ], 35 | devtool: "source-map", 36 | module: { 37 | rules: [{ 38 | test: /\.tsx?$/, 39 | exclude: /node_modules/, 40 | loader: "ts-loader", 41 | options: { 42 | appendTsSuffixTo: [/\.vue$/] 43 | } 44 | }, { 45 | test: /\.css$/, 46 | loader: "style-loader!css-loader" 47 | }, { 48 | enforce: "pre", 49 | test: /\.tsx?$/, 50 | exclude: /node_modules/, 51 | use: "source-map-loader" 52 | }, { 53 | test: /\.vue$/, 54 | loader: "vue-loader", 55 | options: { 56 | loaders: { 57 | ts: "ts-loader" 58 | } 59 | } 60 | }] 61 | }, 62 | resolve: { 63 | extensions: [".tsx", ".ts", ".js", ".html", ".vue"] 64 | } 65 | }; --------------------------------------------------------------------------------