├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.CN.md ├── README.md ├── babel.config.js ├── demo.jpg ├── demo.light.jpg ├── examples ├── App.vue ├── css │ ├── iconfont.css │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── index.html ├── main.js ├── router │ └── index.ts ├── shims-vue.d.ts └── views │ ├── DockLayoutBaseTest.vue │ ├── DockLayoutCustomTest.vue │ ├── DockLayoutDataTest.vue │ ├── DockLayoutExtraTest.vue │ ├── DockLayoutTests.vue │ └── DockLayoutThemeTest.vue ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── src ├── DockCommonStyles.scss ├── DockDropLayout.vue ├── DockHostData.ts ├── DockInstance.ts ├── DockLayout.vue ├── DockLayoutData.ts ├── DockPanelDefaultCloseDark.svg ├── DockPanelDefaultCloseLight.svg ├── DockPanelDefaultTab.vue ├── DockPanelHost.vue ├── DockSplit.vue ├── DockUtils.ts ├── DockVector.ts ├── DynamicRender.vue └── vue-shim.d.ts ├── tsconfig.json └── vue.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true 6 | }, 7 | 8 | 'extends': [ 9 | 'plugin:vue/vue3-essential', 10 | 'eslint:recommended', 11 | '@vue/typescript/recommended' 12 | ], 13 | 14 | parserOptions: { 15 | ecmaVersion: 2020 16 | }, 17 | 18 | rules: { 19 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 21 | }, 22 | 23 | overrides: [ 24 | { 25 | files: [ 26 | '**/__tests__/*.{j,t}s?(x)', 27 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 28 | ], 29 | env: { 30 | mocha: true 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2023/05/20 4 | 5 | Official release. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 梦欤 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.CN.md: -------------------------------------------------------------------------------- 1 | vue-dock-layout 2 | --- 3 | 4 | 一个Vue3的可拖拽网格布局组件(类似Visual studio) 5 | 6 | ![Screenshot](./demo.jpg) 7 | 8 | --- 9 | 10 | [查看在线示例](https://imengyu.top/pages/vue-dock-layout-demo/) 11 | 12 | **注:此项目已不再开发中。我们建议使用[vue-code-layout](https://github.com/imengyu/vue-code-layout)** 13 | 14 | ## 特性 15 | 16 | * 体积小,易用 17 | * 可自定义 18 | 19 | ### 安装 20 | 21 | ``` 22 | npm install -save @imengyu/vue-dock-layout 23 | ``` 24 | 25 | ## 使用方法 26 | 27 | ### 先导入 28 | 29 | ```ts 30 | import { DockLayout, DockLayoutInterface } from '@imengyu/vue-dock-layout'; 31 | ``` 32 | 33 | ### 制作布局 34 | 35 | 要使用 vue-dock-layout ,你需要先在您的界面中添加一个容器组件,这是您的应用的内容承载区域。 36 | 37 | ```html 38 | 57 | ``` 58 | 59 | 布局组件的布局是以网格为布局方式的,每个分割区域为一个网格,网格中嵌入您的自定义内容。 60 | 61 | 组件提供了一些接口,允许您以编程方式快速设置界面布局: 62 | 63 | ```ts 64 | const dockLayout = ref(); 65 | 66 | onMounted(() => { 67 | nextTick(() => { 68 | //这里先设置界面布局 69 | //这里先添加了一个横向布局,中有三个区域,left/center/right,宽度占比为20%:60%:20% 70 | //然后left区域又被分割为了leftA/leftB两个区域,宽度为50%:50% 71 | dockLayout.value?.setData({ 72 | name: 'root', 73 | size: 0, 74 | grids: [ 75 | { 76 | size: 20, 77 | name: 'left', 78 | grids: [ 79 | { 80 | size: 50, 81 | name: 'leftA', 82 | }, 83 | { 84 | size: 50, 85 | name: 'leftB', 86 | }, 87 | ] 88 | }, 89 | { 90 | size: 60, 91 | name: 'center', 92 | //这里设置了中心区域没有面板时不会被自动移除 93 | alwaysVisible: true, 94 | }, 95 | { 96 | size: 20, 97 | name: 'right', 98 | }, 99 | ], 100 | }); 101 | //下方代码向网格添加内容面板 102 | //每个内容面板以key作为标识符,在 DockLayout 的 panelRender 插槽中可以从 panel 参数中读取。 103 | dockLayout.value?.addPanels([ 104 | { 105 | key: 'panel1', 106 | title: 'panel1', 107 | }, 108 | { 109 | key: 'panel2', 110 | title: 'panel2', 111 | }, 112 | ], 'leftA'); 113 | dockLayout.value?.addPanels([ 114 | { 115 | key: 'panel3', 116 | title: 'panel3', 117 | }, 118 | ], 'leftB'); 119 | }) 120 | }); 121 | ``` 122 | 123 | 以上示例可以在 [在线示例](https://imengyu.top/pages/vue-dock-layout-demo/) 中的第一个示例找到。 124 | 125 | 用户可以自定义拖拽界面。因此在程序离开时,如果您需要保存用户的自定义设置,可读取当前网格布局数据, 126 | 在下次程序加载时设置到组件中。 127 | 128 | ```js 129 | onBeforeUnmount(() => { 130 | const layoutData = dockLayout.value?.getSaveData() 131 | //Save layoutData to anywhere... 132 | 133 | //Next time, load and set to dockLayout 134 | dockLayout.value?.setData(layoutData); 135 | }) 136 | ``` 137 | 138 | ### 自定义 139 | 140 | #### 主题 141 | 142 | 组件默认提供了 亮色(`light`)与 暗色 (`dark`)两个主题供您使用,主题可以使用 `DockLayout` 组件的 `theme` 属性指定。 143 | 144 | ```html 145 | 146 | ... 147 | 148 | ``` 149 | 150 | 两个主题效果如下图所示: 151 | 152 | |light|dark| 153 | |---|---| 154 | |![Screenshot](./demo.light.jpg)|![Screenshot](./demo.jpg)| 155 | 156 | #### 自定义渲染 157 | 158 | 组件提供了一些位置的渲染插槽,你可以进行自定义渲染。 159 | 160 | 具体示例和源码请[查看在线示例](https://imengyu.top/pages/vue-dock-layout-demo/#/DockLayoutThemeTest) 161 | 162 | ##### tabItemRender 163 | 164 | 用于面板标题的自定义渲染。 165 | 166 | ```vue 167 | 168 | 179 | 180 | ``` 181 | 182 | ##### emptyPanel 183 | 184 | 用于渲染面板没有内容面板时显示的底板。 185 | 186 | ```vue 187 | 188 | 194 | 195 | ``` 196 | 197 | ## API 参考 198 | 199 | DockLayout 是布局组件的主要容器。 200 | 201 | ##### Props 202 | 203 | | 属性 | 描述 | 类型 | 默认值 | 204 | | :----: | :----: | :----: | :----: | 205 | | tabHeight | Tab组件的高度,用于相关计算 | `number` | 40 | 206 | | startVerticalDirection | 第一个布局网格是不是垂直的 | `boolean` | `false` | 207 | | allowFloatWindow | 是否允许浮动窗口 | `boolean` | `false` | 208 | | theme | 主题,可选 'light', 'dark' | `string` | `dark` | 209 | 210 | ##### Events 211 | 212 | | 事件名 | 描述 | 参数 | 213 | | :----: | :----: | :----: | 214 | | active-tab-change | 当激活的内容面板更改时触发事件 | `currentActive : DockPanel|null`,`lastActive : DockPanel|null` | 215 | | tab-closed | 内容面板关闭时触发此事件 | `panel: DockPanel` | 216 | 217 | ##### Slots 218 | 219 | | 插槽名 | 描述 | 参数 | 220 | | :----: | :----: | :----: | 221 | | emptyPanel | 用于面板标题的自定义渲染 | { name: string, dockData: DockData } | 222 | | tabItemRender | 用于渲染网格没有内容面板时显示的底板 | DockTabItemRenderData | 223 | 224 | ##### 方法 225 | 226 | ```ts 227 | //使用ts时,可以使用 DockLayoutInterface 来获得代码提示 228 | const dockLayout = ref(); 229 | ``` 230 | ###### `getSaveData() : IDockGrid` 231 | 232 | 说明: 233 | 234 | 获取当前界面网格布局数据 235 | 236 | 返回值: 237 | 238 | 界面网格布局数据 239 | 240 | ###### `setData(data: IDockGrid) : void` 241 | 242 | 说明: 243 | 244 | 设置界面网格布局数据 245 | 246 | | 参数 | 描述 | 247 | | :----: | :----: | 248 | | data | 网格数据 | 249 | 250 | ###### `updatePanel(panel: IDockPanel) : void` 251 | 252 | 说明: 253 | 254 | 更新面板实例的属性 255 | 256 | | 参数 | 描述 | 257 | | :----: | :----: | 258 | | panel | 面板 | 259 | 260 | ###### `addPanel(panel: IDockPanel, insertTo?: string|DockData) : void` 261 | 262 | 说明: 263 | 264 | 向容器内插入面板。 265 | 266 | * 注意:如果面板key已经插入当前容器,并且 `insertTo` 不为空,则会进行移动此面板至操作,新panel属性不会变化,需要手动调用 `updatePanel` 进行更新属性操作。 267 | * 注意:如果 `insertTo` 不为空,则它的网格容器必须先存在,否则会添加失败。 268 | * panel.key 不可为空。 269 | 270 | | 参数 | 描述 | 271 | | :----: | :----: | 272 | | panel | 面板 | 273 | | insertTo | 将面板插入指定名称的网格,为空则插入至顶级网格 | 274 | 275 | ###### `addPanels(panels: IDockPanel[], insertTo?: string|DockData) : void` 276 | 277 | 说明: 278 | 279 | 向容器内插入面板。同 `addPanel`,只不过此函数一次性添加多个面板。 280 | 281 | | 参数 | 描述 | 282 | | :----: | :----: | 283 | | panels | 面板数组 | 284 | | insertTo | 将面板插入指定名称的网格,为空则插入至顶级网格 | 285 | 286 | ###### `removePanel(key: string) : void` 287 | 288 | 说明: 289 | 290 | 移除指定的面板 291 | 292 | | 参数 | 描述 | 293 | | :----: | :----: | 294 | | key | 面板唯一Key | 295 | 296 | ###### `removePanels(keys: string[]) : void` 297 | 298 | 说明: 299 | 300 | 移除多个指定的面板 301 | 302 | | 参数 | 描述 | 303 | | :----: | :----: | 304 | | keys | 面板唯一Key | 305 | 306 | ###### `activePanel(key: string) : void` 307 | 308 | 说明: 309 | 310 | 激活指定的面板 311 | 312 | | 参数 | 描述 | 313 | | :----: | :----: | 314 | | key | 面板唯一Key | 315 | 316 | ## 支持 317 | 318 | 作者开发不易,如果这个项目对您有帮助,希望你可以帮我点个 ⭐ ,这将是对我极大的鼓励。谢谢啦 (●'◡'●) 319 | 320 | ## Changelog 321 | 322 | [Changelog](./CHANGELOG.md) 323 | 324 | ## 作者的其他项目 325 | 326 | * [vue3-context-menu 一个简洁美观简单的Vue3右键菜单组件](https://github.com/imengyu/vue3-context-menu/) 327 | * [vue-dynamic-form 一款Vue3动态表单渲染库](https://github.com/imengyu/vue-dynamic-form) 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | vue-dock-layout 3 | --- 4 | A dock layout component like `visual studio` for Vue3 5 | 6 | ![Screenshot](./demo.jpg) 7 | 8 | [中文说明](https://github.com/imengyu/vue-dock-layout/blob/main/README.CN.md) 9 | 10 | **Note: This project is no longer under development. We recommend using [vue-code-layout](https://github.com/imengyu/vue-code-layout).** 11 | 12 | --- 13 | 14 | [Click here View online Demo](https://imengyu.top/pages/vue-dock-layout-demo/) 15 | 16 | ## Features 17 | 18 | * Simple and easy to use, small size 19 | * Customizable 20 | 21 | ### Install 22 | 23 | ``` 24 | npm install -save @imengyu/vue-dock-layout 25 | ``` 26 | 27 | ## Useage 28 | 29 | ### Import 30 | 31 | ```ts 32 | import { DockLayout, DockLayoutInterface } from '@imengyu/vue-dock-layout'; 33 | ``` 34 | 35 | ### Make layoyt 36 | 37 | To use vue dock layout, you need to first add a container component to your interface (`DockLayout`), which is the content hosting area of your application. 38 | 39 | ```html 40 | 59 | ``` 60 | 61 | The layout of layout components is based on a grid layout, with each segmented area forming a grid, and your custom content is embedded in the grid as a panel. 62 | 63 | The component provides some interfaces that allow you to quickly set the interface layout programmatically: 64 | 65 | ```ts 66 | const dockLayout = ref(); 67 | 68 | onMounted(() => { 69 | nextTick(() => { 70 | //Set the interface layout here first 71 | //Here we have added a horizontal layout with three areas, 72 | //left/center/right, with a width ratio of 20%: 60%: 20% 73 | //Then the left region is divided into two regions: leftA/leftB, with a width of 50%: 50% 74 | dockLayout.value?.setData({ 75 | name: 'root', 76 | size: 0, 77 | grids: [ 78 | { 79 | size: 20, 80 | name: 'left', 81 | grids: [ 82 | { 83 | size: 50, 84 | name: 'leftA', 85 | }, 86 | { 87 | size: 50, 88 | name: 'leftB', 89 | }, 90 | ] 91 | }, 92 | { 93 | size: 60, 94 | name: 'center', 95 | //Here we set that the center area will not be 96 | //automatically removed when there is no panel 97 | alwaysVisible: true, 98 | }, 99 | { 100 | size: 20, 101 | name: 'right', 102 | }, 103 | ], 104 | }); 105 | //The code below adds a content panel to the grid 106 | //Each content panel is identified by a key, which can be read from the 107 | //panel parameter in the panel render slot of DockLayout. 108 | dockLayout.value?.addPanels([ 109 | { 110 | key: 'panel1', 111 | title: 'panel1', 112 | }, 113 | { 114 | key: 'panel2', 115 | title: 'panel2', 116 | }, 117 | ], 'leftA'); 118 | dockLayout.value?.addPanels([ 119 | { 120 | key: 'panel3', 121 | title: 'panel3', 122 | }, 123 | ], 'leftB'); 124 | }) 125 | }); 126 | ``` 127 | 128 | The above examples can be found in [online Demo](https://imengyu.top/pages/vue-dock-layout-demo/). The first example in was found. 129 | 130 | Users can customize the drag and drop interface. Therefore, when the program leaves, if you need to save the user's custom settings, you can read the current grid layout data, set it to the component the next time the program loads. 131 | 132 | ```js 133 | onBeforeUnmount(() => { 134 | const layoutData = dockLayout.value?.getSaveData() 135 | //Save layoutData to anywhere... 136 | 137 | //Next time, load and set to dockLayout 138 | dockLayout.value?.setData(layoutData); 139 | }) 140 | ``` 141 | 142 | ### Customize 143 | 144 | #### Theme 145 | 146 | The component default provides two themes for you to use: `light` and `dark`. The theme can be specified using the `theme` attribute of the `DockLayout` component. 147 | 148 | ```html 149 | 150 | ... 151 | 152 | ``` 153 | 154 | The two theme effects are shown in the following figure: 155 | 156 | |light|dark| 157 | |---|---| 158 | |![Screenshot](./demo.light.jpg)|![Screenshot](./demo.jpg)| 159 | 160 | #### Custom rendering 161 | 162 | The component provides rendering slots in some locations that you can customize for rendering. 163 | 164 | For specific examples and source code, please see [online Dome](https://imengyu.top/pages/vue-dock-layout-demo/#/DockLayoutThemeTest). 165 | 166 | ##### tabItemRender 167 | 168 | Custom rendering for panel titles. 169 | 170 | ```vue 171 | 172 | 183 | 184 | ``` 185 | 186 | ##### emptyPanel 187 | 188 | This slot is used to render the backplane displayed when the grid has no content panel. 189 | 190 | ```vue 191 | 192 | 198 | 199 | ``` 200 | 201 | ## API reference 202 | 203 | DockLayout is the main container for layout components. 204 | 205 | ##### Props 206 | 207 | | Name | description | type | default value | 208 | | :----: | :----: | :----: | :----: | 209 | | tabHeight | The height of the Tab component, used for related calculations | `number` | 40 | 210 | | startVerticalDirection | Is the first layout grid vertical? | `boolean` | `false` | 211 | | allowFloatWindow | Allow floating windows? | `boolean` | `false` | 212 | | theme | Theme, can be 'light' or 'dark' | `string` | `dark` | 213 | 214 | ##### Events 215 | 216 | | Name | description | params | 217 | | :----: | :----: | :----: | 218 | | active-tab-change | Trigger event when the active content panel changes | `currentActive : DockPanel|null`,`lastActive : DockPanel|null` | 219 | | tab-closed | Trigger this event when the content panel is closed | `panel: DockPanel` | 220 | 221 | ##### Slots 222 | 223 | | Name | description | params | 224 | | :----: | :----: | :----: | 225 | | emptyPanel | This slot is used to render panel titles. | { name: string, dockData: DockData } | 226 | | tabItemRender | This slot is used to render the backplane displayed when the grid has no content panel. | DockTabItemRenderData | 227 | 228 | ##### Instance Function 229 | 230 | ```ts 231 | //When using TS, you can use DockLayoutInterface to obtain code prompts 232 | const dockLayout = ref(); 233 | ``` 234 | 235 | ###### `getSaveData() : IDockGrid` 236 | 237 | Explain: 238 | 239 | Obtain current interface grid layout data 240 | 241 | Return: 242 | 243 | grid layout data 244 | 245 | ###### `setData(data: IDockGrid) : void` 246 | 247 | Explain: 248 | 249 | Set interface grid layout data 250 | 251 | | params | description | 252 | | :----: | :----: | 253 | | data | grid layout data | 254 | 255 | ###### `updatePanel(panel: IDockPanel) : void` 256 | 257 | Explain: 258 | 259 | Update properties of panel instances 260 | 261 | | params | description | 262 | | :----: | :----: | 263 | | panel | Target panel | 264 | 265 | ###### `addPanel(panel: IDockPanel, insertTo?: string|DockData) : void` 266 | 267 | Explain: 268 | 269 | Insert the panel into the container. 270 | 271 | * Note: If the panel key has already been inserted into the current container and `insertTo` is not empty, the panel will be moved to the operation, and the new panel properties will not change. You need to manually call `updatePanel` to update the properties. 272 | * Note: If `insertTo` is not empty, its grid container must first exist, otherwise the addition will fail. 273 | * `panel.key` cannot be empty. 274 | 275 | | params | description | 276 | | :----: | :----: | 277 | | panel | Target panel | 278 | | insertTo | Insert the panel into the grid with the specified name. If left blank, insert it into the top-level grid | 279 | 280 | ###### `addPanels(panels: IDockPanel[], insertTo?: string|DockData) : void` 281 | 282 | Explain: 283 | 284 | Insert the panel into the container. Same as' addPanel ', but this function adds multiple panels at once. 285 | 286 | | params | description | 287 | | :----: | :----: | 288 | | panels | Panel array | 289 | | insertTo | Insert the panel into the grid with the specified name. If left blank, insert it into the top-level grid | 290 | 291 | ###### `removePanel(key: string) : void` 292 | 293 | Explain: 294 | 295 | Remove specified panel 296 | 297 | | params | description | 298 | | :----: | :----: | 299 | | key | Target panel key | 300 | 301 | ###### `removePanels(keys: string[]) : void` 302 | 303 | Explain: 304 | 305 | Remove multiple specified panels 306 | 307 | | params | description | 308 | | :----: | :----: | 309 | | keys | Target panel keys | 310 | 311 | ###### `activePanel(key: string) : void` 312 | 313 | Explain: 314 | 315 | Activate the specified panel 316 | 317 | | params | description | 318 | | :----: | :----: | 319 | | key | Target panel key | 320 | 321 | ## Changelog 322 | 323 | [Changelog](./CHANGELOG.md) 324 | 325 | ## Other projects of the author 326 | 327 | * [vue3-context-menu A very simple context menu component for Vue3](https://github.com/imengyu/vue3-context-menu/) 328 | * [vue-dynamic-form A data driven form component for vue3](https://github.com/imengyu/vue-dynamic-form) 329 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: ['@vue/babel-plugin-jsx'] 6 | } 7 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dock-layout/069910ef40cfad5c1e92178f5c8a404ca66898e1/demo.jpg -------------------------------------------------------------------------------- /demo.light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dock-layout/069910ef40cfad5c1e92178f5c8a404ca66898e1/demo.light.jpg -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /examples/css/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2648583 */ 3 | src: url('iconfont.woff2?t=1625231696258') format('woff2'), 4 | url('iconfont.woff?t=1625231696258') format('woff'), 5 | url('iconfont.ttf?t=1625231696258') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-reload-:before { 17 | content: "\e898"; 18 | } 19 | 20 | .icon-print:before { 21 | content: "\e899"; 22 | } 23 | 24 | .icon-reload-1:before { 25 | content: "\e89e"; 26 | } 27 | 28 | .icon-save:before { 29 | content: "\e8a4"; 30 | } 31 | 32 | .icon-settings-1:before { 33 | content: "\e8a7"; 34 | } 35 | 36 | .icon-terminal:before { 37 | content: "\e8c2"; 38 | } 39 | 40 | .icon-yidong:before { 41 | content: "\e68c"; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /examples/css/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dock-layout/069910ef40cfad5c1e92178f5c8a404ca66898e1/examples/css/iconfont.ttf -------------------------------------------------------------------------------- /examples/css/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dock-layout/069910ef40cfad5c1e92178f5c8a404ca66898e1/examples/css/iconfont.woff -------------------------------------------------------------------------------- /examples/css/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dock-layout/069910ef40cfad5c1e92178f5c8a404ca66898e1/examples/css/iconfont.woff2 -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 | Fork me on GitHub 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import './css/iconfont.css' 5 | 6 | createApp(App) 7 | .use(router) 8 | .mount('#app') 9 | -------------------------------------------------------------------------------- /examples/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | import DockLayoutBaseTest from '../views/DockLayoutBaseTest.vue' 3 | import DockLayoutCustomTest from '../views/DockLayoutCustomTest.vue' 4 | import DockLayoutExtraTest from '../views/DockLayoutExtraTest.vue' 5 | import DockLayoutDataTest from '../views/DockLayoutDataTest.vue' 6 | import DockLayoutThemeTest from '../views/DockLayoutThemeTest.vue' 7 | import HomeView from '../views/DockLayoutTests.vue' 8 | 9 | const routes: Array = [ 10 | { 11 | path: '/', 12 | name: 'home', 13 | component: HomeView, 14 | children: [ 15 | { 16 | path: '', 17 | name: 'DockLayoutBaseTest', 18 | component: DockLayoutBaseTest 19 | }, 20 | { 21 | path: 'DockLayoutCustomTest', 22 | name: 'DockLayoutCustomTest', 23 | component: DockLayoutCustomTest 24 | }, 25 | { 26 | path: 'DockLayoutDataTest', 27 | name: 'DockLayoutDataTest', 28 | component: DockLayoutDataTest 29 | }, 30 | { 31 | path: 'DockLayoutExtraTest', 32 | name: 'DockLayoutExtraTest', 33 | component: DockLayoutExtraTest 34 | }, 35 | { 36 | path: 'DockLayoutThemeTest', 37 | name: 'DockLayoutThemeTest', 38 | component: DockLayoutThemeTest 39 | }, 40 | ] 41 | }, 42 | ] 43 | 44 | const router = createRouter({ 45 | history: createWebHashHistory(process.env.BASE_URL), 46 | routes 47 | }) 48 | 49 | export default router 50 | -------------------------------------------------------------------------------- /examples/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /examples/views/DockLayoutBaseTest.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 131 | -------------------------------------------------------------------------------- /examples/views/DockLayoutCustomTest.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 146 | 147 | 162 | -------------------------------------------------------------------------------- /examples/views/DockLayoutDataTest.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 149 | -------------------------------------------------------------------------------- /examples/views/DockLayoutExtraTest.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 154 | 155 | -------------------------------------------------------------------------------- /examples/views/DockLayoutTests.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 71 | -------------------------------------------------------------------------------- /examples/views/DockLayoutThemeTest.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import DockLayout from './lib/DockLayout.vue' 2 | import DockPanelDefaultTab from './lib/DockPanelDefaultTab.vue' 3 | import DockPanelHost from './lib/DockPanelHost.vue' 4 | import DockSplit from './lib/DockSplit.vue' 5 | 6 | declare module 'vue-dock-layout' { 7 | } 8 | 9 | export * from './lib/DockHostData' 10 | export * from './lib/DockLayoutData' 11 | 12 | export { 13 | DockLayout, 14 | DockSplit, 15 | DockPanelHost, 16 | DockPanelDefaultTab, 17 | } 18 | 19 | export default DockLayout; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import DockLayout from './src/DockLayout.vue' 2 | import DockPanelDefaultTab from './src/DockPanelDefaultTab.vue' 3 | import DockPanelHost from './src/DockPanelHost.vue' 4 | import DockSplit from './src/DockSplit.vue' 5 | 6 | export * from './src/DockHostData' 7 | export * from './src/DockLayoutData' 8 | 9 | export { 10 | DockLayout, 11 | DockSplit, 12 | DockPanelHost, 13 | DockPanelDefaultTab, 14 | } 15 | 16 | export default DockLayout; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imengyu/vue-dock-layout", 3 | "version": "0.6.0", 4 | "description": "Dock layout component like visual studio for Vue3", 5 | "main": "lib/vue-dock-layout.umd.min.js", 6 | "files": [ 7 | "src", 8 | "lib", 9 | "examples", 10 | "index.js", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "build": "vue-cli-service build --target lib --name vue-dock-layout --dest lib index.js", 16 | "build-demo": "vue-cli-service build", 17 | "build-types": "vue-tsc --declaration --emitDeclarationOnly", 18 | "serve": "vue-cli-service serve", 19 | "test:unit": "vue-cli-service test:unit", 20 | "lint": "vue-cli-service lint", 21 | "docs:dev": "vitepress dev docs/zh", 22 | "docs:build-zh": "vitepress build docs/zh", 23 | "docs:build-en": "vitepress build docs/en" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/imengyu/vue-dock-layout.git" 28 | }, 29 | "keywords": [ 30 | "vue3", 31 | "vue", 32 | "vue-dock-layout", 33 | "dock-layout", 34 | "docklayout", 35 | "dock", 36 | "停靠组件", 37 | "停靠", 38 | "panel" 39 | ], 40 | "author": "imengyu", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/imengyu/vue-dock-layout/issues" 44 | }, 45 | "homepage": "https://github.com/imengyu/vue-dock-layout#readme", 46 | "devDependencies": { 47 | "@codemirror/lang-javascript": "^6.1.1", 48 | "@codemirror/lang-json": "^6.0.1", 49 | "@codemirror/theme-one-dark": "^6.1.0", 50 | "@typescript-eslint/eslint-plugin": "^4.33.0", 51 | "@typescript-eslint/parser": "^4.33.0", 52 | "@vue/babel-plugin-jsx": "^1.1.1", 53 | "@vue/cli-plugin-babel": "~5.0.0", 54 | "@vue/cli-plugin-eslint": "~5.0.0", 55 | "@vue/cli-plugin-router": "~5.0.0", 56 | "@vue/cli-plugin-typescript": "~5.0.0", 57 | "@vue/cli-plugin-unit-mocha": "~5.0.0", 58 | "@vue/cli-plugin-vuex": "~5.0.0", 59 | "@vue/cli-service": "~5.0.0", 60 | "@vue/compiler-sfc": "^3.2.31", 61 | "@vue/eslint-config-typescript": "^7.0.0", 62 | "@vue/test-utils": "^2.0.0-0", 63 | "codemirror": "^6.0.1", 64 | "eslint": "^7.29.0", 65 | "eslint-plugin-vue": "^7.12.1", 66 | "sass": "^1.32.7", 67 | "sass-loader": "^12.0.0", 68 | "typescript": "^4.3.5", 69 | "vitepress": "^1.0.0-alpha.76", 70 | "vue": "^3.2.13", 71 | "vue-codemirror": "^6.1.1", 72 | "vue-router": "^4.1.5", 73 | "vue-tsc": "^1.0.24" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/DockCommonStyles.scss: -------------------------------------------------------------------------------- 1 | $primary-color: rgb(206, 100, 255); 2 | $primary-color-dark: rgb(189, 73, 243); 3 | 4 | $tab-mormal-color: #161727; 5 | $tab-active-color: #282c34; 6 | $tab-border-color: #111216; 7 | $tab-button-normal-color: #cccccc; 8 | $tab-button-hover-color: #7e7e7e; 9 | 10 | $light-theme-tab-mormal-color: #e0e0e0; 11 | $light-theme-tab-active-color: #fff; 12 | $light-theme-tab-border-color: #d8d8d8; 13 | $light-theme-tab-button-normal-color: #cccccc; 14 | $light-theme-tab-button-hover-color: #7e7e7e; 15 | 16 | $tab-height: 35px; 17 | 18 | /* PC Scrollbar */ 19 | 20 | @mixin pc-fix-scrollbar() { 21 | @media (min-width: 768px) { 22 | &::-webkit-scrollbar { 23 | width: 5px; 24 | height: 5px; 25 | } 26 | 27 | &::-webkit-scrollbar-thumb { 28 | background: #707070; 29 | border-radius: 3px; 30 | 31 | &:hover { 32 | background: #e0e0e0; 33 | } 34 | } 35 | 36 | &::-webkit-scrollbar-track { 37 | background: transparent; 38 | } 39 | } 40 | } 41 | 42 | @mixin pc-fix-scrollbar-white() { 43 | @media (min-width: 768px) { 44 | &::-webkit-scrollbar { 45 | width: 5px; 46 | height: 5px; 47 | } 48 | 49 | &::-webkit-scrollbar-thumb { 50 | background: #d6d6d6; 51 | opacity: .7; 52 | border-radius: 3px; 53 | 54 | &:hover { 55 | background: #707070; 56 | } 57 | } 58 | 59 | &::-webkit-scrollbar-track { 60 | background: transparent; 61 | } 62 | } 63 | } 64 | 65 | /* Debug */ 66 | 67 | $debug-color: #d32a00; 68 | 69 | .dock-debug { 70 | position: absolute; 71 | top: $tab-height + 2px; 72 | left: 2px; 73 | padding: 1px; 74 | font-size: 10px; 75 | border: 1px solid $debug-color; 76 | color: $debug-color; 77 | background-color: #fff; 78 | z-index: 100; 79 | } -------------------------------------------------------------------------------- /src/DockDropLayout.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 119 | 120 | 251 | -------------------------------------------------------------------------------- /src/DockHostData.ts: -------------------------------------------------------------------------------- 1 | import { DockData, DockPanel } from "./DockLayoutData"; 2 | import { VNode } from "vue"; 3 | import { IKeyAnyObject } from "./DockUtils"; 4 | 5 | /** 6 | * 主容器内部使用的公共接口 7 | */ 8 | export interface DockHostData { 9 | isDragging: boolean; 10 | onStartDrag: (item: DockPanel) => void; 11 | onEndDrag: () => void; 12 | onGridDrag: (thisGrid : DockData, nextGrid : DockData) => void; 13 | onGridDropFinish: (grid : DockData) => void; //拖动完成 14 | onActiveTabChange: (grid : DockData, lastActive : DockPanel|null, currentActive : DockPanel) => void; 15 | onClosePanel: (panel: DockPanel) => void; 16 | renderSlot: (name: string, param: IKeyAnyObject) => VNode[]; 17 | hasSlot: (name: string) => boolean; 18 | getDockPanel: (key: string) => DockPanel|null; 19 | clearAllFloatDragLightBox: () => void; 20 | checkAndRemoveEmptyGrid: (parent: DockData) => void;//移除后,检查整理相关网格 21 | forceFlushAllPanelPos: (parent: DockData) => void; 22 | dropCurrentPanel: DockPanel|null; 23 | dropCurrentRegion: DockData|null,//当前鼠标所在区域的网格信息 24 | showDropLayout : boolean; 25 | tabHeight: number; 26 | theme: 'dark'|'light'; 27 | host: HTMLDivElement|null, 28 | } -------------------------------------------------------------------------------- /src/DockInstance.ts: -------------------------------------------------------------------------------- 1 | import DockLayout from './DockLayout.vue'; 2 | 3 | export { 4 | DockLayout 5 | } -------------------------------------------------------------------------------- /src/DockLayout.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 957 | 958 | -------------------------------------------------------------------------------- /src/DockLayoutData.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from "vue"; 2 | import DockUtils, { IKeyAnyObject } from "./DockUtils"; 3 | import { Rect } from "./DockVector"; 4 | 5 | /** 6 | * 描述拖动界面面板属性的接口 7 | */ 8 | export interface IDockPanel { 9 | /** 10 | * 当前面板的标签,用于拖动相关判断 11 | */ 12 | tag?: string; 13 | /** 14 | * 面板唯一Key 15 | */ 16 | key: string; 17 | /** 18 | * 面板标题 19 | */ 20 | title?: string; 21 | /** 22 | * 面板的图标类 23 | */ 24 | iconClass?: string; 25 | /** 26 | * 关闭按扭是否显示未保存圆点。默认:否 27 | */ 28 | closeUnSave?: boolean; 29 | /** 30 | * 是否显示关闭按扭。默认:否 31 | */ 32 | closeable?: boolean; 33 | /** 34 | * 是否使用 tabItemRender 插槽自定义绘制Tab条目,默认:否 35 | */ 36 | customTab?: boolean; 37 | /** 38 | * 如果指定了渲染函数,则会调用此渲染函数渲染面板内容 39 | */ 40 | render?: ((panel: IDockPanel) => VNode)|null; 41 | /** 42 | * 关闭按扭点击事件回调。如果返回 true,则执行移除面板操作。 43 | */ 44 | onClose?: (() => boolean) | null; 45 | /** 46 | * 标签右键点击事件回调 47 | */ 48 | onTabRightClick?: (() => void) | null; 49 | 50 | uid?: string; 51 | } 52 | 53 | /** 54 | * 描述拖动界面网格属性的接口 55 | */ 56 | export interface IDockGrid { 57 | /** 58 | * 格子的大小(占父级的百分比1-100),为0则表示自动平均占据父容器 59 | */ 60 | size: number, 61 | /** 62 | * 子网格 63 | */ 64 | grids?: IDockGrid[], 65 | /** 66 | * 子面板 67 | */ 68 | panels?: IDockPanel[], 69 | /** 70 | * 是否是永远显示(否则在当前网格没有子面板时会被移除掉) 71 | */ 72 | alwaysVisible?: boolean; 73 | /** 74 | * 当前格子名称 75 | */ 76 | name?: string; 77 | /** 78 | * 标签 79 | */ 80 | tag?: string; 81 | /** 82 | * 可放入的面板标签数组。此限制仅在用户拖动时有效,使用 `addPanel`, `setData` 等接口添加的面板不受此限制。 83 | */ 84 | acceptPanelTags?: string[]; 85 | /** 86 | * 是否允许拖拽入当前面板时深层插入,深层插入会在当前同级创建新的网格,如果你希望把面板限制在当前容器中(搭配acceptPanelTags),可以设置此属性为false。默认:是 87 | */ 88 | allowDep?: boolean; 89 | /** 90 | * Tab的自定义样式 91 | */ 92 | tabStyle?: IKeyAnyObject; 93 | /** 94 | * Tab条目的自定义样式 95 | */ 96 | tabItemStyle?: IKeyAnyObject; 97 | 98 | uid?: string; 99 | } 100 | 101 | /** 102 | * 拖动界面网格方向定义 103 | */ 104 | export type DockDirection = 'vertical' | 'horizontal' | 'unknow'; 105 | 106 | /** 107 | * tabItemRender 插槽数据定义 108 | */ 109 | export interface DockTabItemRenderData { 110 | dockData: DockData, 111 | panel: DockPanel, 112 | index: number, 113 | onTabItemMouseDown: (e: MouseEvent, item: DockPanel) => void, 114 | onTabItemDragStart: (ev: DragEvent, item: DockPanel) => void, 115 | onTabItemDragEnd: (ev: DragEvent) => void, 116 | onTabItemClose: (panel: DockPanel) => void, 117 | } 118 | 119 | export function getTargetGridSize(nowLen: number) : number { 120 | if (nowLen === 0) 121 | return 100; 122 | else if (nowLen <= 6) 123 | return 100 / nowLen; 124 | return 14; 125 | } 126 | 127 | 128 | /** 129 | * 停靠容器数据 130 | */ 131 | export class DockData implements IDockGrid { 132 | 133 | constructor() { 134 | this.uid = DockUtils.genNonDuplicateID(16); 135 | } 136 | 137 | /** 138 | * 当前容器的布局方向 139 | */ 140 | currentDirection: DockDirection = 'unknow'; 141 | /** 142 | * 子容器 143 | */ 144 | grids = new Array(); 145 | /** 146 | * 其中包含的面板 147 | */ 148 | panels = new Array(); 149 | /** 150 | * 当前容器的大小(占父级的百分比1-100) 151 | */ 152 | size = 0; 153 | /** 154 | * 是否是永远显示(否则在当前网格没有子面板时会被移除) 155 | */ 156 | alwaysVisible = false; 157 | /** 158 | * 当前激活的面板 159 | */ 160 | activeTab: DockPanel | null = null; 161 | /** 162 | * 名称 163 | */ 164 | name = ''; 165 | /** 166 | * 标签 167 | */ 168 | tag = ''; 169 | /** 170 | * 允许拖放的子面板 171 | */ 172 | acceptPanelTags = [] as string[]; 173 | /** 174 | * 允许深层插入 175 | */ 176 | allowDep = true; 177 | /** 178 | * Tab的自定义样式 179 | */ 180 | tabStyle = {}; 181 | /** 182 | * Tab条目的自定义样式 183 | */ 184 | tabItemStyle = {}; 185 | 186 | //内部使用 187 | uid = ''; 188 | startSize = 0; 189 | lastLayoutSize = new Rect(); 190 | parent: DockData | null = null; 191 | allowIsolated = false; 192 | 193 | isIsolated() : boolean { 194 | return this.parent === null; 195 | } 196 | 197 | addPanel(panel: DockPanel, insertIndex?: number): boolean { 198 | if (!this.allowIsolated && this.isIsolated()) { 199 | console.error('addPanel on a isolated DockData', this.name); 200 | return false; 201 | } 202 | if (panel.parent !== null && (panel.parent !== this)) { 203 | console.error('Panel ' + panel.key + ' Not removed before adding, current parent', panel.parent, 'target parent', this.name); 204 | return false; 205 | } 206 | if (!this.panels.includes(panel)) { 207 | if (typeof insertIndex === 'number') this.panels.splice(insertIndex, 0, panel); 208 | else this.panels.push(panel); 209 | panel.parent = this; 210 | if (this.activeTab == null) { 211 | this.activeTab = panel; 212 | return true; 213 | } 214 | } 215 | return false; 216 | } 217 | removePanel(panel: DockPanel): boolean { 218 | const i = this.panels.indexOf(panel); 219 | if (i >= 0) { 220 | this.panels.splice(i, 1); 221 | panel.parent = null; 222 | if (this.activeTab == panel) { 223 | //移除面板后,检查网格选中,如果选中项是移除的面板,则重新选中一个面板 224 | this.activeTab = this.panels[i >= this.panels.length ? this.panels.length - 1 : i]; 225 | return true; 226 | } 227 | } 228 | return false; 229 | } 230 | addGrid(grid: DockData, insertIndex?: number, forceSize = false): void { 231 | if (!this.allowIsolated && this.isIsolated()) { 232 | console.error('addGrid on a isolated DockData', this.name); 233 | return; 234 | } 235 | 236 | if (!this.grids.includes(grid)) { 237 | 238 | if (typeof insertIndex === 'number') 239 | this.grids.splice(insertIndex, 0, grid); 240 | else 241 | this.grids.push(grid); 242 | grid.parent = this; 243 | 244 | if (!forceSize) { 245 | //计算网格新插入后的大小 246 | const targetSize = getTargetGridSize(this.grids.length); 247 | const cutSize = targetSize / (this.grids.length - 1); 248 | 249 | //重新分配子的大小 250 | this.grids.forEach((g) => { 251 | g.size -= cutSize; 252 | }); 253 | 254 | grid.size = targetSize; 255 | } 256 | } 257 | } 258 | removeGrid(grid: DockData): void { 259 | const index = this.grids.indexOf(grid); 260 | if (index >= 0) { 261 | this.grids.splice(index, 1); 262 | 263 | grid.parent = null; 264 | 265 | //该子大小腾出来了,重新分配其他子的大小 266 | const equalDivisionSize = grid.size / this.grids.length; 267 | this.grids.forEach((g) => { 268 | g.size += equalDivisionSize 269 | }); 270 | } 271 | } 272 | resetPanelsParent(): void { 273 | this.panels.forEach((v) => v.parent = this); 274 | } 275 | resetGridsParent(): void { 276 | this.grids.forEach((v) => v.parent = this); 277 | } 278 | //依据父级方向设置当前的方向 279 | setDirectionByParent(parent: DockData): void { 280 | if (parent.currentDirection === 'vertical') { 281 | this.currentDirection = 'horizontal'; 282 | } else { 283 | this.currentDirection = 'vertical'; 284 | } 285 | } 286 | //从父级继承一些用户属性 287 | inhertProps(parent: DockData) : void { 288 | //this.alwaysVisible = parent.alwaysVisible; 289 | this.acceptPanelTags = parent.acceptPanelTags; 290 | this.allowDep = parent.allowDep; 291 | this.tabItemStyle = parent.tabItemStyle; 292 | this.tabStyle = parent.tabStyle; 293 | this.tag = parent.tag; 294 | } 295 | 296 | /** 297 | * 递归序列化为JSON以供保存 298 | */ 299 | toJSON(): IDockGrid { 300 | return { 301 | size: this.size, 302 | grids: this.grids.map((v) => v.toJSON()), 303 | panels: this.panels.map((v) => v.toJSON()), 304 | alwaysVisible: this.alwaysVisible, 305 | name: this.name, 306 | acceptPanelTags: this.acceptPanelTags, 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * 停靠容器数据 313 | */ 314 | export class DockRootData extends DockData { 315 | 316 | constructor() { 317 | super(); 318 | this.allowIsolated = true; 319 | } 320 | } 321 | 322 | /** 323 | * 描述拖动界面网格属性实体 324 | */ 325 | export class DockPanel implements IDockPanel { 326 | 327 | constructor() { 328 | this.uid = DockUtils.genNonDuplicateID(16); 329 | } 330 | 331 | /** 332 | * 父级 333 | */ 334 | parent: DockData | null = null; 335 | /** 336 | * 当前面板的标签,用于拖动相关判断 337 | */ 338 | tag = ''; 339 | 340 | /** 341 | * 如果指定了渲染函数,则会调用此渲染函数渲染面板内容 342 | */ 343 | render: ((panel: IDockPanel) => VNode)|null = null; 344 | 345 | //内部使用 346 | uid = ''; 347 | visible = false; 348 | finalRegion = new Rect(); 349 | 350 | /** 351 | * 面板唯一Key 352 | */ 353 | key = ''; 354 | /** 355 | * 面板标题 356 | */ 357 | title = ''; 358 | /** 359 | * 面板的图标类 360 | */ 361 | iconClass = ''; 362 | /** 363 | * 关闭按扭是否显示未保存圆点 364 | */ 365 | closeUnSave = false; 366 | /** 367 | * 是否显示关闭按扭 368 | */ 369 | closeable = false; 370 | /** 371 | * 是否使用 tabItemRender 插槽自定义绘制Tab条目,默认:否 372 | */ 373 | customTab = false; 374 | /** 375 | * 关闭按扭点击事件回调。如果返回 true,则执行移除面板操作。 376 | */ 377 | onClose: (() => boolean) | null = null; 378 | /** 379 | * 标签右键点击事件回调 380 | */ 381 | onTabRightClick: (() => void) | null = null; 382 | 383 | /** 384 | * 递归序列化为JSON以供保存 385 | */ 386 | toJSON(): IDockPanel { 387 | return { 388 | uid: this.uid, 389 | tag: this.tag, 390 | key: this.key, 391 | title: this.title, 392 | iconClass: this.iconClass, 393 | closeUnSave: this.closeUnSave, 394 | closeable: this.closeable, 395 | customTab: this.customTab, 396 | }; 397 | } 398 | /** 399 | * 从JSON创建当前对象 400 | * @param json 401 | */ 402 | formJSON(json: IDockPanel): void { 403 | this.uid = DockUtils.defaultIfUndefined(json.uid, this.uid); 404 | this.tag = DockUtils.defaultIfUndefined(json.tag, this.tag); 405 | this.key = DockUtils.defaultIfUndefined(json.key, this.key); 406 | this.title = DockUtils.defaultIfUndefined(json.title, this.title); 407 | this.iconClass = DockUtils.defaultIfUndefined(json.iconClass, this.iconClass); 408 | this.closeUnSave = DockUtils.defaultIfUndefined(json.closeUnSave, this.closeUnSave); 409 | this.closeable = DockUtils.defaultIfUndefined(json.closeable, this.closeable); 410 | this.customTab = DockUtils.defaultIfUndefined(json.customTab, this.customTab); 411 | this.onClose = DockUtils.defaultIfUndefined(json.onClose, this.onClose); 412 | this.onTabRightClick = DockUtils.defaultIfUndefined(json.onTabRightClick, this.onTabRightClick); 413 | this.render = DockUtils.defaultIfUndefined(json.render, this.render); 414 | } 415 | } 416 | 417 | /** 418 | * DockLayout 公共接口 419 | */ 420 | export interface DockLayoutInterface { 421 | /** 422 | * 获取界面网格布局数据 423 | * @public 424 | */ 425 | getSaveData: () => IDockGrid; 426 | /** 427 | * 设置界面网格布局数据 428 | * @public 429 | * @param data 网格数据 430 | */ 431 | setData: (data: IDockGrid) => void; 432 | /** 433 | * 更新面板实例的属性 434 | * @public 435 | * @param panel 436 | */ 437 | updatePanel: (panel: IDockPanel) => void; 438 | /** 439 | * 向容器内插入面板。 440 | * * 注意:如果面板key已经插入当前容器,并且 `insertTo` 不为空,则会进行移动此面板至操作, 441 | * 新panel属性不会变化,需要手动调用 `updatePanel` 进行更新属性操作。 442 | * * 注意:如果 `insertTo` 不为空,则它的网格容器必须先存在,否则会添加失败。 443 | * * panel.key 不可为空。 444 | * @public 445 | * @param panel 面板 446 | * @param insertTo 将面板插入指定名称的网格,为空则插入至顶级网格 447 | */ 448 | addPanel: (panel: IDockPanel, insertTo?: string|DockData) => void; 449 | /** 450 | * 向容器内插入面板。同 `addPanel` 451 | * @public 452 | * @param panels 面板数组 453 | * @param insertTo 将面板插入指定名称的网格,为空则插入至顶级网格 454 | */ 455 | addPanels: (panels: IDockPanel[], insertTo?: string|DockData) => void; 456 | /** 457 | * 移除指定的面板 458 | * @public 459 | * @param key 面板唯一Key 460 | */ 461 | removePanel: (key: string) => void; 462 | /** 463 | * 移除多个指定的面板 464 | * @public 465 | * @param keys 面板唯一Key 466 | */ 467 | removePanels: (keys: string[]) => void; 468 | /** 469 | * 激活指定的面板 470 | * @public 471 | * @param key 面板唯一Key 472 | */ 473 | activePanel: (key: string) => void; 474 | 475 | } -------------------------------------------------------------------------------- /src/DockPanelDefaultCloseDark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/DockPanelDefaultCloseLight.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/DockPanelDefaultTab.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | -------------------------------------------------------------------------------- /src/DockPanelHost.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 210 | 211 | -------------------------------------------------------------------------------- /src/DockSplit.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 290 | -------------------------------------------------------------------------------- /src/DockUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export type IKeyAnyObject = Record; 3 | 4 | export default { 5 | getTop, 6 | getLeft, 7 | /** 8 | * 字符串判空 9 | * @param str 字符串 10 | */ 11 | isNullOrEmpty(str : string | undefined | null | Record| number) : boolean { 12 | return !str || typeof str === 'undefined' || str === '' 13 | }, 14 | /** 15 | * 如果参数未定义,则返回默认值,否则返回这个参数 16 | * @param v 要判断的数值 17 | * @param drfaultValue 默认值 18 | */ 19 | defaultIfUndefined(v : undefined|null|T, drfaultValue: T) : T { 20 | return (v != null && typeof v != 'undefined') ? v : drfaultValue; 21 | }, 22 | /** 23 | * 生成不重复随机字符串 24 | * @param randomLength 字符长度 25 | */ 26 | genNonDuplicateID(randomLength : number) : string { 27 | let idStr = Date.now().toString(36) 28 | idStr += Math.random().toString(36).substr(3,randomLength) 29 | return idStr 30 | }, 31 | }; 32 | 33 | /** 34 | * 获取元素的绝对纵坐标 35 | * @param e 元素 36 | * @param stopClass 递归向上查找,遇到指定类的父级时停止 37 | */ 38 | function getTop(e: HTMLElement, stopClass ? : string) : number { 39 | let offset = e.offsetTop; 40 | if (e.offsetParent != null && (!stopClass || !(e.offsetParent).classList.contains(stopClass)) ) 41 | offset += getTop(e.offsetParent, stopClass); 42 | return offset; 43 | } 44 | /** 45 | * 获取元素的绝对横坐标 46 | * @param e 元素 47 | * @param stopClass 递归向上查找,遇到指定类的父级时停止 48 | */ 49 | function getLeft(e: HTMLElement, stopClass ? : string) : number { 50 | let offset = e.offsetLeft; 51 | if (e.offsetParent != null && (!stopClass || !(e.offsetParent).classList.contains(stopClass))) 52 | offset += getLeft(e.offsetParent, stopClass); 53 | 54 | return offset; 55 | } -------------------------------------------------------------------------------- /src/DockVector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 矩形类 [Rectangle base class] 3 | */ 4 | export class Rect { 5 | public x = 0; 6 | public y = 0; 7 | public w = 0; 8 | public h = 0; 9 | 10 | /** 11 | * 创建一个矩形 [Create a rectangle instance] 12 | * @param x 点x轴坐标或一个点对象,使用对象时可不填写y [Point X-axis coordinate or a point object. If the object is used, the parameter Y will be ignored] 13 | * @param y 点y轴坐标 [Point Y-axis coordinate] 14 | * @param y 宽度 [Width] 15 | * @param y 高度 [Height] 16 | */ 17 | public constructor(x?: number | Rect, y? : number, w? : number, h? : number) { 18 | this.set(x || 0,y,w,h); 19 | } 20 | 21 | /** 22 | * 设置此矩形实例的值 [Set the value for this rectangle instance] 23 | * * 参数与构造函数相同 【The parameter is the same as the constructor】 24 | */ 25 | public set(x: number | Rect, y? : number, w? : number, h? : number) : void { 26 | if(typeof x === "number") this.x = x; 27 | else if(typeof x == "object") { 28 | this.x = x.x; 29 | this.y = x.y; 30 | this.w = x.w; 31 | this.h = x.h; 32 | } 33 | if(typeof y != "undefined") this.y = y; 34 | if(typeof w != "undefined") this.w = w; 35 | if(typeof h != "undefined") this.h = h; 36 | } 37 | 38 | /** 39 | * 检查点x是否处于当前矩形中 [Check whether point x is in the current rectangle] 40 | * @param x 点x轴坐标或一个点对象,使用对象时可不填写y [Point X-axis coordinate or a point object. If the object is used, the parameter Y will be ignored] 41 | * @param y 点y轴坐标 [Point Y-axis coordinate] 42 | * @returns 43 | */ 44 | public testInRect(x: Vector2|number, y = 0) : boolean { 45 | if(typeof x === 'object') { 46 | const point = x; 47 | return point.x >= this.getLeft() && point.y >= this.getTop() 48 | && point.x <= this.getRight() && point.y <= this.getBottom(); 49 | } 50 | else 51 | return x >= this.getLeft() && y >= this.getTop() 52 | && x <= this.getRight() && y <= this.getBottom(); 53 | } 54 | /** 55 | * 检查当前矩形是否与其他矩形相交 [Checks whether the current rectangle intersects with other rectangle] 56 | * @param rect 矩形 [Other rectangle] 57 | * @returns 58 | */ 59 | public testRectCross(rect : Rect) : boolean { 60 | 61 | /* 第一个中心点*/ 62 | const a_cx = this.x + (this.w/2); 63 | const a_cy = this.y + (this.h/2); 64 | /* 第二个中心点*/ 65 | const b_cx = rect.x + (rect.w/2); 66 | const b_cy = rect.y + (rect.h/2); 67 | 68 | return ((Math.abs(a_cx - b_cx) <= (this.w/2 + rect.w/2)) 69 | && (Math.abs(a_cy - b_cy) <= (this.h/2 + rect.h/2))); 70 | } 71 | /** 72 | * 设置当前矩形的坐标 [Sets the coordinates of the current rectangle] 73 | * @param pointOrX 点x轴坐标或一个点对象,使用对象时可不填写y [Point X-axis coordinate or a point object. If the object is used, the parameter Y will be ignored] 74 | * @param y 点y轴坐标 [Point Y-axis coordinate] 75 | */ 76 | public setPos(pointOrX : Vector2 | number, y?:number) : void { 77 | if(typeof pointOrX == "number"){ 78 | this.x = pointOrX; 79 | this.y = y || this.y; 80 | }else { 81 | this.x = pointOrX.x; 82 | this.y = pointOrX.y; 83 | } 84 | } 85 | /** 86 | * 设置当前矩形的大小 [Sets the size of the current rectangle] 87 | * @param sizeOrW 宽度或一个点对象,使用对象时可不填写h [Width value or a point object. If the object is used, the parameter h will be ignored] 88 | * @param h 89 | */ 90 | public setSize(sizeOrW : Vector2 | number, h?:number) : void { 91 | if(typeof sizeOrW == "number"){ 92 | this.w = sizeOrW; 93 | this.h = h || this.h; 94 | }else { 95 | this.w = sizeOrW.x; 96 | this.h = sizeOrW.y; 97 | } 98 | } 99 | 100 | /** 101 | * 将当前矩形按中心坐标,扩展指定 size 大小 [Expand the current rectangle to the specified size according to the center coordinate] 102 | * @param size 扩展大小 103 | */ 104 | public expand(size : number) : void { 105 | this.x -= size; 106 | this.y -= size; 107 | this.w += size * 2; 108 | this.h += size * 2; 109 | } 110 | /** 111 | * 将矩形坐标和大小放大指定倍数 【Enlarges the rectangular coordinates and size by a specified factor】 112 | * @param v 113 | */ 114 | public multiply(v : number) : void { 115 | this.x *= v; 116 | this.y *= v; 117 | this.w *= v; 118 | this.h *= v; 119 | } 120 | 121 | /** 122 | * 获取矩形右坐标,如果大小为负数,将自动翻转 【Get the right coordinate of the rectangle, if the size is negative, it will be flipped automatically】 123 | * @returns 124 | */ 125 | public getRight() : number { return this.w < 0 ? this.x : this.x + this.w; } 126 | /** 127 | * 获取矩形下坐标,如果大小为负数,将自动翻转 【Get the bottom coordinate of the rectangle, if the size is negative, it will be flipped automatically】 128 | * @returns 129 | */ 130 | public getBottom() : number { return this.h < 0 ? this.y : this.y + this.h; } 131 | /** 132 | * 获取矩形左坐标,如果大小为负数,将自动翻转 【Get the left coordinate of the rectangle, if the size is negative, it will be flipped automatically】 133 | * @returns 134 | */ 135 | public getLeft() : number { return this.w < 0 ? this.x + this.w : this.x; } 136 | /** 137 | * 获取矩形上坐标,如果大小为负数,将自动翻转 【Get the top coordinate of the rectangle, if the size is negative, it will be flipped automatically】 138 | * @returns 139 | */ 140 | public getTop() : number { return this.h < 0 ? this.y + this.h : this.y; } 141 | /** 142 | * 获取矩形坐标 143 | * @returns 144 | */ 145 | public getPoint() : Vector2 { 146 | return new Vector2(this.x, this.y); 147 | } 148 | 149 | private center = new Vector2(); 150 | 151 | /** 152 | * 计算中心点 【Calculation center point】 153 | * @returns 154 | */ 155 | public calcCenter() : Vector2 { 156 | this.center.x = this.x + this.w / 2; 157 | this.center.y = this.y + this.h / 2; 158 | return this.center; 159 | } 160 | 161 | /** 162 | * 转为字符串形式 163 | * @returns 164 | */ 165 | public toString() : string { 166 | return `{x=${this.x},y=${this.y},w=${this.w},h=${this.h}}`; 167 | } 168 | 169 | /** 170 | * 使用两个点构造一个矩形 【Construct a rectangle with two points】 171 | * @param pt1 172 | * @param pt2 173 | * @returns 174 | */ 175 | public static makeBy2Point(rect : Rect, pt1 : Vector2, pt2 : Vector2) : Rect { 176 | let x1 = 0, x2 = 0, y1 = 0, y2 = 0; 177 | if(pt1.x <= pt2.x) { 178 | x1 = pt1.x; 179 | x2 = pt2.x; 180 | } else { 181 | x1 = pt2.x; 182 | x2 = pt1.x; 183 | } 184 | if(pt1.y <= pt2.y) { 185 | y1 = pt1.y; 186 | y2 = pt2.y; 187 | } else { 188 | y1 = pt2.y; 189 | y2 = pt1.y; 190 | } 191 | 192 | rect.set(x1, y1, x2 - x1, y2 - y1); 193 | return rect; 194 | } 195 | } 196 | 197 | /** 198 | * 2D Vector 199 | */ 200 | export class Vector2 { 201 | 202 | /** 203 | * X axis 204 | */ 205 | public x = 0; 206 | /** 207 | * Y axis 208 | */ 209 | public y = 0; 210 | 211 | public constructor(x: number|Vector2 = 0, y = 0) { 212 | if (typeof x === 'object') { 213 | this.y = x.y; 214 | this.x = x.x; 215 | } else { 216 | this.y = y; 217 | this.x = x; 218 | } 219 | } 220 | 221 | /** 222 | * Set new vector values 223 | * @param x X axis or other Vector 224 | * @param y Y axis or none 225 | */ 226 | public set(x : number|Vector2, y = 0) : Vector2 { 227 | if(typeof x === "number") { 228 | this.y = y; 229 | this.x = x; 230 | }else { 231 | this.y = x.y; 232 | this.x = x.x; 233 | } 234 | return this; 235 | } 236 | /** 237 | * Clone a new item 238 | * @returns 239 | */ 240 | public clone() : Vector2 { 241 | return new Vector2(this.x, this.y); 242 | } 243 | /** 244 | * 将当前二维向量加指定数字 【Adds the specified number to the current 2D vector】 245 | * @param v 246 | * @returns 247 | */ 248 | public add(v : number|Vector2) : Vector2 { 249 | if(typeof v === "number") { 250 | this.x += v; 251 | this.y += v; 252 | } 253 | else if(typeof v == "object") { 254 | this.x += v.x; 255 | this.y += v.y; 256 | } 257 | return this; 258 | } 259 | /** 260 | * 将当前二维向量减以指定数字【Subtract the current 2D vector to specify a number】 261 | * @param v 262 | * @returns 263 | */ 264 | public substract(v : number|Vector2) : Vector2 { 265 | if(typeof v === "number") { 266 | this.x -= v; 267 | this.y -= v; 268 | } 269 | else if(typeof v == "object") { 270 | this.x -= v.x; 271 | this.y -= v.y; 272 | } 273 | return this; 274 | } 275 | /** 276 | * 将当前二维向量乘以指定数字 【Multiplies the current 2D vector by the specified number】 277 | * @param v 278 | * @returns 279 | */ 280 | public multiply(v : number) : Vector2 { 281 | this.x *= v; 282 | this.y *= v; 283 | return this; 284 | } 285 | /** 286 | * 将当前二维向量除以指定数字 【Divides the current 2D vector by the specified number】 287 | * @param v 288 | * @returns 289 | */ 290 | public divide(v : number) : Vector2 { 291 | this.x /= v; 292 | this.y /= v; 293 | return this; 294 | } 295 | /** 296 | * Test two vector2's value is equal 297 | */ 298 | public equal(another : Vector2) : boolean { 299 | return this.x == another.x && this.y == another.y; 300 | } 301 | /** 302 | * 转为字符串 303 | * @returns 304 | */ 305 | public toString() : string { 306 | return `{x=${this.x},y=${this.y}}`; 307 | } 308 | } 309 | 310 | -------------------------------------------------------------------------------- /src/DynamicRender.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { App, defineComponent } from 'vue' 3 | const component: ReturnType & { 4 | install(app: App): void 5 | } 6 | export default component 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "declarationDir": "lib", 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": [ 18 | "src/*" 19 | ] 20 | }, 21 | "lib": [ 22 | "esnext", 23 | "dom", 24 | "dom.iterable", 25 | "scripthost" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.tsx", 31 | "src/**/*.vue", 32 | "tests/**/*.ts", 33 | "tests/**/*.tsx" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const resolve = dir => path.join(__dirname, dir) 4 | 5 | module.exports = { 6 | pages: { 7 | index: { 8 | entry: 'examples/main.js', 9 | template: 'examples/index.html', 10 | filename: 'index.html' 11 | } 12 | }, 13 | publicPath: './', 14 | chainWebpack: config => { 15 | config.module 16 | .rule('js') 17 | .include 18 | .add(resolve('src')) 19 | .end() 20 | .use('babel') 21 | .loader('babel-loader') 22 | .tap(option => { 23 | return option 24 | }) 25 | } 26 | } --------------------------------------------------------------------------------