Compare commits

..

No commits in common. "master" and "v0.0.5" have entirely different histories.

983 changed files with 49510 additions and 134927 deletions

View File

@ -1,46 +0,0 @@
{
"packages": [
"packages/dockview-core",
"packages/dockview-vue",
"packages/dockview-react",
"packages/dockview"
],
"sandboxes": [
"/packages/docs/sandboxes/constraints-dockview",
"/packages/docs/sandboxes/customheader-dockview",
"/packages/docs/sandboxes/demo-dockview",
"/packages/docs/sandboxes/dnd-dockview",
"/packages/docs/sandboxes/dockview-app",
"/packages/docs/sandboxes/editor-gridview",
"/packages/docs/sandboxes/events-dockview",
"/packages/docs/sandboxes/externaldnd-dockview",
"/packages/docs/sandboxes/floatinggroup-dockview",
"/packages/docs/sandboxes/fullwidthtab-dockview",
"/packages/docs/sandboxes/headeractions-dockview",
"/packages/docs/sandboxes/groupcontol-dockview",
"/packages/docs/sandboxes/iframe-dockview",
"/packages/docs/sandboxes/keyboard-dockview",
"/packages/docs/sandboxes/layout-dockview",
"/packages/docs/sandboxes/lockedgroup-dockview",
"/packages/docs/sandboxes/maximizegroup-dockview",
"/packages/docs/sandboxes/nativeapp-dockview",
"/packages/docs/sandboxes/nested-dockview",
"/packages/docs/sandboxes/popoutgroup-dockview",
"/packages/docs/sandboxes/rendering-dockview",
"/packages/docs/sandboxes/rendermode-dockview",
"/packages/docs/sandboxes/resize-dockview",
"/packages/docs/sandboxes/resizecontainer-dockview",
"/packages/docs/sandboxes/scrollbars-dockview",
"/packages/docs/sandboxes/simple-dockview",
"/packages/docs/sandboxes/simple-gridview",
"/packages/docs/sandboxes/simple-paneview",
"/packages/docs/sandboxes/tabheight-dockview",
"/packages/docs/sandboxes/updatetitle-dockview",
"/packages/docs/sandboxes/watermark-dockview",
"/packages/docs/sandboxes/javascript/fullwidthtab-dockview",
"/packages/docs/sandboxes/javascript/simple-dockview",
"/packages/docs/sandboxes/javascript/tabheight-dockview",
"/packages/docs/sandboxes/javascript/vanilla-dockview"
],
"node": "18"
}

View File

@ -1,10 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single

21
.eslintrc Normal file
View File

@ -0,0 +1,21 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-case-declarations":"off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-non-null-assertion":"off"
}
}

View File

@ -1,19 +0,0 @@
module.exports = {
root: true,
parserOptions: {
sourceType: 'module',
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
rules: {
'no-case-declarations': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
},
};

View File

@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
[dockview.dev](https://dockview.dev) provides a number of examples with Code Sandbox templates. Are you able to produce the bug by forking one of those templates? Sharing a link to the forked sandbox with the bug would be extremely helpful.
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -3,60 +3,60 @@
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: 'CodeQL'
name: "CodeQL"
on:
# push:
# branches: [master]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [master]
schedule:
- cron: '0 4 * * *'
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 4 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v4
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,46 +0,0 @@
name: Deploy Docs
on:
workflow_dispatch:
jobs:
deploy-nightly-demo-app:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- run: yarn install
- run: npm run build
working-directory: packages/dockview-core
- run: npm run build
working-directory: packages/dockview
- run: npm run build
working-directory: packages/dockview-vue
- run: npm run build
working-directory: packages/dockview-react
- run: npm run build
working-directory: packages/docs
- run: npm run docs
working-directory: .
- run: npm run package-docs
working-directory: .
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
TARGET_FOLDER: .
CLEAN: true # Automatically remove deleted files from the deploy branch

40
.github/workflows/deploy_nightly.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Deploy Nightly Demo App
on:
schedule:
- cron: '0 3 * * *' # every day at 3 am UTC
jobs:
deploy-nightly-demo-app:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '12.x'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run bootstrap-no-hoist
- run: npm run build
- run: npm run test
- run: npm run package-all
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: output # The folder the action should deploy.
TARGET_FOLDER: output
CLEAN: true # Automatically remove deleted files from the deploy branch

View File

@ -7,27 +7,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# might be required for sonar to work correctly
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: '20.x'
node-version: '12.x'
- uses: actions/cache@v4
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- run: yarn
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm run bootstrap
- run: npm run build
- run: npm run test:cov
- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v5
- run: npm run test-cov
- run: npm run codecov-publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
CI: true
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,78 +0,0 @@
name: Publish to npm
env:
NPM_CONFIG_PROVENANCE: true
on:
workflow_dispatch:
release:
types: [published]
jobs:
publish:
if: github.event_name == 'release'
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- run: yarn
- name: Publish dockview-core
run: npm publish --provenance
working-directory: packages/dockview-core
- name: Publish dockview
run: npm publish --provenance
working-directory: packages/dockview
- name: Publish dockview-vue
run: npm publish --provenance
working-directory: packages/dockview-vue
- name: Publish dockview-react
run: npm publish --provenance
working-directory: packages/dockview-react
publish-experimental:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- run: yarn
- run: npm run set-experimental-versions
- name: Publish dockview-core
run: npm publish --provenance --tag experimental
working-directory: packages/dockview-core
- name: Publish dockview
run: npm publish --provenance --tag experimental
working-directory: packages/dockview
- name: Publish dockview-vue
run: npm publish --provenance --tag experimental
working-directory: packages/dockview-vue
- name: Publish dockview-react
run: npm publish --provenance --tag experimental
working-directory: packages/dockview-react

9
.gitignore vendored
View File

@ -7,11 +7,4 @@ typedocs/
.DS_Store
*-debug.log
.build
storybook-static/
.rollup.cache/
test-report.xml
*.code-workspace
yarn-error.log
/build
/docs/
/generated/
storybook-static/

View File

@ -3,13 +3,7 @@
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"esbenp.prettier-vscode",
"redhat.vscode-yaml",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"vue.volar"
],
"recommendations": ["esbenp.prettier-vscode", "redhat.vscode-yaml"],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 mathuo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

103
README.md
View File

@ -1,38 +1,97 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
<p>Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support</p>
</div>
---
[![npm version](https://badge.fury.io/js/dockview-core.svg)](https://www.npmjs.com/package/dockview-core)
[![npm](https://img.shields.io/npm/dm/dockview-core)](https://www.npmjs.com/package/dockview-core)
![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)
[![npm version](https://badge.fury.io/js/dockview.svg)](https://www.npmjs.com/package/dockview)
[![CI Build](https://github.com/mathuo/dockview/workflows/CI/badge.svg)](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=coverage)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview-core)](https://bundlephobia.com/result?p=dockview-core)
[![codecov](https://codecov.io/gh/mathuo/dockview/branch/master/graph/badge.svg?token=BF083TK64H)](https://codecov.io/gh/mathuo/dockview/branch/master)
##
![](packages/docs/static/img/splashscreen.gif)
A zero dependency layout manager based on the layering of split-view components with ReactJS support.
- View the live demo [here](https://mathuo.github.io/dockview/).
- Storybook demo [here](https://mathuo.github.io/dockview/output/storybook-static).
- Automatically generated TypeDocs can be found [here](https://mathuo.github.io/dockview/output/docs/index.html).
Please see the website: https://dockview.dev
## Installation
You can install the project from [npm](https://www.npmjs.com/package/dockview). The project comes with TypeScript typings.
## Features
```bash
npm install --save dockview
```
- Serialization / deserialization with full layout management
- Support for split-views, grid-views and 'dockable' views
- Themeable and customizable
- Tab and Group docking / Drag n' Drop
- Popout Windows
- Floating Groups
- Extensive API
- Supports Shadow DOMs
- High test coverage
- Documentation website with live examples
- Transparent builds and Code Analysis
- Security at mind - verifed publishing and builds through GitHub Actions
## Configuration
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#user-content-provenance).
You must import the core css stylesheet but you are free to supply your own theming in addition to the core stylesheet. The location to reference for the stylesheet is
```
dockview/dist/styles.css
```
By default the seperator between panels is `transparent` but this can be set through the CSS varibable `--separator-border`. Alternatively, or if you require the `DockviewReact` you should attach the classname of an included theme; either `dockview-theme-dark` or `dockview-theme-light`.
### Sandbox examples
- [Dockview](https://codesandbox.io/s/simple-dockview-t6491)
- [Gridview](https://codesandbox.io/s/simple-gridview-jrp0n)
- [Splitview](https://codesandbox.io/s/simple-splitview-l53nn)
- [Paneview](https://codesandbox.io/s/simple-paneview-v8qvb)
## React
### Splitview
```javascript
import {
ISplitviewPanelProps,
Orientation,
SplitviewReact,
SplitviewReadyEvent
} from "dockview";
const components = {
"my-component": (props: ISplitviewPanelProps) => {
return (
<div>
<span>This is a panel</span>
<span>{props.arbitraryProp}</span>
</div>
)
}
}
const Example = () => {
const onReady = (event: SplitviewReadyEvent) => {
event.addPanel({
id: "panel-1",
component: "my-component",
params: {
arbitraryProp: "Hello World"
}
});
event.addPanel({
id: "panel-2",
component: "my-component",
params: {
arbitraryProp: "World Hello"
}
});
}
return (
<SplitviewReact
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
)
}
```
## Run the demo locally

View File

@ -1,8 +0,0 @@
# Reporting a Vulnerability
- Dockview is an entirely open source project.
- All build and publication scripts use public Github Action files found [here](https://github.com/mathuo/dockview/tree/master/.github/workflows).
- All npm publications are verified through the use of [provenance statements](https://docs.npmjs.com/generating-provenance-statements/).
- All builds are scanned with SonarCube and outputs can be found [here](https://sonarcloud.io/summary/overall?id=mathuo_dockview).
If you believe you have found a security or vulnerability issue please send a complete example to github.mathuo@gmail.com where it will be investigated.

View File

@ -1 +0,0 @@
import '@testing-library/jest-dom';

20
jest.config.base.js Normal file
View File

@ -0,0 +1,20 @@
const {join, normalize} = require("path");
const tsconfig = normalize(join(__dirname, "tsconfig.test.json"))
module.exports = {
displayName: { name: "root" },
preset: "ts-jest",
projects: ["<rootDir>/packages/*/jest.config.js"],
transform: {
"^.+\\.tsx?$":"ts-jest"
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
globals: {
"ts-jest": {
tsconfig,
experimental: true,
compilerHost: true
}
}
}

16
jest.config.js Normal file
View File

@ -0,0 +1,16 @@
const baseConfig = require("./jest.config.base");
module.exports = {
...baseConfig,
displayName: { name: "root", color: "blue" },
projects: ["<rootDir>/packages/*/jest.config.js"],
collectCoverage: true,
collectCoverageFrom:[
"<rootDir>/packages/*/src/**/*.{js,jsx,ts,tsx}",
],
coveragePathIgnorePatterns: [
"/node_modules/",
"<rootDir>packages/*/src/__tests__/",
],
coverageDirectory: "coverage"
};

View File

@ -1,17 +0,0 @@
import { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
displayName: { name: 'root', color: 'blue' },
projects: ['<rootDir>/packages/*/jest.config.ts'],
collectCoverage: true,
collectCoverageFrom: ['<rootDir>/packages/*/src/**/*.{js,jsx,ts,tsx}'],
coveragePathIgnorePatterns: [
'/node_modules/',
'<rootDir>/packages/*/src/__tests__/',
],
coverageDirectory: 'coverage',
testResultsProcessor: 'jest-sonar-reporter',
};
export default config;

View File

@ -1,12 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "4.2.5",
"npmClient": "yarn",
"command": {
"publish": {
"message": "chore(release): publish %s"
}
}
}
"packages": [
"packages/*"
],
"version": "0.0.5"
}

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"module": "es6",
"declaration": true,
"target": "es5",
"moduleResolution": "node",
"esModuleInterop": true,
"downlevelIteration": true,
"incremental": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"allowUnreachableCode": false,
"forceConsistentCasingInFileNames": true,
// "strict": true,
"strictBindCallApply": true, // pass
"alwaysStrict": true, // pass
"noImplicitThis": true, // pass
"strictFunctionTypes": true, // pass
"strictNullChecks": false,
"strictPropertyInitialization": false,
"lib": [
"ES2015",
"ES2016.Array.Include",
"ES2017.String",
"ES2018.Promise",
"DOM",
]
}
}

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"target": "es5",
"esModuleInterop": true,
"downlevelIteration": true,
"incremental": true,
"sourceMap": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"allowUnreachableCode": false,
"forceConsistentCasingInFileNames": true,
// "strict": true,
"strictBindCallApply": true, // pass
"alwaysStrict": true, // pass
"noImplicitThis": true, // pass
"strictFunctionTypes": true, // pass
"strictNullChecks": false,
"strictPropertyInitialization": false,
"lib": [
"ES2015",
"ES2016.Array.Include",
"ES2017.String",
"ES2018.Promise",
"DOM",
]
}
}

18027
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,81 +1,61 @@
{
"name": "dockview-monorepo-root",
"private": true,
"description": "Monorepo for https://github.com/mathuo/dockview",
"homepage": "https://github.com/mathuo/dockview#readme",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mathuo/dockview.git"
},
"license": "MIT",
"author": "https://github.com/mathuo",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "lerna run build --scope '{dockview-core,dockview,dockview-vue,dockview-react}'",
"clean": "lerna run clean",
"docs": "typedoc",
"generate-docs": "node scripts/docs.mjs",
"lint": "eslint packages/**/src/** --ext .ts,.tsx,.js,.jsx",
"package": "node scripts/package.js",
"package-docs": "node scripts/package-docs.js",
"set-experimental-versions": "node scripts/set-experimental-versions",
"test": "jest",
"test:cov": "jest --coverage",
"version": "lerna version"
},
"resolutions": {
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.6",
"@testing-library/react": "^14.1.2",
"@total-typescript/shoehorn": "^0.1.1",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"fs-extra": "^11.2.0",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-dart-sass": "^1.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-sonar-reporter": "^2.0.0",
"jsdom": "^23.0.1",
"lerna": "^8.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^5.0.5",
"rollup": "^4.9.2",
"rollup-plugin-postcss": "^4.0.2",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typedoc": "^0.25.6",
"typescript": "^5.3.3",
"vite": "^5.1.5",
"vue": "^3.4.21",
"vue-sfc-loader": "^0.1.0",
"vue-tsc": "^2.0.5"
},
"engines": {
"node": ">=18.0"
}
"name": "splitview-root",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"lint": "eslint packages/**/src/** --ext .ts,.tsx,.js,.jsx",
"package": "node scripts/package.js",
"package-all": "npm run build-demo && npm run docs && node scripts/package.js",
"build": "lerna run build --ignore splitview-demo",
"build-demo": "lerna run build --scope splitview-demo",
"docs": "lerna run docs --scope dockview",
"clean": "lerna run clean",
"bootstrap": "lerna bootstrap --hoist",
"bootstrap-no-hoist": "lerna bootstrap",
"test-cov": "jest --coverage",
"codecov-publish": "codecov",
"version-beta-build": "lerna version prerelease --preid beta",
"publish-app": "lerna publish"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mathuo/dockview.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"homepage": "https://github.com/mathuo/dockview#readme",
"devDependencies": {
"@testing-library/dom": "^7.28.1",
"@types/jest": "^26.0.15",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1",
"codecov": "^3.8.1",
"cross-env": "^7.0.2",
"css-loader": "^3.6.0",
"eslint": "^7.14.0",
"fs-extra": "^9.0.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-sass": "^4.1.0",
"jest": "^26.6.3",
"jsdom": "^16.4.0",
"lerna": "^3.22.1",
"merge2": "^1.4.1",
"node-sass": "^4.14.1",
"rimraf": "^3.0.2",
"sass-loader": "^8.0.2",
"style-loader": "^1.3.0",
"ts-jest": "^26.4.4",
"ts-loader": "^7.0.5",
"typescript": "^4.1.2",
"webpack": "^5.6.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {}
}

View File

@ -1,18 +0,0 @@
# Project Structure
This mono-repository has a number of packages containing the code for the [dockview](https://www.npmjs.com/package/dockview) library and the documentation website [dockview.dev](dockview.dev).
## dockview-core
- Contains the core logic for the dockview library.
- Written entirely in vanilla JavaScript/TypeScript.
## dockview
- Depends on `dockview-core`.
- Exports a `React` wrapper.
- Published as [dockview](https://www.npmjs.com/package/dockview) on npm.
## docs
- Code for [dockview.dev](dockview.dev).

View File

@ -1,56 +0,0 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews with ReactJS support written in TypeScript</p>
</div>
---
[![npm version](https://badge.fury.io/js/dockview.svg)](https://www.npmjs.com/package/dockview)
[![npm](https://img.shields.io/npm/dm/dockview)](https://www.npmjs.com/package/dockview)
[![CI Build](https://github.com/mathuo/dockview/workflows/CI/badge.svg)](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=coverage)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview)](https://bundlephobia.com/result?p=dockview)
##
Please see the website: https://dockview.dev
## Features
- Serialization / deserialization with full layout management
- Support for split-views, grid-views and 'dockable' views
- Themeable and customizable
- Tab and Group docking / Drag n' Drop
- Popout Windows
- Floating Groups
- Extensive API
- Supports Shadow DOMs
- High test coverage
- Documentation website with live examples
- Transparent builds and Code Analysis
- Security at mind - verifed publishing and builds through GitHub Actions
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
## Quick start
Dockview has a peer dependency on `react >= 16.8.0` and `react-dom >= 16.8.0`. You can install dockview from [npm](https://www.npmjs.com/package/dockview).
```
npm install --save dockview
```
Within your project you must import or reference the stylesheet at `dockview/dist/styles/dockview.css` and attach a theme.
```css
@import '~dockview/dist/styles/dockview.css';
```
You should also attach a dockview theme to an element containing your components. For example:
```html
<body classname="dockview-theme-dark"></body>
```

View File

@ -1,34 +0,0 @@
import { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
roots: ['<rootDir>/packages/dockview-angular'],
modulePaths: ['<rootDir>/packages/dockview-angular/src'],
displayName: { name: 'dockview-angular', color: 'blue' },
rootDir: '../../',
collectCoverageFrom: [
'<rootDir>/packages/dockview-angular/src/**/*.{js,jsx,ts,tsx}',
],
setupFiles: [
// '<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/resizeObserver.js',
],
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/'],
modulePathIgnorePatterns: [
// '<rootDir>/packages/dockview-angular/src/__tests__/__mocks__',
// '<rootDir>/packages/dockview-angular/src/__tests__/__test_utils__',
],
coverageDirectory: '<rootDir>/packages/dockview-angular/coverage/',
testResultsProcessor: 'jest-sonar-reporter',
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.test.json',
},
],
},
};
export default config;

View File

@ -1,59 +0,0 @@
{
"name": "dockview-angular",
"version": "4.2.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
"split-view",
"gridview",
"grid-view",
"dockview",
"dock-view",
"grid",
"tabs",
"layout",
"layout manager",
"dock layout",
"dock",
"docking",
"splitter",
"drag-and-drop",
"drag",
"drop",
"react",
"react-component"
],
"homepage": "https://github.com/mathuo/dockview",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/mathuo/dockview.git"
},
"license": "MIT",
"author": "https://github.com/mathuo",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "npm run build:package && npm run build:bundles",
"build:bundles": "rollup -c",
"build:cjs": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.json --verbose --extendedDiagnostics",
"build:css": "gulp sass",
"build:esm": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.esm.json --verbose --extendedDiagnostics",
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
"clean": "rimraf dist/ .build/ .rollup.cache/",
"prepublishOnly": "npm run rebuild && npm run test",
"rebuild": "npm run clean && npm run build",
"test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview",
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
},
"dependencies": {
"dockview-core": "^4.2.5"
}
}

View File

@ -1,113 +0,0 @@
/* eslint-disable */
const { join } = require('path');
const typescript = require('@rollup/plugin-typescript');
const terser = require('@rollup/plugin-terser');
const postcss = require('rollup-plugin-postcss');
const nodeResolve = require('@rollup/plugin-node-resolve');
const { name, version, homepage, license } = require('./package.json');
const main = join(__dirname, './scripts/rollupEntryTarget.ts');
const mainNoStyles = join(__dirname, './src/index.ts');
const outputDir = join(__dirname, 'dist');
function outputFile(format, isMinified, withStyles) {
let filename = join(outputDir, name);
if (format !== 'umd') {
filename += `.${format}`;
}
if (isMinified) {
filename += '.min';
}
if (!withStyles) {
filename += '.noStyle';
}
return `${filename}.js`;
}
function getInput(options) {
const { withStyles } = options;
if (withStyles) {
return main;
}
return mainNoStyles;
}
function createBundle(format, options) {
const { withStyles, isMinified } = options;
const input = getInput(options);
const file = outputFile(format, isMinified, withStyles);
const external = [];
const output = {
file,
format,
sourcemap: true,
globals: {},
banner: [
`/**`,
` * ${name}`,
` * @version ${version}`,
` * @link ${homepage}`,
` * @license ${license}`,
` */`,
].join('\n'),
};
const plugins = [
nodeResolve({
include: ['node_modules/dockview-core/**'],
}),
typescript({
tsconfig: 'tsconfig.esm.json',
}),
];
if (isMinified) {
plugins.push(terser());
}
if (withStyles) {
plugins.push(postcss());
}
if (format === 'umd') {
output['name'] = name;
}
external.push('react', 'react-dom');
if (format === 'umd') {
output.globals['react'] = 'React';
output.globals['react-dom'] = 'ReactDOM';
}
return {
input,
output,
plugins,
external,
};
}
module.exports = [
// amd
createBundle('amd', { withStyles: false, isMinified: false }),
createBundle('amd', { withStyles: true, isMinified: false }),
createBundle('amd', { withStyles: false, isMinified: true }),
createBundle('amd', { withStyles: true, isMinified: true }),
// umd
createBundle('umd', { withStyles: false, isMinified: false }),
createBundle('umd', { withStyles: true, isMinified: false }),
createBundle('umd', { withStyles: false, isMinified: true }),
createBundle('umd', { withStyles: true, isMinified: true }),
// cjs
createBundle('cjs', { withStyles: true, isMinified: false }),
// esm
createBundle('esm', { withStyles: true, isMinified: false }),
createBundle('esm', { withStyles: true, isMinified: true }),
];

View File

@ -1,2 +0,0 @@
import '../dist/styles/dockview.css';
export * from '../src/index';

View File

@ -1,5 +0,0 @@
describe('empty', () => {
test('that passes', () => {
expect(true).toBeTruthy();
});
});

View File

@ -1 +0,0 @@
export * from 'dockview-core';

View File

@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ES2020",
"moduleResolution": "node",
"target": "es6",
"outDir": "dist/esm",
"tsBuildInfoFile": ".build/tsconfig.tsbuildinfo.esm",
"jsx": "react",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["**/node_modules", "src/__tests__"]
}

View File

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist/cjs",
"tsBuildInfoFile": ".build/tsconfig.tsbuildinfo.cjs",
"jsx": "react",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["**/node_modules", "src/__tests__"]
}

View File

@ -1,5 +0,0 @@
{
"extends": ["../../typedoc.base.json"],
"entryPoints": ["src/index.ts"],
"exclude": ["**/dist/**"]
}

View File

@ -1,38 +0,0 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
</div>
---
[![npm version](https://badge.fury.io/js/dockview.svg)](https://www.npmjs.com/package/dockview)
[![npm](https://img.shields.io/npm/dm/dockview)](https://www.npmjs.com/package/dockview)
[![CI Build](https://github.com/mathuo/dockview/workflows/CI/badge.svg)](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=coverage)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview)](https://bundlephobia.com/result?p=dockview)
##
![](packages/docs/static/img/splashscreen.gif)
Please see the website: https://dockview.dev
## Features
- Serialization / deserialization with full layout management
- Support for split-views, grid-views and 'dockable' views
- Themeable and customizable
- Tab and Group docking / Drag n' Drop
- Popout Windows
- Floating Groups
- Extensive API
- Supports Shadow DOMs
- High test coverage
- Documentation website with live examples
- Transparent builds and Code Analysis
- Security at mind - verifed publishing and builds through GitHub Actions
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).

View File

@ -1,13 +0,0 @@
const gulp = require('gulp');
const gulpSass = require('gulp-dart-sass');
const concat = require('gulp-concat');
gulp.task('sass', () => {
return gulp
.src('./src/**/*.scss')
.pipe(gulpSass().on('error', gulpSass.logError))
.pipe(concat('dockview.css'))
.pipe(gulp.dest('./dist/styles/'));
});
gulp.task('run', gulp.series(['sass']));

View File

@ -1,34 +0,0 @@
import { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
roots: ['<rootDir>/packages/dockview-core'],
modulePaths: ['<rootDir>/packages/dockview-core/src'],
displayName: { name: 'dockview-core', color: 'blue' },
rootDir: '../../',
collectCoverageFrom: [
'<rootDir>/packages/dockview-core/src/**/*.{js,jsx,ts,tsx}',
],
setupFiles: [
'<rootDir>/packages/dockview-core/src/__tests__/__mocks__/resizeObserver.js',
],
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/'],
modulePathIgnorePatterns: [
'<rootDir>/packages/dockview-core/src/__tests__/__mocks__',
'<rootDir>/packages/dockview-core/src/__tests__/__test_utils__',
],
coverageDirectory: '<rootDir>/packages/dockview-core/coverage/',
testResultsProcessor: 'jest-sonar-reporter',
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.test.json',
},
],
},
};
export default config;

View File

@ -1,56 +0,0 @@
{
"name": "dockview-core",
"version": "4.2.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
"split-view",
"gridview",
"grid-view",
"dockview",
"dock-view",
"grid",
"tabs",
"layout",
"layout manager",
"dock layout",
"dock",
"docking",
"splitter",
"drag-and-drop",
"drag",
"drop",
"react",
"react-component"
],
"homepage": "https://github.com/mathuo/dockview",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/mathuo/dockview.git"
},
"license": "MIT",
"author": "https://github.com/mathuo",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "npm run build:package && npm run build:bundles",
"build:bundles": "rollup -c",
"build:cjs": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.json --verbose --extendedDiagnostics",
"build:css": "gulp sass",
"build:esm": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.esm.json --verbose --extendedDiagnostics",
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
"clean": "rimraf dist/ .build/ .rollup.cache/",
"prepublishOnly": "npm run rebuild && npm run test",
"rebuild": "npm run clean && npm run build",
"test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-core",
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-core --coverage"
}
}

View File

@ -1,102 +0,0 @@
/* eslint-disable */
const { join } = require('path');
const typescript = require('@rollup/plugin-typescript');
const terser = require('@rollup/plugin-terser');
const postcss = require('rollup-plugin-postcss');
const { name, version, homepage, license } = require('./package.json');
const main = join(__dirname, './scripts/rollupEntryTarget.ts');
const mainNoStyles = join(__dirname, './src/index.ts');
const outputDir = join(__dirname, 'dist');
function outputFile(format, isMinified, withStyles) {
let filename = join(outputDir, name);
if (format !== 'umd') {
filename += `.${format}`;
}
if (isMinified) {
filename += '.min';
}
if (!withStyles) {
filename += '.noStyle';
}
return `${filename}.js`;
}
function getInput(options) {
const { withStyles } = options;
if (withStyles) {
return main;
}
return mainNoStyles;
}
function createBundle(format, options) {
const { withStyles, isMinified, isReact } = options;
const input = getInput(options);
const file = outputFile(format, isMinified, withStyles, isReact);
const external = [];
const output = {
file,
format,
sourcemap: true,
globals: {},
banner: [
`/**`,
` * ${name}`,
` * @version ${version}`,
` * @link ${homepage}`,
` * @license ${license}`,
` */`,
].join('\n'),
};
const plugins = [
typescript({
tsconfig: 'tsconfig.esm.json',
}),
];
if (isMinified) {
plugins.push(terser());
}
if (withStyles) {
plugins.push(postcss());
}
if (format === 'umd') {
output['name'] = name;
}
return {
input,
output,
plugins,
external,
};
}
module.exports = [
// amd
createBundle('amd', { withStyles: false, isMinified: false }),
createBundle('amd', { withStyles: true, isMinified: false }),
createBundle('amd', { withStyles: false, isMinified: true }),
createBundle('amd', { withStyles: true, isMinified: true }),
// umd
createBundle('umd', { withStyles: false, isMinified: false }),
createBundle('umd', { withStyles: true, isMinified: false }),
createBundle('umd', { withStyles: false, isMinified: true }),
createBundle('umd', { withStyles: true, isMinified: true }),
// cjs
createBundle('cjs', { withStyles: true, isMinified: false }),
// esm
createBundle('esm', { withStyles: true, isMinified: false }),
createBundle('esm', { withStyles: true, isMinified: true }),
];

View File

@ -1,2 +0,0 @@
import '../dist/styles/dockview.css';
export * from '../src/index';

View File

@ -1,47 +0,0 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import {
TabPartInitParameters,
IContentRenderer,
ITabRenderer,
} from '../../dockview/types';
import { PanelUpdateEvent } from '../../panel/types';
import { TabLocation } from '../../dockview/framework';
export class DockviewPanelModelMock implements IDockviewPanelModel {
constructor(
readonly contentComponent: string,
readonly content: IContentRenderer,
readonly tabComponent: string,
readonly tab: ITabRenderer
) {
//
}
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return this.tab;
}
init(params: TabPartInitParameters): void {
//
}
updateParentGroup(
group: DockviewGroupPanel,
isPanelVisible: boolean
): void {
//
}
update(event: PanelUpdateEvent): void {
//
}
layout(width: number, height: number): void {
//
}
dispose(): void {
//
}
}

View File

@ -1,45 +0,0 @@
import { fromPartial } from '@total-typescript/shoehorn';
export function setupMockWindow() {
const listeners: Record<string, (() => void)[]> = {};
let width = 1000;
let height = 2000;
return fromPartial<Window>({
addEventListener: (type: string, listener: () => void) => {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
if (type === 'load') {
listener();
}
},
removeEventListener: (type: string, listener: () => void) => {
if (listeners[type]) {
const index = listeners[type].indexOf(listener);
if (index > -1) {
listeners[type].splice(index, 1);
}
}
},
dispatchEvent: (event: Event) => {
const items = listeners[event.type];
if (!items) {
return;
}
items.forEach((item) => item());
},
document: document,
close: () => {
listeners['beforeunload']?.forEach((f) => f());
},
get innerWidth() {
return width++;
},
get innerHeight() {
return height++;
},
});
}

View File

@ -1,13 +0,0 @@
class ResizeObserver {
observe() {
// do nothing
}
unobserve() {
// do nothing
}
disconnect() {
// do nothing
}
}
window.ResizeObserver = ResizeObserver;

View File

@ -1,73 +0,0 @@
import React from 'react';
/**
* useful utility type to erase readonly signatures for testing purposes
*
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-mapped-type-modifiers-and-readonly-arrays
*/
export type Writable<T> = T extends object
? { -readonly [K in keyof T]: Writable<T[K]> }
: T;
export function setMockRefElement(node: Partial<HTMLElement>): void {
const mockRef = {
get current() {
return node;
},
set current(_value) {
//noop
},
};
jest.spyOn(React, 'useRef').mockReturnValueOnce(mockRef);
}
export function createOffsetDragOverEvent(params: {
clientX: number;
clientY: number;
}): Event {
const event = new Event('dragover', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'clientX', { get: () => params.clientX });
Object.defineProperty(event, 'clientY', { get: () => params.clientY });
return event;
}
/**
* `jest.runAllTicks` doesn't seem to exhaust all events in the micro-task queue so
* as a **hacky** alternative we'll wait for an empty Promise to complete which runs
* on the micro-task queue so will force a run-to-completion emptying the queue
* of any pending micro-task
*/
export function exhaustMicrotaskQueue(): Promise<void> {
return new Promise<void>((resolve) => resolve());
}
export const mockGetBoundingClientRect = ({
left,
top,
height,
width,
}: {
left: number;
top: number;
height: number;
width: number;
}) => {
const result = {
left,
top,
height,
width,
right: left + width,
bottom: top + height,
x: left,
y: top,
};
return {
...result,
toJSON: () => result,
};
};

View File

@ -1,69 +0,0 @@
import { PanelApiImpl } from '../../api/panelApi';
import { IPanel } from '../../panel/types';
describe('api', () => {
let api: PanelApiImpl;
beforeEach(() => {
api = new PanelApiImpl('dummy_id', 'fake-component');
});
test('updateParameters', () => {
const panel = {
update: jest.fn(),
} as Partial<IPanel>;
api.initialize(panel as IPanel);
expect(panel.update).toHaveBeenCalledTimes(0);
api.updateParameters({ keyA: 'valueA' });
expect(panel.update).toHaveBeenCalledTimes(1);
expect(panel.update).toHaveBeenCalledWith({
params: { keyA: 'valueA' },
});
});
test('should update isFcoused getter', () => {
expect(api.isFocused).toBeFalsy();
api._onDidChangeFocus.fire({ isFocused: true });
expect(api.isFocused).toBeTruthy();
api._onDidChangeFocus.fire({ isFocused: false });
expect(api.isFocused).toBeFalsy();
});
test('should update isActive getter', () => {
expect(api.isFocused).toBeFalsy();
api._onDidActiveChange.fire({ isActive: true });
expect(api.isActive).toBeTruthy();
api._onDidActiveChange.fire({ isActive: false });
expect(api.isActive).toBeFalsy();
});
test('should update isActive getter', () => {
expect(api.isVisible).toBeTruthy();
api._onDidVisibilityChange.fire({ isVisible: false });
expect(api.isVisible).toBeFalsy();
api._onDidVisibilityChange.fire({ isVisible: true });
expect(api.isVisible).toBeTruthy();
});
test('should update width and height getter', () => {
expect(api.height).toBe(0);
expect(api.width).toBe(0);
api._onDidDimensionChange.fire({ height: 10, width: 20 });
expect(api.height).toBe(10);
expect(api.width).toBe(20);
api._onDidDimensionChange.fire({ height: 20, width: 10 });
expect(api.height).toBe(20);
expect(api.width).toBe(10);
});
});

View File

@ -1,155 +0,0 @@
import {
SplitviewApi,
PaneviewApi,
GridviewApi,
DockviewApi,
} from '../../api/component.api';
import { GridviewComponent } from '../../gridview/gridviewComponent';
import { PaneviewComponent } from '../../paneview/paneviewComponent';
import { SplitviewComponent } from '../../splitview/splitviewComponent';
import { DockviewComponent } from '../../dockview/dockviewComponent';
describe('component.api', () => {
describe('splitview', () => {
test('splitviewapi', () => {
const list: (keyof SplitviewComponent)[] = [
'minimumSize',
'maximumSize',
'height',
'width',
'length',
'orientation',
'onDidLayoutChange',
'onDidAddView',
'onDidRemoveView',
'panels',
'focus',
'toJSON',
];
for (const _ of list) {
const f = jest.fn();
const component: Partial<SplitviewComponent> = {
[_]: f(),
};
const cut = new SplitviewApi(<SplitviewComponent>component);
(cut as any)[_];
expect(f).toBeCalledTimes(1);
}
});
});
describe('paneview', () => {
test('panviewapi', () => {
const list: (keyof PaneviewComponent)[] = [
'minimumSize',
'maximumSize',
'height',
'width',
'onDidLayoutChange',
'onDidAddView',
'onDidRemoveView',
'panels',
'focus',
'toJSON',
];
for (const _ of list) {
const f = jest.fn();
const component: Partial<PaneviewComponent> = {
[_]: f(),
};
const cut = new PaneviewApi(<PaneviewComponent>component);
(cut as any)[_];
expect(f).toBeCalledTimes(1);
}
});
});
describe('gridview', () => {
test('gridviewapi', () => {
const list: (keyof GridviewComponent)[] = [
'minimumHeight',
'maximumHeight',
'minimumWidth',
'maximumWidth',
'width',
'height',
'onDidLayoutChange',
'orientation',
'focus',
'toJSON',
'onDidActiveGroupChange',
'onDidAddGroup',
'onDidRemoveGroup',
'onDidLayoutFromJSON',
];
for (const _ of list) {
const f = jest.fn();
const component: Partial<GridviewComponent> = {
[_]: f(),
};
const cut = new GridviewApi(<GridviewComponent>component);
(cut as any)[_];
expect(f).toBeCalledTimes(1);
}
});
});
describe('dockview', () => {
test('dockviewapi', () => {
const list: (keyof DockviewComponent)[] = [
'minimumHeight',
'maximumHeight',
'minimumWidth',
'maximumWidth',
'width',
'height',
'size',
'totalPanels',
'onDidLayoutChange',
'panels',
'groups',
'activeGroup',
'activePanel',
'focus',
'closeAllGroups',
'toJSON',
'onDidActiveGroupChange',
'onDidAddGroup',
'onDidRemoveGroup',
'onDidActivePanelChange',
'onDidAddPanel',
'onDidRemovePanel',
'onDidLayoutFromJSON',
];
for (const _ of list) {
const f = jest.fn();
const component: Partial<DockviewComponent> = {
[_]: f(),
};
const cut = new DockviewApi(<DockviewComponent>component);
(cut as any)[_];
expect(f).toBeCalledTimes(1);
}
});
});
});

View File

@ -1,123 +0,0 @@
import { DockviewPanelApiImpl } from '../../api/dockviewPanelApi';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('groupPanelApi', () => {
test('title', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const panelMock = jest.fn<DockviewPanel, []>(() => {
return {
update: jest.fn(),
setTitle: jest.fn(),
} as any;
});
const panel = new panelMock();
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const cut = new DockviewPanelApiImpl(
panel,
group,
<DockviewComponent>accessor,
'fake-component'
);
cut.setTitle('test_title');
expect(panel.setTitle).toBeCalledTimes(1);
expect(panel.setTitle).toBeCalledWith('test_title');
});
test('updateParameters', () => {
const groupPanel: Partial<DockviewPanel> = {
id: 'test_id',
update: jest.fn(),
};
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
{}
);
const cut = new DockviewPanelApiImpl(
<DockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor,
'fake-component'
);
cut.updateParameters({ keyA: 'valueA' });
expect(groupPanel.update).toHaveBeenCalledWith({
params: { keyA: 'valueA' },
});
expect(groupPanel.update).toHaveBeenCalledTimes(1);
});
test('onDidGroupChange', () => {
const groupPanel: Partial<DockviewPanel> = {
id: 'test_id',
};
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
{}
);
const cut = new DockviewPanelApiImpl(
<DockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor,
'fake-component'
);
let events = 0;
const disposable = cut.onDidGroupChange(() => {
events++;
});
expect(events).toBe(0);
expect(cut.group).toBe(groupViewPanel);
const groupViewPanel2 = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
{}
);
cut.group = groupViewPanel2;
expect(events).toBe(1);
expect(cut.group).toBe(groupViewPanel2);
disposable.dispose();
});
});

View File

@ -1,179 +0,0 @@
import { fireEvent } from '@testing-library/dom';
import { DragHandler } from '../../dnd/abstractDragHandler';
import { IDisposable } from '../../lifecycle';
describe('abstractDragHandler', () => {
test('that className dv-dragged is added to element after dragstart event', () => {
jest.useFakeTimers();
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
expect(element.classList.contains('dv-dragged')).toBeFalsy();
fireEvent.dragStart(element);
expect(element.classList.contains('dv-dragged')).toBeTruthy();
jest.runAllTimers();
expect(element.classList.contains('dv-dragged')).toBeFalsy();
handler.dispose();
});
test('that iframes and webviews have pointerEvents=none set whilst drag action is in process', () => {
jest.useFakeTimers();
const element = document.createElement('div');
const iframe = document.createElement('iframe');
const webview = document.createElement('webview');
const span = document.createElement('span');
document.body.appendChild(element);
document.body.appendChild(iframe);
document.body.appendChild(webview);
document.body.appendChild(span);
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
expect(iframe.style.pointerEvents).toBeFalsy();
expect(webview.style.pointerEvents).toBeFalsy();
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragStart(element);
expect(iframe.style.pointerEvents).toBe('none');
expect(webview.style.pointerEvents).toBe('none');
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragEnd(element);
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
expect(span.style.pointerEvents).toBeFalsy();
handler.dispose();
});
test('that the disabling of pointerEvents is restored on a premature disposal of the handler', () => {
jest.useFakeTimers();
const element = document.createElement('div');
const iframe = document.createElement('iframe');
const webview = document.createElement('webview');
const span = document.createElement('span');
document.body.appendChild(element);
document.body.appendChild(iframe);
document.body.appendChild(webview);
document.body.appendChild(span);
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
expect(iframe.style.pointerEvents).toBeFalsy();
expect(webview.style.pointerEvents).toBeFalsy();
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragStart(element);
expect(iframe.style.pointerEvents).toBe('none');
expect(webview.style.pointerEvents).toBe('none');
expect(span.style.pointerEvents).toBeFalsy();
handler.dispose();
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
expect(span.style.pointerEvents).toBeFalsy();
});
test('that .preventDefault() is called for cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return true;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
handler.dispose();
});
test('that .preventDefault() is not called for non-cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return false;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0);
handler.dispose();
});
});

View File

@ -1,101 +0,0 @@
import {
getPaneData,
getPanelData,
LocalSelectionTransfer,
PanelTransfer,
PaneTransfer,
} from '../../dnd/dataTransfer';
describe('dataTransfer', () => {
describe('getPanelData', () => {
test('should be undefined when there is no local transfer object', () => {
expect(getPanelData()).toBeUndefined();
});
test('should be undefined when there is a local transfer object that is not a PanelTransfer', () => {
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
[new PaneTransfer('viewId', 'groupId')],
PaneTransfer.prototype
);
expect(getPanelData()).toBeUndefined();
});
test('should retrieve the PanelTransfer object when transfer is active', () => {
const transferObject = new PanelTransfer(
'viewId',
'groupId',
'panelId'
);
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
[transferObject],
PanelTransfer.prototype
);
expect(getPanelData()).toBe(transferObject);
});
test('should retrieve the PanelTransfer when a new transfer overrides an existing one', () => {
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
[new PaneTransfer('viewId', 'groupId')],
PaneTransfer.prototype
);
expect(getPanelData()).toBeUndefined();
const transferObject = new PanelTransfer(
'viewId',
'groupId',
'panelId'
);
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
[transferObject],
PanelTransfer.prototype
);
expect(getPanelData()).toBe(transferObject);
});
});
describe('getPaneData', () => {
test('should be undefined when there is no local transfer object', () => {
expect(getPaneData()).toBeUndefined();
});
test('should be undefined when there is a local transfer object that is not a PaneTransfer', () => {
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
[new PanelTransfer('viewId', 'groupId', 'panelId')],
PanelTransfer.prototype
);
expect(getPaneData()).toBeUndefined();
});
test('should retrieve the PaneTransfer object when transfer is active', () => {
const transferObject = new PaneTransfer('viewId', 'groupId');
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
[transferObject],
PaneTransfer.prototype
);
expect(getPaneData()).toBe(transferObject);
});
test('should retrieve the PanelTransfer when a new transfer overrides an existing one', () => {
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
[new PanelTransfer('viewId', 'groupId', 'panelId')],
PanelTransfer.prototype
);
expect(getPaneData()).toBeUndefined();
const transferObject = new PaneTransfer('viewId', 'groupId');
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
[transferObject],
PaneTransfer.prototype
);
expect(getPaneData()).toBe(transferObject);
});
});
});

View File

@ -1,401 +0,0 @@
import {
calculateQuadrantAsPercentage,
calculateQuadrantAsPixels,
directionToPosition,
Droptarget,
Position,
positionToDirection,
} from '../../dnd/droptarget';
import { fireEvent } from '@testing-library/dom';
import { createOffsetDragOverEvent } from '../__test_utils__/utils';
describe('droptarget', () => {
let element: HTMLElement;
let droptarget: Droptarget;
beforeEach(() => {
element = document.createElement('div');
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
fireEvent.dragEnter(element);
const event = new Event('dragover');
fireEvent(element, event);
expect(
(event as any)['__dockview_droptarget_event_is_used__']
).toBeTruthy();
});
test('that the drop target is removed when receiving a marked dragover event', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
const event = new Event('dragover');
(event as any)['__dockview_droptarget_event_is_used__'] = true;
fireEvent(element, event);
expect(element.querySelector('.dv-drop-target-dropzone')).toBeNull();
});
test('directionToPosition', () => {
expect(directionToPosition('above')).toBe('top');
expect(directionToPosition('below')).toBe('bottom');
expect(directionToPosition('left')).toBe('left');
expect(directionToPosition('right')).toBe('right');
expect(directionToPosition('within')).toBe('center');
expect(() => directionToPosition('bad_input' as any)).toThrow(
"invalid direction 'bad_input'"
);
});
test('positionToDirection', () => {
expect(positionToDirection('top')).toBe('above');
expect(positionToDirection('bottom')).toBe('below');
expect(positionToDirection('left')).toBe('left');
expect(positionToDirection('right')).toBe('right');
expect(positionToDirection('center')).toBe('within');
expect(() => positionToDirection('bad_input' as any)).toThrow(
"invalid position 'bad_input'"
);
});
test('non-directional', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
});
test('drop', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
jest.spyOn(target, 'clientWidth', 'get').mockImplementation(() => 200);
fireEvent(
target,
createOffsetDragOverEvent({
clientX: 19,
clientY: 0,
})
);
expect(position).toBeUndefined();
fireEvent.drop(target);
expect(position).toBe('left');
});
test('default', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
expect(droptarget.state).toBeUndefined();
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
const target = element.querySelector(
'.dv-drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
jest.spyOn(target, 'clientWidth', 'get').mockImplementation(() => 200);
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 19, clientY: 0 })
);
function check(
element: HTMLElement,
box: {
left: string;
top: string;
width: string;
height: string;
}
) {
expect(element.style.top).toBe(box.top);
expect(element.style.left).toBe(box.left);
expect(element.style.width).toBe(box.width);
expect(element.style.height).toBe(box.height);
}
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('left');
check(
element
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
left: '0px',
width: '50%',
height: '100%',
}
);
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 40, clientY: 19 })
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('top');
check(
element
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
left: '0px',
width: '100%',
height: '50%',
}
);
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 160, clientY: 81 })
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('bottom');
check(
element
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '50%',
left: '0px',
width: '100%',
height: '50%',
}
);
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 161, clientY: 0 })
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('right');
check(
element
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
left: '50%',
width: '50%',
height: '100%',
}
);
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
);
expect(droptarget.state).toBe('center');
expect(
(
element
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target);
expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.dv-drop-target');
expect(viewQuery.length).toBe(0);
});
describe('calculateQuadrantAsPercentage', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPercentage(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
describe('calculateQuadrantAsPixels', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPixels(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
});

View File

@ -1,31 +0,0 @@
import { addGhostImage } from '../../dnd/ghost';
describe('ghost', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.clearAllTimers();
});
test('that a custom class is added, the element is added to the document and all is removed afterwards', () => {
const dataTransferMock = jest.fn<Partial<DataTransfer>, []>(() => {
return {
setDragImage: jest.fn(),
};
});
const element = document.createElement('div');
const dataTransfer = <DataTransfer>new dataTransferMock();
addGhostImage(dataTransfer, element);
expect(element.className).toBe('dv-dragged');
expect(element.parentElement).toBe(document.body);
expect(dataTransfer.setDragImage).toBeCalledTimes(1);
expect(dataTransfer.setDragImage).toBeCalledWith(element, 0, 0);
jest.runAllTimers();
expect(element.className).toBe('');
expect(element.parentElement).toBe(null);
});
});

View File

@ -1,114 +0,0 @@
import { fireEvent } from '@testing-library/dom';
import { GroupDragHandler } from '../../dnd/groupDragHandler';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { DockviewComponent } from '../../dockview/dockviewComponent';
describe('groupDragHandler', () => {
test('that the dnd transfer object is setup and torndown', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id',
api: { location: { type: 'grid' } } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'test_accessor_id' } as DockviewComponent,
group
);
fireEvent.dragStart(element, new Event('dragstart'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeTruthy();
const transferObject =
LocalSelectionTransfer.getInstance<PanelTransfer>().getData(
PanelTransfer.prototype
)![0];
expect(transferObject).toBeTruthy();
expect(transferObject.viewId).toBe('test_accessor_id');
expect(transferObject.groupId).toBe('test_group_id');
expect(transferObject.panelId).toBeNull();
fireEvent.dragStart(element, new Event('dragend'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeFalsy();
cut.dispose();
});
test('that the event is cancelled when floating and shiftKey=true', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { location: { type: 'floating' } } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'accessor_id' } as DockviewComponent,
group
);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
test('that the event is never cancelled when the group is not floating', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { location: { type: 'grid' } } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'accessor_id' } as DockviewComponent,
group
);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
});

View File

@ -1,174 +0,0 @@
import { fireEvent } from '@testing-library/dom';
import { ContentContainer } from '../../../../dockview/components/panel/content';
import {
GroupPanelPartInitParameters,
IContentRenderer,
} from '../../../../dockview/types';
import { CompositeDisposable } from '../../../../lifecycle';
import { PanelUpdateEvent } from '../../../../panel/types';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer';
class TestContentRenderer
extends CompositeDisposable
implements IContentRenderer
{
readonly element: HTMLElement;
constructor(public id: string) {
super();
this.element = document.createElement('div');
this.element.id = id;
}
init(parameters: GroupPanelPartInitParameters): void {
//
}
layout(width: number, height: number): void {
//
}
update(event: PanelUpdateEvent): void {
//
}
toJSON(): object {
return {};
}
focus(): void {
//
}
}
describe('contentContainer', () => {
beforeEach(() => {
jest.useFakeTimers();
});
test('basic focus test', () => {
let focus = 0;
let blur = 0;
const disposable = new CompositeDisposable();
const overlayRenderContainer = new OverlayRenderContainer(
document.createElement('div'),
fromPartial<DockviewComponent>({})
);
const cut = new ContentContainer(
fromPartial<DockviewComponent>({
renderer: 'onlyWhenVisible',
overlayRenderContainer,
}),
fromPartial<DockviewGroupPanelModel>({
renderContainer: overlayRenderContainer,
})
);
disposable.addDisposables(
cut.onDidFocus(() => {
focus++;
}),
cut.onDidBlur(() => {
blur++;
})
);
const contentRenderer = new TestContentRenderer('id-1');
const panel = fromPartial<IDockviewPanel>({
view: {
content: contentRenderer,
},
api: { renderer: 'onlyWhenVisible' },
});
cut.openPanel(panel as IDockviewPanel);
expect(focus).toBe(0);
expect(blur).toBe(0);
// container has focus within
fireEvent.focus(contentRenderer.element);
expect(focus).toBe(1);
expect(blur).toBe(0);
// container looses focus
fireEvent.blur(contentRenderer.element);
jest.runAllTimers();
expect(focus).toBe(1);
expect(blur).toBe(1);
const contentRenderer2 = new TestContentRenderer('id-2');
const panel2 = {
view: {
content: contentRenderer2,
} as Partial<IDockviewPanelModel>,
api: { renderer: 'onlyWhenVisible' },
} as Partial<IDockviewPanel>;
cut.openPanel(panel2 as IDockviewPanel);
// expect(focus).toBe(2);
// expect(blur).toBe(1);
// new panel recieves focus
fireEvent.focus(contentRenderer2.element);
expect(focus).toBe(2);
expect(blur).toBe(1);
// new panel looses focus
fireEvent.blur(contentRenderer2.element);
jest.runAllTimers();
expect(focus).toBe(2);
expect(blur).toBe(2);
disposable.dispose();
});
test("that panels renderered as 'onlyWhenVisible' are removed when closed", () => {
const overlayRenderContainer = fromPartial<OverlayRenderContainer>({
detatch: jest.fn(),
});
const cut = new ContentContainer(
fromPartial<DockviewComponent>({
overlayRenderContainer,
}),
fromPartial<DockviewGroupPanelModel>({
renderContainer: overlayRenderContainer,
})
);
const panel1 = fromPartial<IDockviewPanel>({
api: {
renderer: 'onlyWhenVisible',
},
view: { content: new TestContentRenderer('panel_1') },
});
const panel2 = fromPartial<IDockviewPanel>({
api: {
renderer: 'onlyWhenVisible',
},
view: { content: new TestContentRenderer('panel_2') },
});
cut.openPanel(panel1);
expect(panel1.view.content.element.parentElement).toBe(cut.element);
expect(cut.element.childNodes.length).toBe(1);
cut.openPanel(panel2);
expect(panel1.view.content.element.parentElement).toBeNull();
expect(panel2.view.content.element.parentElement).toBe(cut.element);
expect(cut.element.childNodes.length).toBe(1);
});
});

View File

@ -1,310 +0,0 @@
import { fireEvent } from '@testing-library/dom';
import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../dnd/dataTransfer';
import { DockviewComponent } from '../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab';
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('tab', () => {
test('that empty tab has inactive-tab class', () => {
const accessorMock = jest.fn();
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
new accessorMock(),
new groupMock()
);
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
});
test('that active tab has active-tab class', () => {
const accessorMock = jest.fn();
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
new accessorMock(),
new groupMock()
);
cut.setActive(true);
expect(cut.element.className).toBe('dv-tab dv-active-tab');
cut.setActive(false);
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
});
test('that an external event does not render a drop target and calls through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
groupPanel
);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that if you drag over yourself a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that if you drag over another tab a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
PanelTransfer.prototype
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that dropping on a tab with the same id but from a different component should not render a drop over and call through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'anothercomponentid',
'anothergroupid',
'panel1'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that dropping on a tab from a different component should not render a drop over and call through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'anothercomponentid',
'anothergroupid',
'panel2'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
});

View File

@ -1,63 +0,0 @@
import { DockviewApi } from '../../../../api/component.api';
import { DockviewPanelApi, TitleEvent } from '../../../../api/dockviewPanelApi';
import { DefaultTab } from '../../../../dockview/components/tab/defaultTab';
import { fromPartial } from '@total-typescript/shoehorn';
import { Emitter } from '../../../../events';
import { fireEvent } from '@testing-library/dom';
describe('defaultTab', () => {
test('that title updates', () => {
const cut = new DefaultTab();
let el = cut.element.querySelector('.dv-default-tab-content');
expect(el).toBeTruthy();
expect(el!.textContent).toBe('');
const onDidTitleChange = new Emitter<TitleEvent>();
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: onDidTitleChange.event,
});
const containerApi = fromPartial<DockviewApi>({});
cut.init({
api,
containerApi,
params: {},
title: 'title_abc',
});
el = cut.element.querySelector('.dv-default-tab-content');
expect(el).toBeTruthy();
expect(el!.textContent).toBe('title_abc');
onDidTitleChange.fire({ title: 'title_def' });
expect(el!.textContent).toBe('title_def');
});
test('that click closes tab', () => {
const cut = new DefaultTab();
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: jest.fn(),
close: jest.fn(),
});
const containerApi = fromPartial<DockviewApi>({});
cut.init({
api,
containerApi,
params: {},
title: 'title_abc',
});
let el = cut.element.querySelector('.dv-default-tab-action');
fireEvent.pointerDown(el!);
expect(api.close).toHaveBeenCalledTimes(0);
fireEvent.click(el!);
expect(api.close).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,66 +0,0 @@
import { Tabs } from '../../../../dockview/components/titlebar/tabs';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
describe('tabs', () => {
describe('disableCustomScrollbars', () => {
test('enabled by default', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('enabled when disabled flag is false', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'custom',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('disabled when disabled flag is true', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'native',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(0);
});
});
});

View File

@ -1,867 +0,0 @@
import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../../dnd/dataTransfer';
import { TabsContainer } from '../../../../dockview/components/titlebar/tabsContainer';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { fireEvent } from '@testing-library/dom';
import { TestPanel } from '../../dockviewGroupPanelModel.spec';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewPanelApi } from '../../../../api/dockviewPanelApi';
describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
model: groupView,
};
});
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that a drag over event from another tab should render a drop target', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const dropTargetContainer = document.createElement('div');
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'testcomponentid',
'anothergroupid',
'anotherpanelid'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
});
test('that dropping over the empty space should render a drop target', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that dropping the first tab should render a drop target', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that dropping a tab from another component should not render a drop target', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'anothercomponentid',
'anothergroupid',
'panel1'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('left actions', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
// add left action
const left = document.createElement('div');
left.className = 'test-left-actions-element';
cut.setLeftActionsElement(left);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-left-actions-element'
);
expect(query[0].children.length).toBe(1);
// add left action
const left2 = document.createElement('div');
left2.className = 'test-left-actions-element-2';
cut.setLeftActionsElement(left2);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-left-actions-element-2'
);
expect(query[0].children.length).toBe(1);
// remove left action
cut.setLeftActionsElement(undefined);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
});
test('right actions', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
// add right action
const right = document.createElement('div');
right.className = 'test-right-actions-element';
cut.setRightActionsElement(right);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-right-actions-element'
);
expect(query[0].children.length).toBe(1);
// add right action
const right2 = document.createElement('div');
right2.className = 'test-right-actions-element-2';
cut.setRightActionsElement(right2);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-right-actions-element-2'
);
expect(query[0].children.length).toBe(1);
// remove right action
cut.setRightActionsElement(undefined);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
});
test('that a tab will become floating when clicked if not floating and shift is selected', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'grid' } } as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
expect(accessor.addFloatingGroup).toHaveBeenCalledWith(groupPanel, {
x: 100,
y: 60,
inDragMode: true,
});
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
});
test('that a tab that is already floating cannot be floated again', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'floating' } } as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(0);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
});
test('that selecting a tab with shift down will move that tab into a new floating group', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'floating' } } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const createPanel = (id: string) =>
fromPartial<IDockviewPanel>({
id,
view: {
tab: {
element: document.createElement('div'),
},
content: {
element: document.createElement('div'),
},
},
});
const panel = createPanel('test_id');
cut.openPanel(panel);
const el = cut.element.querySelector('.dv-tab')!;
expect(el).toBeTruthy();
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(el, event);
// a floating group with a single tab shouldn't be eligible
expect(preventDefaultSpy).toHaveBeenCalledTimes(0);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
const panel2 = createPanel('test_id_2');
cut.openPanel(panel2);
fireEvent(el, event);
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
});
test('pre header actions', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'grid' } } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.dv-pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setPrefixActionsElement(actions);
result = cut.element.querySelector('.dv-pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setPrefixActionsElement(updatedActions);
result = cut.element.querySelector('.dv-pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setPrefixActionsElement(undefined);
result = cut.element.querySelector('.dv-pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('left header actions', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'grid' } } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.dv-left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setLeftActionsElement(actions);
result = cut.element.querySelector('.dv-left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setLeftActionsElement(updatedActions);
result = cut.element.querySelector('.dv-left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setLeftActionsElement(undefined);
result = cut.element.querySelector('.dv-left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('right header actions', () => {
const accessor = fromPartial<DockviewComponent>({
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'grid' } } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setRightActionsElement(actions);
result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setRightActionsElement(updatedActions);
result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setRightActionsElement(undefined);
result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('class dv-single-tab is present when only one tab exists`', () => {
const cut = new TabsContainer(
fromPartial<DockviewComponent>({
options: {},
onDidOptionsChange: jest.fn(),
}),
fromPartial<DockviewGroupPanel>({})
);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
const panel1 = new TestPanel(
'panel_1',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
const panel2 = new TestPanel(
'panel_2',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
cut.closePanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
cut.closePanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
});
});

View File

@ -1,20 +0,0 @@
import { VoidContainer } from '../../../../dockview/components/titlebar/voidContainer';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { fireEvent } from '@testing-library/dom';
describe('voidContainer', () => {
test('that `pointerDown` triggers activation', () => {
const accessor = fromPartial<DockviewComponent>({
doSetGroupActive: jest.fn(),
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(accessor.doSetGroupActive).not.toHaveBeenCalled();
fireEvent.pointerDown(cut.element);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group);
});
});

View File

@ -1,190 +0,0 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
import { GroupOptions } from '../../dockview/dockviewGroupPanelModel';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewPanelModelMock } from '../__mocks__/mockDockviewPanelModel';
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { ContentContainer } from '../../dockview/components/panel/content';
describe('dockviewGroupPanel', () => {
test('default minimum/maximium width/height', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
expect(cut.minimumWidth).toBe(100);
expect(cut.minimumHeight).toBe(100);
expect(cut.maximumHeight).toBe(Number.MAX_SAFE_INTEGER);
expect(cut.maximumWidth).toBe(Number.MAX_SAFE_INTEGER);
});
test('that onDidActivePanelChange is configured at inline', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
api: {},
renderer: 'always',
overlayRenderContainer: {
attach: jest.fn(),
detatch: jest.fn(),
},
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
let counter = 0;
cut.api.onDidActivePanelChange((event) => {
counter++;
});
cut.model.openPanel(
fromPartial<IDockviewPanel>({
updateParentGroup: jest.fn(),
view: {
tab: { element: document.createElement('div') },
content: new ContentContainer(accessor, cut.model),
},
api: {
renderer: 'onlyWhenVisible',
onDidTitleChange: jest.fn(),
onDidParametersChange: jest.fn(),
},
layout: jest.fn(),
runEvents: jest.fn(),
})
);
expect(counter).toBe(1);
});
test('group constraints', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
doSetGroupActive: jest.fn(),
overlayRenderContainer: fromPartial<OverlayRenderContainer>({
attach: jest.fn(),
detatch: jest.fn(),
}),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
cut.api.setConstraints({
minimumHeight: 10,
maximumHeight: 100,
minimumWidth: 20,
maximumWidth: 200,
});
// initial constraints
expect(cut.minimumWidth).toBe(20);
expect(cut.minimumHeight).toBe(10);
expect(cut.maximumHeight).toBe(100);
expect(cut.maximumWidth).toBe(200);
const panelModel = new DockviewPanelModelMock(
'content_component',
fromPartial<IContentRenderer>({
element: document.createElement('div'),
}),
'tab_component',
fromPartial<ITabRenderer>({
element: document.createElement('div'),
})
);
const panel = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
minimumWidth: 21,
minimumHeight: 11,
maximumHeight: 101,
maximumWidth: 201,
}
);
cut.model.openPanel(panel);
// active panel constraints
expect(cut.minimumWidth).toBe(21);
expect(cut.minimumHeight).toBe(11);
expect(cut.maximumHeight).toBe(101);
expect(cut.maximumWidth).toBe(201);
const panel2 = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
minimumWidth: 22,
minimumHeight: 12,
maximumHeight: 102,
maximumWidth: 202,
}
);
cut.model.openPanel(panel2);
// active panel constraints
expect(cut.minimumWidth).toBe(22);
expect(cut.minimumHeight).toBe(12);
expect(cut.maximumHeight).toBe(102);
expect(cut.maximumWidth).toBe(202);
const panel3 = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
}
);
cut.model.openPanel(panel3);
// active panel without specified constraints so falls back to group constraints
expect(cut.minimumWidth).toBe(20);
expect(cut.minimumHeight).toBe(10);
expect(cut.maximumHeight).toBe(100);
expect(cut.maximumWidth).toBe(200);
});
});

View File

@ -1,287 +0,0 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewApi } from '../../api/component.api';
import { DockviewPanel } from '../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('dockviewPanel', () => {
test('update title', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
let latestTitle: string | undefined = undefined;
const disposable = cut.api.onDidTitleChange((event) => {
latestTitle = event.title;
});
expect(cut.title).toBeUndefined();
cut.init({ title: 'new title', params: {} });
expect(latestTitle).toBe('new title');
expect(cut.title).toBe('new title');
cut.setTitle('another title');
expect(latestTitle).toBe('another title');
expect(cut.title).toBe('another title');
disposable.dispose();
});
test('that .setTitle updates the title', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
cut.init({ title: 'myTitle', params: {} });
expect(cut.title).toBe('myTitle');
cut.setTitle('newTitle');
expect(cut.title).toBe('newTitle');
cut.api.setTitle('new title 2');
expect(cut.title).toBe('new title 2');
});
test('dispose cleanup', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest
.fn()
.mockReturnValue({ dispose: jest.fn() }),
onDidLocationChange: jest
.fn()
.mockReturnValue({ dispose: jest.fn() }),
onDidActiveChange: jest
.fn()
.mockReturnValue({ dispose: jest.fn() }),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
cut.init({ params: {}, title: 'title' });
cut.dispose();
expect(model.dispose).toHaveBeenCalled();
});
test('get params', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
expect(cut.params).toEqual(undefined);
cut.update({ params: { variableA: 'A', variableB: 'B' } });
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
});
test('setSize propagates to underlying group', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
setSize: jest.fn(),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
cut.api.setSize({ height: 123, width: 456 });
expect(group.api.setSize).toHaveBeenCalledWith({
height: 123,
width: 456,
});
expect(group.api.setSize).toHaveBeenCalledTimes(1);
});
test('updateParameter', () => {
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
cut.init({ params: { a: '1', b: '2' }, title: 'A title' });
expect(cut.params).toEqual({ a: '1', b: '2' });
// update 'a' and add 'c'
cut.update({ params: { a: '-1', c: '3' } });
expect(cut.params).toEqual({ a: '-1', b: '2', c: '3' });
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3' },
});
cut.update({ params: { d: '4', e: '5', f: '6' } });
expect(cut.params).toEqual({
a: '-1',
b: '2',
c: '3',
d: '4',
e: '5',
f: '6',
});
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '4', e: '5', f: '6' },
});
cut.update({
params: {
d: '',
e: null,
f: undefined,
g: '',
h: null,
i: undefined,
},
});
expect(cut.params).toEqual({
a: '-1',
b: '2',
c: '3',
d: '',
e: null,
g: '',
h: null,
});
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '', e: null, g: '', h: null },
});
});
});

View File

@ -1,200 +0,0 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
import { DefaultTab } from '../../dockview/components/tab/defaultTab';
import { fromPartial } from '@total-typescript/shoehorn';
describe('dockviewGroupPanel', () => {
let contentMock: jest.Mock<IContentRenderer>;
let tabMock: jest.Mock<ITabRenderer>;
let accessorMock: DockviewComponent;
beforeEach(() => {
contentMock = jest.fn<IContentRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
dispose: jest.fn(),
update: jest.fn(),
};
return partial as IContentRenderer;
});
tabMock = jest.fn<ITabRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
dispose: jest.fn(),
update: jest.fn(),
};
return partial as IContentRenderer;
});
accessorMock = fromPartial<DockviewComponent>({
options: {
createComponent(options) {
switch (options.name) {
case 'contentComponent':
return new contentMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return new tabMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
},
});
});
test('that dispose is called on content and tab renderers when present', () => {
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent',
'tabComponent'
);
cut.dispose();
expect(cut.content.dispose).toHaveBeenCalled();
expect(cut.tab.dispose).toHaveBeenCalled();
});
test('that update is called on content and tab renderers when present', () => {
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent',
'tabComponent'
);
cut.update({
params: {},
});
expect(cut.content.update).toHaveBeenCalled();
expect(cut.tab.update).toHaveBeenCalled();
});
test('that the default tab is created', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
createComponent(options) {
switch (options.name) {
case 'contentComponent':
return new contentMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
});
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent',
'tabComponent'
);
expect(cut.tab).toEqual(tabMock);
});
test('that the provided default tab is chosen when no implementation is provided', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
defaultTabComponent: 'tabComponent',
createComponent(options) {
switch (options.name) {
case 'contentComponent':
return new contentMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
});
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent'
);
expect(cut.tab).toEqual(tabMock);
});
test('that is library default tab instance is created when no alternative exists', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
createComponent(options) {
switch (options.name) {
case 'contentComponent':
return new contentMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
},
});
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent'
);
expect(cut.tab instanceof DefaultTab).toBeTruthy();
});
test('that the default content is created', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
createComponent(options) {
switch (options.name) {
case 'contentComponent':
return contentMock;
default:
throw new Error(`unsupported`);
}
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
});
const cut = new DockviewPanelModel(
accessorMock,
'id',
'contentComponent'
);
expect(cut.content).toEqual(contentMock);
});
});

View File

@ -1,83 +0,0 @@
import {
disableIframePointEvents,
isInDocument,
quasiDefaultPrevented,
quasiPreventDefault,
} from '../dom';
describe('dom', () => {
test('quasiPreventDefault', () => {
const event = new Event('myevent');
expect((event as any)['dv-quasiPreventDefault']).toBeUndefined();
quasiPreventDefault(event);
expect((event as any)['dv-quasiPreventDefault']).toBe(true);
});
test('quasiDefaultPrevented', () => {
const event = new Event('myevent');
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = false;
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = true;
expect(quasiDefaultPrevented(event)).toBeTruthy();
});
test('isInDocument: DOM element', () => {
const el = document.createElement('div');
expect(isInDocument(el)).toBeFalsy();
document.body.appendChild(el);
expect(isInDocument(el)).toBeTruthy();
});
test('isInDocument: Shadow DOM element', () => {
const el = document.createElement('div');
document.body.appendChild(el);
const shadow = el.attachShadow({ mode: 'open' });
const el2 = document.createElement('div');
expect(isInDocument(el2)).toBeFalsy();
shadow.appendChild(el2);
expect(isInDocument(el2)).toBeTruthy();
});
test('disableIframePointEvents', () => {
const el1 = document.createElement('iframe');
const el2 = document.createElement('iframe');
const el3 = document.createElement('webview');
const el4 = document.createElement('webview');
document.body.appendChild(el1);
document.body.appendChild(el2);
document.body.appendChild(el3);
document.body.appendChild(el4);
el1.style.pointerEvents = 'inherit';
el3.style.pointerEvents = 'inherit';
expect(el1.style.pointerEvents).toBe('inherit');
expect(el2.style.pointerEvents).toBe('');
expect(el3.style.pointerEvents).toBe('inherit');
expect(el4.style.pointerEvents).toBe('');
const f = disableIframePointEvents();
expect(el1.style.pointerEvents).toBe('none');
expect(el2.style.pointerEvents).toBe('none');
expect(el3.style.pointerEvents).toBe('none');
expect(el4.style.pointerEvents).toBe('none');
f.release();
expect(el1.style.pointerEvents).toBe('inherit');
expect(el2.style.pointerEvents).toBe('');
expect(el3.style.pointerEvents).toBe('inherit');
expect(el4.style.pointerEvents).toBe('');
});
});

View File

@ -1,278 +0,0 @@
import {
AsapEvent,
Emitter,
Event,
addDisposableListener,
} from '../events';
describe('events', () => {
describe('emitter', () => {
it('debug mode is off', () => {
expect(Emitter.ENABLE_TRACKING).toBeFalsy();
});
it('should emit values', () => {
const emitter = new Emitter<number>();
let value: number | undefined = undefined;
emitter.fire(-1);
expect(value).toBeUndefined();
const stream = emitter.event((x) => {
value = x;
});
emitter.fire(0);
expect(value).toBe(0);
emitter.fire(1);
expect(value).toBe(1);
stream.dispose();
});
it('should stop emitting after dispose', () => {
const emitter = new Emitter<number>();
let value: number | undefined = undefined;
const stream = emitter.event((x) => {
value = x;
});
emitter.fire(0);
expect(value).toBe(0);
stream.dispose();
value = undefined;
emitter.fire(1);
expect(value).toBeUndefined();
});
it('should stop emitting after dispose', () => {
const emitter = new Emitter<number>();
let value: number | undefined = undefined;
const stream = emitter.event((x) => {
value = x;
});
emitter.fire(0);
expect(value).toBe(0);
stream.dispose();
value = undefined;
emitter.fire(1);
expect(value).toBeUndefined();
});
it('should relay last value in replay mode', () => {
const emitter = new Emitter<number>({ replay: true });
let value: number | undefined = undefined;
emitter.fire(1);
const stream = emitter.event((x) => {
value = x;
});
expect(value).toBe(1);
stream.dispose();
});
});
describe('asapEvent', () => {
test('that asapEvents fire once per event-loop-cycle', () => {
jest.useFakeTimers();
const event = new AsapEvent();
let preFireCount = 0;
let postFireCount = 0;
event.onEvent(() => {
preFireCount++;
});
for (let i = 0; i < 100; i++) {
event.fire();
}
/**
* check that subscribing after the events have fired but before the event-loop cycle completes
* results in no event fires.
*/
event.onEvent((e) => {
postFireCount++;
});
expect(preFireCount).toBe(0);
expect(postFireCount).toBe(0);
jest.runAllTimers();
expect(preFireCount).toBe(1);
expect(postFireCount).toBe(0);
});
});
it('should emit a value when any event fires', () => {
const emitter1 = new Emitter<number>();
const emitter2 = new Emitter<number>();
const emitter3 = new Emitter<number>();
let value: number | undefined = 0;
const stream = Event.any(
emitter1.event,
emitter2.event,
emitter3.event
)((x) => {
value = x;
});
emitter2.fire(2);
expect(value).toBe(2);
emitter1.fire(1);
expect(value).toBe(1);
emitter3.fire(3);
expect(value).toBe(3);
});
it('addDisposableListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
true
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
true
);
});
it('addDisposableListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
undefined
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
undefined
);
});
it('addDisposableListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
true
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
true
);
});
it('addDisposableListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
undefined
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
undefined
);
});
});

View File

@ -1,206 +0,0 @@
import { Emitter } from '../../events';
import {
BaseGrid,
IGridPanelView,
BaseGridOptions,
} from '../../gridview/baseComponentGridview';
import { IViewSize } from '../../gridview/gridview';
import { CompositeDisposable } from '../../lifecycle';
import {
PanelInitParameters,
PanelUpdateEvent,
Parameters,
} from '../../panel/types';
import { LayoutPriority, Orientation } from '../../splitview/splitview';
class TestPanel implements IGridPanelView {
_onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event;
isVisible: boolean = true;
isActive: boolean = true;
params: Parameters = {};
constructor(
public readonly id: string,
public readonly element: HTMLElement,
public readonly minimumWidth: number,
public readonly maximumWidth: number,
public readonly minimumHeight: number,
public readonly maximumHeight: number,
public priority: LayoutPriority,
public snap: boolean
) {}
init(params: PanelInitParameters): void {
//
}
setActive(isActive: boolean): void {
//
}
toJSON(): object {
return {};
}
layout(width: number, height: number): void {
//
}
update(event: PanelUpdateEvent<Parameters>): void {
//
}
focus(): void {
//
}
fromJSON(json: object): void {
//
}
dispose(): void {
//
}
}
class ClassUnderTest extends BaseGrid<TestPanel> {
readonly gridview = this.gridview;
constructor(parentElement: HTMLElement, options: BaseGridOptions) {
super(parentElement, options);
}
doRemoveGroup(
group: TestPanel,
options?: { skipActive?: boolean; skipDispose?: boolean }
): TestPanel {
return super.doRemoveGroup(group, options);
}
doAddGroup(group: TestPanel, location?: number[], size?: number): void {
this._groups.set(group.id, {
value: group,
disposable: {
dispose: () => {
//
},
},
});
super.doAddGroup(group, location, size);
}
public fromJSON(data: any): void {
//
}
public toJSON(): object {
return {};
}
public clear(): void {
//
}
}
describe('baseComponentGridview', () => {
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const cut = new ClassUnderTest(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
cut.dispose();
expect(container.parentElement).toBe(root);
});
test('that .layout(...) force flag works', () => {
const cut = new ClassUnderTest(document.createElement('div'), {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
const spy = jest.spyOn(cut.gridview, 'layout');
cut.layout(100, 100);
expect(spy).toHaveBeenCalledTimes(1);
cut.layout(100, 100, false);
expect(spy).toHaveBeenCalledTimes(1);
cut.layout(100, 100, true);
expect(spy).toHaveBeenCalledTimes(2);
cut.layout(150, 150, false);
expect(spy).toHaveBeenCalledTimes(3);
cut.layout(150, 150, true);
expect(spy).toHaveBeenCalledTimes(4);
});
test('can add group', () => {
const cut = new ClassUnderTest(document.createElement('div'), {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
const events: { type: string; panel: TestPanel | undefined }[] = [];
const disposable = new CompositeDisposable(
cut.onDidAdd((event) => {
events.push({ type: 'add', panel: event });
}),
cut.onDidRemove((event) => {
events.push({ type: 'remove', panel: event });
}),
cut.onDidActiveChange((event) => {
events.push({ type: 'active', panel: event });
})
);
const panel1 = new TestPanel(
'id',
document.createElement('div'),
0,
100,
0,
100,
LayoutPriority.Normal,
false
);
cut.doAddGroup(panel1);
expect(events.length).toBe(1);
expect(events[0]).toEqual({ type: 'add', panel: panel1 });
const panel2 = new TestPanel(
'id',
document.createElement('div'),
0,
100,
0,
100,
LayoutPriority.Normal,
false
);
cut.doAddGroup(panel2);
expect(events.length).toBe(2);
expect(events[1]).toEqual({ type: 'add', panel: panel2 });
cut.doRemoveGroup(panel1);
expect(events.length).toBe(3);
expect(events[2]).toEqual({ type: 'remove', panel: panel1 });
disposable.dispose();
cut.dispose();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('gridviewPanel', () => {
test('get panel', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
} as any;
});
const accessor = new accessorMock();
const cut = new DockviewGroupPanel(accessor, 'id', {});
expect(cut.params).toEqual(undefined);
cut.update({ params: { variableA: 'A', variableB: 'B' } });
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
});
});

View File

@ -1,83 +0,0 @@
import {
CompositeDisposable,
Disposable,
MutableDisposable,
} from '../lifecycle';
describe('lifecycle', () => {
test('mutable disposable', () => {
const mutableDisposable = new MutableDisposable();
let disposed = 0;
const disposable = () => ({
dispose: () => {
disposed++;
},
});
mutableDisposable.value = disposable();
expect(disposed).toBe(0);
mutableDisposable.value = disposable();
expect(disposed).toBe(1);
mutableDisposable.dispose();
expect(disposed).toBe(2);
mutableDisposable.dispose();
});
test('composite disposable', () => {
const d1 = {
dispose: jest.fn(),
};
const d2 = {
dispose: jest.fn(),
};
const d3 = {
dispose: jest.fn(),
};
const d4 = {
dispose: jest.fn(),
};
const cut = new CompositeDisposable(d1, d2);
cut.addDisposables(d3, d4);
cut.dispose();
expect(d1.dispose).toHaveBeenCalledTimes(1);
expect(d2.dispose).toHaveBeenCalledTimes(1);
expect(d3.dispose).toHaveBeenCalledTimes(1);
expect(d4.dispose).toHaveBeenCalledTimes(1);
});
test('that isDisposed=true once CompositeDisposable is disposed', () => {
class Test extends CompositeDisposable {
checkIsDisposed(): boolean {
return this.isDisposed;
}
}
const cut = new Test();
expect(cut.checkIsDisposed()).toBeFalsy();
cut.dispose();
expect(cut.checkIsDisposed()).toBeTruthy();
});
test('Disposable.from(...)', () => {
const func = jest.fn();
const disposable = Disposable.from(func);
expect(func).not.toHaveBeenCalled();
disposable.dispose();
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,21 +0,0 @@
import { clamp, range } from '../math';
describe('math', () => {
describe('clamp', () => {
it('should clamp between a minimum and maximum value', () => {
expect(clamp(45, 40, 50)).toBe(45);
expect(clamp(35, 40, 50)).toBe(40);
expect(clamp(55, 40, 50)).toBe(50);
});
it('if min > max return min', () => {
expect(clamp(55, 50, 40)).toBe(50);
});
});
test('range', () => {
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
expect(range(5, 0)).toEqual([5, 4, 3, 2, 1]);
expect(range(5)).toEqual([0, 1, 2, 3, 4]);
});
});

View File

@ -1,418 +0,0 @@
import { Overlay } from '../../overlay/overlay';
import { mockGetBoundingClientRect } from '../__test_utils__/utils';
describe('overlay', () => {
test('toJSON, top left', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: 10,
top: 20,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
cut.dispose();
});
test('toJSON, bottom right', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
right: 10,
bottom: 20,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
bottom: -20,
right: 0,
width: 40,
height: 50,
});
cut.dispose();
});
test('that out-of-bounds dimensions are fixed, top left', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: -1000,
top: -1000,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
cut.dispose();
});
test('that out-of-bounds dimensions are fixed, bottom right', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
bottom: -1000,
right: -1000,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
bottom: -20,
right: 0,
width: 40,
height: 50,
});
cut.dispose();
});
test('setBounds, top left', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 1000,
width: 1000,
left: 0,
top: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const element: HTMLElement = container.querySelector(
'.dv-resize-container'
)!;
expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
return mockGetBoundingClientRect({
left: 300,
top: 400,
width: 200,
height: 100,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 0,
top: 0,
width: 1000,
height: 1000,
});
}
);
cut.setBounds({ height: 100, width: 200, left: 300, top: 400 });
expect(element.style.height).toBe('100px');
expect(element.style.width).toBe('200px');
expect(element.style.left).toBe('300px');
expect(element.style.top).toBe('400px');
cut.dispose();
});
test('setBounds, bottom right', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 1000,
width: 1000,
right: 0,
bottom: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const element: HTMLElement = container.querySelector(
'.dv-resize-container'
)!;
expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
return mockGetBoundingClientRect({
left: 500,
top: 500,
width: 200,
height: 100,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 0,
top: 0,
width: 1000,
height: 1000,
});
}
);
cut.setBounds({ height: 100, width: 200, right: 300, bottom: 400 });
expect(element.style.height).toBe('100px');
expect(element.style.width).toBe('200px');
expect(element.style.right).toBe('300px');
expect(element.style.bottom).toBe('400px');
cut.dispose();
});
test('that the resize handles are added', () => {
const container = document.createElement('div');
const content = document.createElement('div');
const cut = new Overlay({
height: 500,
width: 500,
left: 100,
top: 200,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
expect(container.querySelector('.dv-resize-handle-top')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottom')
).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-left')).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-right')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topright')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomright')
).toBeTruthy();
cut.dispose();
});
test('aria-level attributes and corresponding z-index', () => {
const container = document.createElement('div');
const content = document.createElement('div');
const createOverlay = () =>
new Overlay({
height: 500,
width: 500,
left: 100,
top: 200,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const overlay1 = createOverlay();
const zIndexValue = (delta: number) =>
`calc(var(--dv-overlay-z-index, 999) + ${delta})`;
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
const overlay2 = createOverlay();
const overlay3 = createOverlay();
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('2');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(4));
overlay2.bringToFront();
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay2.element.getAttribute('aria-level')).toBe('2');
expect(overlay3.element.getAttribute('aria-level')).toBe('1');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(4));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(2));
overlay1.bringToFront();
expect(overlay1.element.getAttribute('aria-level')).toBe('2');
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(4));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
overlay2.dispose();
expect(overlay1.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
overlay1.dispose();
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
});
});

View File

@ -1,265 +0,0 @@
import { Droptarget } from '../../dnd/droptarget';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { Emitter } from '../../events';
import {
IRenderable,
OverlayRenderContainer,
} from '../../overlay/overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn';
import { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('overlayRenderContainer', () => {
let referenceContainer: IRenderable;
let parentContainer: HTMLElement;
beforeEach(() => {
parentContainer = document.createElement('div');
referenceContainer = {
element: document.createElement('div'),
dropTarget: fromPartial<Droptarget>({}),
};
});
test('that attach(...) and detach(...) mutate the DOM as expected', () => {
const cut = new OverlayRenderContainer(
parentContainer,
fromPartial<DockviewComponent>({})
);
const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({
api: {
id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true,
location: { type: 'grid' },
},
view: {
content: {
element: panelContentEl,
},
},
group: {
api: {
location: { type: 'grid' },
},
},
});
cut.attach({ panel, referenceContainer });
expect(panelContentEl.parentElement?.parentElement).toBe(
parentContainer
);
cut.detatch(panel);
expect(panelContentEl.parentElement?.parentElement).toBeUndefined();
});
test('add a view that is not currently in the DOM', async () => {
const cut = new OverlayRenderContainer(
parentContainer,
fromPartial<DockviewComponent>({})
);
const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({
api: {
id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true,
location: { type: 'grid' },
},
view: {
content: {
element: panelContentEl,
},
},
group: {
api: {
location: { type: 'grid' },
},
},
});
(parentContainer as jest.Mocked<HTMLDivElement>).getBoundingClientRect =
jest
.fn<DOMRect, []>()
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 100,
top: 200,
width: 1000,
height: 500,
})
)
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 101,
top: 201,
width: 1000,
height: 500,
})
)
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 100,
top: 200,
width: 1000,
height: 500,
})
);
(
referenceContainer.element as jest.Mocked<HTMLDivElement>
).getBoundingClientRect = jest
.fn<DOMRect, []>()
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 150,
top: 300,
width: 100,
height: 200,
})
)
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 150,
top: 300,
width: 101,
height: 201,
})
)
.mockReturnValueOnce(
fromPartial<DOMRect>({
left: 150,
top: 300,
width: 100,
height: 200,
})
);
const container = cut.attach({ panel, referenceContainer });
await exhaustMicrotaskQueue();
expect(panelContentEl.parentElement).toBe(container);
expect(container.parentElement).toBe(parentContainer);
expect(container.style.display).toBe('');
expect(container.style.left).toBe('50px');
expect(container.style.top).toBe('100px');
expect(container.style.width).toBe('100px');
expect(container.style.height).toBe('200px');
expect(
referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(1);
onDidDimensionsChange.fire({});
expect(container.style.display).toBe('');
expect(container.style.left).toBe('49px');
expect(container.style.top).toBe('99px');
expect(container.style.width).toBe('101px');
expect(container.style.height).toBe('201px');
expect(
referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(2);
(panel as Writable<IDockviewPanel>).api.isVisible = false;
onDidVisibilityChange.fire({});
expect(container.style.display).toBe('none');
expect(
referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(2);
(panel as Writable<IDockviewPanel>).api.isVisible = true;
onDidVisibilityChange.fire({});
expect(container.style.display).toBe('');
expect(container.style.left).toBe('50px');
expect(container.style.top).toBe('100px');
expect(container.style.width).toBe('100px');
expect(container.style.height).toBe('200px');
expect(
referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(3);
});
test('related z-index from `aria-level` set on floating panels', async () => {
const group = fromPartial<DockviewGroupPanel>({});
const element = document.createElement('div');
element.setAttribute('aria-level', '2');
const spy = jest.spyOn(element, 'getAttribute');
const accessor = fromPartial<DockviewComponent>({
floatingGroups: [
{
group,
overlay: {
element,
},
},
],
});
const cut = new OverlayRenderContainer(parentContainer, accessor);
const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({
api: {
id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true,
group,
location: { type: 'floating' },
},
view: {
content: {
element: panelContentEl,
},
},
group: {
api: {
location: { type: 'floating' },
},
},
});
cut.attach({ panel, referenceContainer });
await exhaustMicrotaskQueue();
expect(spy).toHaveBeenCalledWith('aria-level');
expect(panelContentEl.parentElement!.style.zIndex).toBe(
'calc(var(--dv-overlay-z-index, 999) + 5)'
);
});
});

View File

@ -1,147 +0,0 @@
import { CompositeDisposable } from '../../lifecycle';
import { Paneview } from '../../paneview/paneview';
import { IPanePart, PaneviewPanel } from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel {
protected getBodyComponent(): IPanePart {
return {
element: document.createElement('div'),
update: () => {
//
},
dispose: () => {
//
},
init: () => {
// /
},
};
}
protected getHeaderComponent(): IPanePart {
return {
element: document.createElement('div'),
update: () => {
//
},
dispose: () => {
//
},
init: () => {
// /
},
};
}
}
describe('paneview', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
});
test('onDidAddView and onDidRemoveView events', () => {
const paneview = new Paneview(container, {
orientation: Orientation.HORIZONTAL,
});
const added: PaneviewPanel[] = [];
const removed: PaneviewPanel[] = [];
const disposable = new CompositeDisposable(
paneview.onDidAddView((view) => added.push(view)),
paneview.onDidRemoveView((view) => removed.push(view))
);
const view1 = new TestPanel({
id: 'id',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
const view2 = new TestPanel({
id: 'id2',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
expect(added.length).toBe(0);
expect(removed.length).toBe(0);
paneview.addPane(view1);
expect(added.length).toBe(1);
expect(removed.length).toBe(0);
expect(added[0]).toBe(view1);
paneview.addPane(view2);
expect(added.length).toBe(2);
expect(removed.length).toBe(0);
expect(added[1]).toBe(view2);
paneview.removePane(0);
expect(added.length).toBe(2);
expect(removed.length).toBe(1);
expect(removed[0]).toBe(view1);
paneview.removePane(0);
expect(added.length).toBe(2);
expect(removed.length).toBe(2);
expect(removed[1]).toBe(view2);
disposable.dispose();
});
test('dispose of paneview', () => {
expect(container.childNodes.length).toBe(0);
const paneview = new Paneview(container, {
orientation: Orientation.HORIZONTAL,
});
const view1 = new TestPanel({
id: 'id',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
const view2 = new TestPanel({
id: 'id2',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
paneview.addPane(view1);
paneview.addPane(view2);
expect(container.childNodes.length).toBeGreaterThan(0);
paneview.dispose();
expect(container.childNodes.length).toBe(0);
});
});

View File

@ -1,619 +0,0 @@
import { PanelDimensionChangeEvent } from '../../api/panelApi';
import { CompositeDisposable } from '../../lifecycle';
import { PanelUpdateEvent } from '../../panel/types';
import { PaneviewComponent } from '../../paneview/paneviewComponent';
import {
PaneviewPanel,
IPanePart,
PanePanelComponentInitParameter,
} from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel {
constructor(id: string, component: string) {
super({
id,
component,
headerComponent: 'header',
orientation: Orientation.VERTICAL,
isExpanded: false,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
}
getHeaderComponent() {
return new (class Header implements IPanePart {
private _element: HTMLElement = document.createElement('div');
get element() {
return this._element;
}
init(params: PanePanelComponentInitParameter) {
//
}
update(params: PanelUpdateEvent) {
//
}
dispose() {
//
}
})();
}
getBodyComponent() {
return new (class Header implements IPanePart {
private _element: HTMLElement = document.createElement('div');
get element() {
return this._element;
}
init(params: PanePanelComponentInitParameter) {
//
}
update(params: PanelUpdateEvent) {
//
}
dispose() {
//
}
})();
}
}
describe('paneviewComponent', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
});
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('vertical panels', () => {
const disposables = new CompositeDisposable();
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(300, 200);
paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel2',
});
const panel1 = paneview.getPanel('panel1') as PaneviewPanel;
const panel2 = paneview.getPanel('panel2') as PaneviewPanel;
let panel1Dimensions: PanelDimensionChangeEvent | undefined = undefined;
disposables.addDisposables(
panel1.api.onDidDimensionsChange((event) => {
panel1Dimensions = event;
})
);
let panel2Dimensions: PanelDimensionChangeEvent | undefined = undefined;
disposables.addDisposables(
panel2.api.onDidDimensionsChange((event) => {
panel2Dimensions = event;
})
);
paneview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 600, height: 22 });
expect(panel2Dimensions).toEqual({ width: 600, height: 22 });
panel1.api.setSize({ size: 300 });
expect(panel1Dimensions).toEqual({ width: 600, height: 22 });
expect(panel2Dimensions).toEqual({ width: 600, height: 22 });
paneview.layout(200, 600);
expect(panel1Dimensions).toEqual({ width: 200, height: 22 });
expect(panel2Dimensions).toEqual({ width: 200, height: 22 });
panel1.api.setExpanded(true);
expect(panel1Dimensions).toEqual({ width: 200, height: 578 });
expect(panel2Dimensions).toEqual({ width: 200, height: 22 });
panel2.api.setExpanded(true);
panel1.api.setSize({ size: 300 });
expect(panel1Dimensions).toEqual({ width: 200, height: 300 });
expect(panel2Dimensions).toEqual({ width: 200, height: 300 });
panel1.api.setSize({ size: 200 });
expect(panel1Dimensions).toEqual({ width: 200, height: 200 });
expect(panel2Dimensions).toEqual({ width: 200, height: 400 });
disposables.dispose();
paneview.dispose();
});
test('serialization', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.fromJSON({
size: 6,
views: [
{
size: 1,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
expanded: false,
},
{
size: 3,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
},
],
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.layout(400, 800);
const panel1 = paneview.getPanel('panel1');
expect(panel1!.api.height).toBe(756);
expect(panel1!.api.width).toBe(400);
expect(panel1!.api.id).toBe('panel1');
// expect(panel1!.api.isActive).toBeTruthy();
// expect(panel1?.api.isFocused).toBeFalsy();
expect(panel1!.api.isVisible).toBeTruthy();
expect(panel1!.api.isExpanded).toBeTruthy();
const panel2 = paneview.getPanel('panel2');
expect(panel2!.api.height).toBe(22);
expect(panel2!.api.width).toBe(400);
expect(panel2!.api.id).toBe('panel2');
// expect(panel2!.api.isActive).toBeTruthy();
// expect(panel2?.api.isFocused).toBeFalsy();
expect(panel2!.api.isVisible).toBeTruthy();
expect(panel2!.api.isExpanded).toBeFalsy();
const panel3 = paneview.getPanel('panel3');
expect(panel3!.api.height).toBe(22);
expect(panel3!.api.width).toBe(400);
expect(panel3!.api.id).toBe('panel3');
// expect(panel3!.api.isActive).toBeTruthy();
// expect(panel3?.api.isFocused).toBeFalsy();
expect(panel3!.api.isVisible).toBeTruthy();
expect(panel3!.api.isExpanded).toBeFalsy();
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
size: 800,
views: [
{
size: 756,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
expanded: true,
headerSize: 22,
},
{
size: 22,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
expanded: false,
headerSize: 22,
},
{
size: 22,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
expanded: false,
headerSize: 22,
},
],
});
});
test('toJSON shouldnt fire any layout events', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel 2',
});
const disposable = paneview.onDidLayoutChange(() => {
fail('onDidLayoutChange shouldnt have been called');
});
const result = paneview.toJSON();
expect(result).toBeTruthy();
disposable.dispose();
});
test('panel is disposed of when component is disposed', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel 2',
});
const panel1 = paneview.getPanel('panel1')!;
const panel2 = paneview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
paneview.dispose();
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('panel is disposed of when removed', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel 2',
});
const panel1 = paneview.getPanel('panel1')!;
const panel2 = paneview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
paneview.removePanel(panel2);
expect(panel1Spy).not.toHaveBeenCalled();
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('panel is disposed of when fromJSON is called', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel 2',
});
const panel1 = paneview.getPanel('panel1')!;
const panel2 = paneview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
paneview.fromJSON({ views: [], size: 0 });
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(400, 600);
paneview.fromJSON({
size: 6,
views: [
{
size: 1,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
minimumSize: 100,
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
expanded: true,
},
{
size: 3,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
expanded: true,
},
],
});
// heights slightly differ because header height isn't accounted for
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
size: 600,
views: [
{
size: 122,
data: {
id: 'panel1',
component: 'default',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
expanded: true,
headerSize: 22,
},
{
size: 456,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
expanded: true,
headerSize: 22,
},
],
});
});
test('that disableAutoResizing is false by default', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(paneview.disableResizing).toBeFalsy();
});
test('that disableAutoResizing can be enabled', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
expect(paneview.disableResizing).toBeTruthy();
});
test('that setVisible toggles visiblity', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
paneview.layout(1000, 1000);
const panel1 = paneview.addPanel({
id: 'panel1',
component: 'default',
title: 'panel1',
});
const panel2 = paneview.addPanel({
id: 'panel2',
component: 'default',
title: 'panel2',
});
expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy();
panel1.api.setVisible(false);
expect(panel1.api.isVisible).toBeFalsy();
expect(panel2.api.isVisible).toBeTruthy();
panel1.api.setVisible(true);
expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy();
});
test('update className', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
className: 'test-a test-b',
});
expect(paneview.element.className).toBe('test-a test-b');
paneview.updateOptions({ className: 'test-b test-c' });
expect(paneview.element.className).toBe('test-b test-c');
});
});

View File

@ -1,901 +0,0 @@
import { Emitter } from '../../events';
import { CompositeDisposable } from '../../lifecycle';
import {
IView,
LayoutPriority,
Orientation,
Sizing,
Splitview,
} from '../../splitview/splitview';
import { fireEvent } from '@testing-library/dom';
class Testview implements IView {
private _element: HTMLElement = document.createElement('div');
private _size = 0;
private _orthogonalSize = 0;
private _priority: LayoutPriority | undefined;
private readonly _onDidChange = new Emitter<{
size?: number;
orthogonalSize?: number;
}>();
readonly onDidChange = this._onDidChange.event;
private readonly _onLayoutCalled = new Emitter<void>();
readonly onLayoutCalled = this._onLayoutCalled.event;
private readonly _onRendered = new Emitter<void>();
readonly onRenderered = this._onRendered.event;
get minimumSize() {
return this._minimumSize;
}
get maximumSize() {
return this._maxiumSize;
}
get element() {
this._onRendered.fire();
return this._element;
}
get size() {
return this._size;
}
get orthogonalSize() {
return this._orthogonalSize;
}
get priority() {
return this._priority;
}
constructor(
private _minimumSize: number,
private _maxiumSize: number,
priority?: LayoutPriority
) {
this._priority = priority;
}
layout(size: number, orthogonalSize: number) {
this._size = size;
this._orthogonalSize = orthogonalSize;
this._onLayoutCalled.fire();
}
fireChangeEvent(value: { size?: number; orthogonalSize?: number }) {
this._onDidChange.fire(value);
}
setVisible(isVisible: boolean) {
//
}
dispose() {
this._onDidChange.dispose();
}
}
describe('splitview', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
jest.clearAllMocks();
});
test('vertical splitview', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
expect(splitview.orientation).toBe(Orientation.HORIZONTAL);
const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-horizontal'
);
expect(viewQuery).toBeTruthy();
splitview.dispose();
});
test('horiziontal splitview', () => {
const splitview = new Splitview(container, {
orientation: Orientation.VERTICAL,
});
expect(splitview.orientation).toBe(Orientation.VERTICAL);
const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-vertical'
);
expect(viewQuery).toBeTruthy();
splitview.dispose();
});
test('has views and sashes', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.addView(new Testview(50, 50));
splitview.addView(new Testview(50, 50));
splitview.addView(new Testview(50, 50));
let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(3);
let sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(2);
splitview.removeView(2);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(2);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(1);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(1);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(0);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(0);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(0);
splitview.dispose();
});
test('visiblity classnames', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
const view1 = new Testview(50, 50);
const view2 = new Testview(50, 50);
splitview.addView(view1);
splitview.addView(view2);
let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
);
expect(viewQuery.length).toBe(2);
splitview.setViewVisible(1, false);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
);
expect(viewQuery.length).toBe(1);
splitview.dispose();
});
test('calls lifecycle methods on view', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
let rendered = false;
let layout = false;
const view = new Testview(50, Number.POSITIVE_INFINITY);
const layoutDisposable = view.onLayoutCalled(() => {
layout = true;
});
const renderDisposable = view.onRenderered(() => {
rendered = true;
});
splitview.addView(view);
splitview.layout(100, 100);
expect(rendered).toBeTruthy();
expect(layout).toBeTruthy();
layoutDisposable.dispose();
renderDisposable.dispose();
splitview.dispose();
});
test('add view at specified index', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view1 = new Testview(50, 200);
const view2 = new Testview(50, 200);
const view3 = new Testview(50, 200);
splitview.addView(view1);
splitview.addView(view2, Sizing.Distribute, 0);
splitview.addView(view3, Sizing.Distribute, 1);
expect(splitview.getViews()).toEqual([view2, view3, view1]);
});
test('streches to viewport', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view = new Testview(50, Number.POSITIVE_INFINITY);
splitview.addView(view);
expect(view.size).toBe(200);
splitview.layout(100, 500);
expect(view.size).toBe(100);
splitview.layout(50, 500);
expect(view.size).toBe(50);
splitview.layout(30, 500);
expect(view.size).toBe(50);
splitview.layout(100, 500);
expect(view.size).toBe(100);
splitview.dispose();
});
test('can resize views 1', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view1 = new Testview(50, 200);
const view2 = new Testview(50, 200);
splitview.addView(view1);
splitview.addView(view2);
expect(view1.size).toBe(100);
expect(view2.size).toBe(100);
view1.fireChangeEvent({ size: 65 });
expect(view1.size).toBe(65);
expect(view2.size).toBe(135);
view2.fireChangeEvent({ size: 75 });
expect(view1.size).toBe(125);
expect(view2.size).toBe(75);
view2.fireChangeEvent({});
expect(view1.size).toBe(125);
expect(view2.size).toBe(75);
splitview.dispose();
});
test('can resize views 2', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view1 = new Testview(50, 200);
const view2 = new Testview(50, 200);
const view3 = new Testview(50, 200);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
splitview.resizeView(1, 100);
expect([view1.size, view2.size, view3.size]).toEqual([50, 100, 50]);
splitview.resizeView(2, 60);
expect([view1.size, view2.size, view3.size]).toEqual([50, 90, 60]);
expect([
splitview.getViewSize(0),
splitview.getViewSize(1),
splitview.getViewSize(2),
splitview.getViewSize(3),
]).toEqual([50, 90, 60, -1]);
splitview.dispose();
});
test('move view', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view1 = new Testview(50, 200);
const view2 = new Testview(50, 200);
const view3 = new Testview(50, 200);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
splitview.moveView(2, 0);
expect(splitview.getViews()).toEqual([view3, view1, view2]);
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
splitview.moveView(0, 2);
expect(splitview.getViews()).toEqual([view1, view2, view3]);
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
splitview.dispose();
});
test('layout called after views added', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
const view1 = new Testview(50, 200);
const view2 = new Testview(50, 200);
const view3 = new Testview(50, 200);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
splitview.layout(200, 500);
expect([view1.size, view2.size, view3.size]).toEqual([67, 67, 66]);
splitview.dispose();
});
test('proportional layout', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
});
splitview.layout(200, 500);
const view1 = new Testview(20, Number.POSITIVE_INFINITY);
const view2 = new Testview(20, Number.POSITIVE_INFINITY);
splitview.addView(view1);
splitview.addView(view2);
expect([view1.size, view2.size]).toEqual([100, 100]);
splitview.layout(100, 500);
expect([view1.size, view2.size]).toEqual([50, 50]);
splitview.dispose();
});
test('disable proportional layout', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(200, 500);
const view1 = new Testview(20, Number.POSITIVE_INFINITY);
const view2 = new Testview(20, Number.POSITIVE_INFINITY);
splitview.addView(view1);
splitview.addView(view2);
expect([view1.size, view2.size]).toEqual([100, 100]);
splitview.layout(100, 500);
expect([view1.size, view2.size]).toEqual([80, 20]);
splitview.dispose();
});
test('high priority', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(300, 500);
const view1 = new Testview(50, Number.POSITIVE_INFINITY);
const view2 = new Testview(
50,
Number.POSITIVE_INFINITY,
LayoutPriority.High
);
const view3 = new Testview(50, Number.POSITIVE_INFINITY);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 100]);
splitview.layout(400, 500);
expect([view1.size, view2.size, view3.size]).toEqual([100, 200, 100]);
splitview.dispose();
});
test('low priority', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(300, 500);
const view1 = new Testview(
50,
Number.POSITIVE_INFINITY,
LayoutPriority.Low
);
const view2 = new Testview(50, Number.POSITIVE_INFINITY);
const view3 = new Testview(50, Number.POSITIVE_INFINITY);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 100]);
splitview.layout(400, 500);
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 200]);
splitview.dispose();
});
test('from descriptor', () => {
const descriptor = {
size: 300,
views: [
{
size: 80,
view: new Testview(0, Number.POSITIVE_INFINITY),
},
{
size: 100,
view: new Testview(0, Number.POSITIVE_INFINITY),
},
{
size: 120,
view: new Testview(0, Number.POSITIVE_INFINITY),
},
],
};
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
descriptor,
});
expect([
descriptor.views[0].size,
descriptor.views[1].size,
descriptor.views[2].size,
]).toEqual([80, 100, 120]);
expect(splitview.size).toBe(300);
});
test('onDidAddView and onDidRemoveView events', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
const added: IView[] = [];
const removed: IView[] = [];
const disposable = new CompositeDisposable(
splitview.onDidAddView((view) => added.push(view)),
splitview.onDidRemoveView((view) => removed.push(view))
);
const view1 = new Testview(0, 100);
const view2 = new Testview(0, 100);
expect(added.length).toBe(0);
expect(removed.length).toBe(0);
splitview.addView(view1);
expect(added.length).toBe(1);
expect(removed.length).toBe(0);
expect(added[0]).toBe(view1);
splitview.addView(view2);
expect(added.length).toBe(2);
expect(removed.length).toBe(0);
expect(added[1]).toBe(view2);
splitview.removeView(0);
expect(added.length).toBe(2);
expect(removed.length).toBe(1);
expect(removed[0]).toBe(view1);
splitview.removeView(0);
expect(added.length).toBe(2);
expect(removed.length).toBe(2);
expect(removed[1]).toBe(view2);
disposable.dispose();
});
test('dispose of splitview', () => {
expect(container.childNodes.length).toBe(0);
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
const view1 = new Testview(0, 100);
const view2 = new Testview(0, 100);
splitview.addView(view1);
splitview.addView(view2);
expect(container.childNodes.length).toBeGreaterThan(0);
let anyEvents = false;
const listener = splitview.onDidRemoveView((e) => {
anyEvents = true; // disposing of the splitview shouldn't fire onDidRemoveView events
});
splitview.dispose();
listener.dispose();
expect(anyEvents).toBeFalsy();
expect(container.childNodes.length).toBe(0);
});
test('dnd: pointer events to move sash', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(400, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(
document,
'removeEventListener'
);
const sashElement = container
.getElementsByClassName('dv-sash')
.item(0) as HTMLElement;
// validate the expected state before drag
expect([view1.size, view2.size]).toEqual([200, 200]);
expect(sashElement).toBeTruthy();
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
// start the drag event
fireEvent(
sashElement,
new MouseEvent('pointerdown', { clientX: 50, clientY: 100 })
);
expect(addEventListenerSpy).toBeCalledTimes(3);
// during a sash drag the views should have pointer-events disabled
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
// expect a delta move of 70 - 50 = 20
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 70, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 75, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent(
document,
new MouseEvent('pointerup', { clientX: 70, clientY: 110 })
);
expect(removeEventListenerSpy).toBeCalledTimes(3);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 100, clientY: 100 })
);
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(3);
expect(removeEventListenerSpy).toBeCalledTimes(3);
});
test('setViewVisible', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(900, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
const view3 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
splitview.setViewVisible(0, false);
expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]);
splitview.setViewVisible(0, true);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
});
test('setViewVisible with one view having high layout priority', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(900, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000, LayoutPriority.High);
const view3 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
splitview.setViewVisible(0, false);
expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]);
splitview.setViewVisible(0, true);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
});
test('set view size', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(900, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
const view3 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
view1.fireChangeEvent({ size: 0 });
expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]);
view1.fireChangeEvent({ size: 300 });
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
});
test('set view size with one view having high layout priority', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(900, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000, LayoutPriority.High);
const view3 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
view1.fireChangeEvent({ size: 0 });
expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]);
view1.fireChangeEvent({ size: 300 });
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
});
test('that margins are applied to view sizing', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
margin: 24,
});
splitview.layout(924, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
const view3 = new Testview(0, 1000);
const view4 = new Testview(0, 1000);
splitview.addView(view1);
expect([view1.size]).toEqual([924]);
splitview.addView(view2);
expect([view1.size, view2.size]).toEqual([450, 450]); // 450 + 24 + 450 = 924
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([292, 292, 292]); // 292 + 24 + 292 + 24 + 292 = 924
splitview.addView(view4);
expect([view1.size, view2.size, view3.size, view4.size]).toEqual([
213, 213, 213, 213,
]); // 213 + 24 + 213 + 24 + 213 + 24 + 213 = 924
let viewQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
height: e.style.height,
width: e.style.width,
}));
let sashQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
}));
// check HTMLElement positions since these are the ones that really matter
expect(viewQuery).toEqual([
{ left: '0px', top: '', width: '213px', height: '' },
// 213 + 24 = 237
{ left: '237px', top: '', width: '213px', height: '' },
// 237 + 213 + 24 = 474
{ left: '474px', top: '', width: '213px', height: '' },
// 474 + 213 + 24 = 474
{ left: '711px', top: '', width: '213px', height: '' },
// 711 + 213 = 924
]);
// 924 / 4 = 231 view size
// 231 - (24*3/4) = 213 margin adjusted view size
// 213 - 4/2 + 24/2 = 223
expect(sashQuery).toEqual([
// 213 - 4/2 + 24/2 = 223
{ left: '223px', top: '0px' },
// 213 + 24 + 213 = 450
// 450 - 4/2 + 24/2 = 460
{ left: '460px', top: '0px' },
// 213 + 24 + 213 + 24 + 213 = 687
// 687 - 4/2 + 24/2 = 697
{ left: '697px', top: '0px' },
]);
splitview.setViewVisible(0, false);
viewQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
height: e.style.height,
width: e.style.width,
}));
sashQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
}));
expect(viewQuery).toEqual([
{ left: '0px', top: '', width: '0px', height: '' },
{ left: '0px', top: '', width: '215px', height: '' },
{ left: '239px', top: '', width: '215px', height: '' },
{ left: '478px', top: '', width: '446px', height: '' },
]);
expect(sashQuery).toEqual([
{ left: '0px', top: '0px' },
{ left: '225px', top: '0px' },
{ left: '464px', top: '0px' },
]);
});
});

View File

@ -1,743 +0,0 @@
import { PanelDimensionChangeEvent } from '../../api/panelApi';
import { Emitter } from '../../events';
import { CompositeDisposable } from '../../lifecycle';
import { Orientation } from '../../splitview/splitview';
import { SplitviewComponent } from '../../splitview/splitviewComponent';
import { SplitviewPanel } from '../../splitview/splitviewPanel';
class TestPanel extends SplitviewPanel {
getComponent() {
return {
update: () => {
//
},
dispose: () => {
//
},
};
}
}
describe('componentSplitview', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
});
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('event leakage', () => {
Emitter.setLeakageMonitorEnabled(true);
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
const panel1 = splitview.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = splitview.addPanel({
id: 'panel2',
component: 'default',
});
splitview.movePanel(0, 1);
splitview.removePanel(panel1);
splitview.dispose();
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
for (const entry of Array.from(
Emitter.MEMORY_LEAK_WATCHER.events
)) {
console.log(entry[1]);
}
throw new Error('not all listeners disposed');
}
Emitter.setLeakageMonitorEnabled(false);
});
test('remove panel', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
const panel3 = splitview.getPanel('panel3')!;
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeFalsy();
expect(panel3.api.isActive).toBeTruthy();
splitview.removePanel(panel3);
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeTruthy();
expect(splitview.length).toBe(2);
splitview.removePanel(panel1);
expect(panel2.api.isActive).toBeTruthy();
expect(splitview.length).toBe(1);
splitview.removePanel(panel2);
expect(splitview.length).toBe(0);
});
test('horizontal dimensions', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
expect(splitview.height).toBe(400);
expect(splitview.width).toBe(600);
});
test('vertical dimensions', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
expect(splitview.height).toBe(400);
expect(splitview.width).toBe(600);
});
test('api resize', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 600);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
const panel3 = splitview.getPanel('panel3')!;
expect(panel1.width).toBe(400);
expect(panel1.height).toBe(200);
expect(panel2.width).toBe(400);
expect(panel2.height).toBe(200);
expect(panel3.width).toBe(400);
expect(panel3.height).toBe(200);
panel1.api.setSize({ size: 100 });
expect(panel1.width).toBe(400);
expect(panel1.height).toBe(100);
expect(panel2.width).toBe(400);
expect(panel2.height).toBe(200);
expect(panel3.width).toBe(400);
expect(panel3.height).toBe(300);
panel2.api.setSize({ size: 100 });
expect(panel1.width).toBe(400);
expect(panel1.height).toBe(100);
expect(panel2.width).toBe(400);
expect(panel2.height).toBe(100);
expect(panel3.width).toBe(400);
expect(panel3.height).toBe(400);
panel3.api.setSize({ size: 100 });
expect(panel1.width).toBe(400);
expect(panel1.height).toBe(100);
expect(panel2.width).toBe(400);
expect(panel2.height).toBe(400);
expect(panel3.width).toBe(400);
expect(panel3.height).toBe(100);
});
test('api', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
const panel1 = splitview.getPanel('panel1');
expect(panel1!.api.height).toBe(400);
expect(panel1!.api.width).toBe(600);
expect(panel1!.api.id).toBe('panel1');
expect(panel1!.api.isActive).toBeTruthy();
// expect(panel1?.api.isFocused).toBeFalsy();
expect(panel1!.api.isVisible).toBeTruthy();
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel2 = splitview.getPanel('panel2');
expect(panel1!.api.isActive).toBeFalsy();
expect(panel2!.api.height).toBe(400);
expect(panel2!.api.width).toBe(300);
expect(panel2!.api.id).toBe('panel2');
expect(panel2!.api.isActive).toBeTruthy();
// expect(panel2!.api.isFocused).toBeFalsy();
expect(panel2!.api.isVisible).toBeTruthy();
panel1?.api.setActive();
expect(panel1!.api.isActive).toBeTruthy();
expect(panel2!.api.isActive).toBeFalsy();
});
test('vertical panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
let panel1Dimensions: PanelDimensionChangeEvent | undefined;
disposables.addDisposables(
panel1.api.onDidDimensionsChange((event) => {
panel1Dimensions = event;
})
);
let panel2Dimensions: PanelDimensionChangeEvent | undefined;
disposables.addDisposables(
panel2.api.onDidDimensionsChange((event) => {
panel2Dimensions = event;
})
);
splitview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 600, height: 200 });
expect(panel2Dimensions).toEqual({ width: 600, height: 200 });
panel1.api.setSize({ size: 300 });
expect(panel1Dimensions).toEqual({ width: 600, height: 300 });
expect(panel2Dimensions).toEqual({ width: 600, height: 100 });
splitview.layout(200, 600);
expect(panel1Dimensions).toEqual({ width: 200, height: 450 });
expect(panel2Dimensions).toEqual({ width: 200, height: 150 });
disposables.dispose();
splitview.dispose();
});
test('horizontal panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
let panel1Dimensions: PanelDimensionChangeEvent | undefined;
disposables.addDisposables(
panel1.api.onDidDimensionsChange((event) => {
panel1Dimensions = event;
})
);
let panel2Dimensions: PanelDimensionChangeEvent | undefined;
disposables.addDisposables(
panel2.api.onDidDimensionsChange((event) => {
panel2Dimensions = event;
})
);
splitview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 300, height: 400 });
expect(panel2Dimensions).toEqual({ width: 300, height: 400 });
panel1.api.setSize({ size: 200 });
expect(panel1Dimensions).toEqual({ width: 200, height: 400 });
expect(panel2Dimensions).toEqual({ width: 400, height: 400 });
splitview.layout(200, 600);
expect(panel1Dimensions).toEqual({ width: 67, height: 600 });
expect(panel2Dimensions).toEqual({ width: 133, height: 600 });
disposables.dispose();
splitview.dispose();
});
test('serialization', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 6);
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'default' } },
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
expect(splitview.length).toBe(3);
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [
{
size: 1,
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
snap: true,
},
{
size: 3,
data: { id: 'panel3', component: 'default' },
snap: false,
},
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
});
test('toJSON shouldnt fire any layout events', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(1000, 1000);
splitview.addPanel({
id: 'panel1',
component: 'default',
});
splitview.addPanel({
id: 'panel2',
component: 'default',
});
const disposable = splitview.onDidLayoutChange(() => {
fail('onDidLayoutChange shouldnt have been called');
});
const result = splitview.toJSON();
expect(result).toBeTruthy();
disposable.dispose();
});
test('panel is disposed of when component is disposed', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(1000, 1000);
splitview.addPanel({
id: 'panel1',
component: 'default',
});
splitview.addPanel({
id: 'panel2',
component: 'default',
});
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
splitview.dispose();
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('panel is disposed of when removed', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(1000, 1000);
splitview.addPanel({
id: 'panel1',
component: 'default',
});
splitview.addPanel({
id: 'panel2',
component: 'default',
});
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
splitview.removePanel(panel2);
expect(panel1Spy).not.toHaveBeenCalled();
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('panel is disposed of when fromJSON is called', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(1000, 1000);
splitview.addPanel({
id: 'panel1',
component: 'default',
});
splitview.addPanel({
id: 'panel2',
component: 'default',
});
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
splitview.fromJSON({
orientation: Orientation.HORIZONTAL,
size: 0,
views: [],
});
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 600);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'default' } },
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [
{
size: 100,
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 200,
data: { id: 'panel2', component: 'default' },
snap: true,
},
{
size: 300,
data: { id: 'panel3', component: 'default' },
snap: false,
},
],
size: 600,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
});
test('that disableAutoResizing is false by default', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(splitview.disableResizing).toBeFalsy();
});
test('that disableAutoResizing can be enabled', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
expect(splitview.disableResizing).toBeTruthy();
});
test('that setVisible toggles visiblity', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(1000, 1000);
const panel1 = splitview.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = splitview.addPanel({
id: 'panel2',
component: 'default',
});
expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy();
panel1.api.setVisible(false);
expect(panel1.api.isVisible).toBeFalsy();
expect(panel2.api.isVisible).toBeTruthy();
panel1.api.setVisible(true);
expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy();
});
test('update className', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
className: 'test-a test-b',
});
expect(splitview.element.className).toBe('test-a test-b');
splitview.updateOptions({ className: 'test-b test-c' });
expect(splitview.element.className).toBe('test-b test-c');
});
});

View File

@ -1,933 +0,0 @@
import {
DockviewMaximizedGroupChanged,
FloatingGroupOptions,
IDockviewComponent,
MovePanelEvent,
PopoutGroupChangePositionEvent,
PopoutGroupChangeSizeEvent,
SerializedDockview,
} from '../dockview/dockviewComponent';
import {
AddGroupOptions,
AddPanelOptions,
DockviewComponentOptions,
DockviewDndOverlayEvent,
MovementOptions,
} from '../dockview/options';
import { Parameters } from '../panel/types';
import { Direction } from '../gridview/baseComponentGridview';
import {
AddComponentOptions,
IGridviewComponent,
SerializedGridviewComponent,
} from '../gridview/gridviewComponent';
import { IGridviewPanel } from '../gridview/gridviewPanel';
import {
AddPaneviewComponentOptions,
SerializedPaneview,
IPaneviewComponent,
} from '../paneview/paneviewComponent';
import { IPaneviewPanel } from '../paneview/paneviewPanel';
import {
AddSplitviewComponentOptions,
ISplitviewComponent,
SerializedSplitview,
} from '../splitview/splitviewComponent';
import { IView, Orientation, Sizing } from '../splitview/splitview';
import { ISplitviewPanel } from '../splitview/splitviewPanel';
import {
DockviewGroupPanel,
IDockviewGroupPanel,
} from '../dockview/dockviewGroupPanel';
import { Event } from '../events';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { PaneviewDidDropEvent } from '../paneview/draggablePaneviewPanel';
import {
GroupDragEvent,
TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types';
import {
DockviewDidDropEvent,
DockviewWillDropEvent,
WillShowOverlayLocationEvent,
} from '../dockview/dockviewGroupPanelModel';
import {
PaneviewComponentOptions,
PaneviewDndOverlayEvent,
} from '../paneview/options';
import { SplitviewComponentOptions } from '../splitview/options';
import { GridviewComponentOptions } from '../gridview/options';
export interface CommonApi<T = any> {
readonly height: number;
readonly width: number;
readonly onDidLayoutChange: Event<void>;
readonly onDidLayoutFromJSON: Event<void>;
focus(): void;
layout(width: number, height: number): void;
fromJSON(data: T): void;
toJSON(): T;
clear(): void;
dispose(): void;
}
export class SplitviewApi implements CommonApi<SerializedSplitview> {
/**
* The minimum size the component can reach where size is measured in the direction of orientation provided.
*/
get minimumSize(): number {
return this.component.minimumSize;
}
/**
* The maximum size the component can reach where size is measured in the direction of orientation provided.
*/
get maximumSize(): number {
return this.component.maximumSize;
}
/**
* Width of the component.
*/
get width(): number {
return this.component.width;
}
/**
* Height of the component.
*/
get height(): number {
return this.component.height;
}
/**
* The current number of panels.
*/
get length(): number {
return this.component.length;
}
/**
* The current orientation of the component.
*/
get orientation(): Orientation {
return this.component.orientation;
}
/**
* The list of current panels.
*/
get panels(): ISplitviewPanel[] {
return this.component.panels;
}
/**
* Invoked after a layout is loaded through the `fromJSON` method.
*/
get onDidLayoutFromJSON(): Event<void> {
return this.component.onDidLayoutFromJSON;
}
/**
* Invoked whenever any aspect of the layout changes.
* If listening to this event it may be worth debouncing ouputs.
*/
get onDidLayoutChange(): Event<void> {
return this.component.onDidLayoutChange;
}
/**
* Invoked when a view is added.
*/
get onDidAddView(): Event<IView> {
return this.component.onDidAddView;
}
/**
* Invoked when a view is removed.
*/
get onDidRemoveView(): Event<IView> {
return this.component.onDidRemoveView;
}
constructor(private readonly component: ISplitviewComponent) {}
/**
* Removes an existing panel and optionally provide a `Sizing` method
* for the subsequent resize.
*/
removePanel(panel: ISplitviewPanel, sizing?: Sizing): void {
this.component.removePanel(panel, sizing);
}
/**
* Focus the component.
*/
focus(): void {
this.component.focus();
}
/**
* Get the reference to a panel given it's `string` id.
*/
getPanel(id: string): ISplitviewPanel | undefined {
return this.component.getPanel(id);
}
/**
* Layout the panel with a width and height.
*/
layout(width: number, height: number): void {
return this.component.layout(width, height);
}
/**
* Add a new panel and return the created instance.
*/
addPanel<T extends object = Parameters>(
options: AddSplitviewComponentOptions<T>
): ISplitviewPanel {
return this.component.addPanel(options);
}
/**
* Move a panel given it's current and desired index.
*/
movePanel(from: number, to: number): void {
this.component.movePanel(from, to);
}
/**
* Deserialize a layout to built a splitivew.
*/
fromJSON(data: SerializedSplitview): void {
this.component.fromJSON(data);
}
/** Serialize a layout */
toJSON(): SerializedSplitview {
return this.component.toJSON();
}
/**
* Remove all panels and clear the component.
*/
clear(): void {
this.component.clear();
}
/**
* Update configuratable options.
*/
updateOptions(options: Partial<SplitviewComponentOptions>): void {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
}
export class PaneviewApi implements CommonApi<SerializedPaneview> {
/**
* The minimum size the component can reach where size is measured in the direction of orientation provided.
*/
get minimumSize(): number {
return this.component.minimumSize;
}
/**
* The maximum size the component can reach where size is measured in the direction of orientation provided.
*/
get maximumSize(): number {
return this.component.maximumSize;
}
/**
* Width of the component.
*/
get width(): number {
return this.component.width;
}
/**
* Height of the component.
*/
get height(): number {
return this.component.height;
}
/**
* All panel objects.
*/
get panels(): IPaneviewPanel[] {
return this.component.panels;
}
/**
* Invoked when any layout change occures, an aggregation of many events.
*/
get onDidLayoutChange(): Event<void> {
return this.component.onDidLayoutChange;
}
/**
* Invoked after a layout is deserialzied using the `fromJSON` method.
*/
get onDidLayoutFromJSON(): Event<void> {
return this.component.onDidLayoutFromJSON;
}
/**
* Invoked when a panel is added. May be called multiple times when moving panels.
*/
get onDidAddView(): Event<IPaneviewPanel> {
return this.component.onDidAddView;
}
/**
* Invoked when a panel is removed. May be called multiple times when moving panels.
*/
get onDidRemoveView(): Event<IPaneviewPanel> {
return this.component.onDidRemoveView;
}
/**
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
*/
get onDidDrop(): Event<PaneviewDidDropEvent> {
return this.component.onDidDrop;
}
get onUnhandledDragOverEvent(): Event<PaneviewDndOverlayEvent> {
return this.component.onUnhandledDragOverEvent;
}
constructor(private readonly component: IPaneviewComponent) {}
/**
* Remove a panel given the panel object.
*/
removePanel(panel: IPaneviewPanel): void {
this.component.removePanel(panel);
}
/**
* Get a panel object given a `string` id. May return `undefined`.
*/
getPanel(id: string): IPaneviewPanel | undefined {
return this.component.getPanel(id);
}
/**
* Move a panel given it's current and desired index.
*/
movePanel(from: number, to: number): void {
this.component.movePanel(from, to);
}
/**
* Focus the component. Will try to focus an active panel if one exists.
*/
focus(): void {
this.component.focus();
}
/**
* Force resize the component to an exact width and height. Read about auto-resizing before using.
*/
layout(width: number, height: number): void {
this.component.layout(width, height);
}
/**
* Add a panel and return the created object.
*/
addPanel<T extends object = Parameters>(
options: AddPaneviewComponentOptions<T>
): IPaneviewPanel {
return this.component.addPanel(options);
}
/**
* Create a component from a serialized object.
*/
fromJSON(data: SerializedPaneview): void {
this.component.fromJSON(data);
}
/**
* Create a serialized object of the current component.
*/
toJSON(): SerializedPaneview {
return this.component.toJSON();
}
/**
* Reset the component back to an empty and default state.
*/
clear(): void {
this.component.clear();
}
/**
* Update configuratable options.
*/
updateOptions(options: Partial<PaneviewComponentOptions>): void {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
}
export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
/**
* Width of the component.
*/
get width(): number {
return this.component.width;
}
/**
* Height of the component.
*/
get height(): number {
return this.component.height;
}
/**
* Minimum height of the component.
*/
get minimumHeight(): number {
return this.component.minimumHeight;
}
/**
* Maximum height of the component.
*/
get maximumHeight(): number {
return this.component.maximumHeight;
}
/**
* Minimum width of the component.
*/
get minimumWidth(): number {
return this.component.minimumWidth;
}
/**
* Maximum width of the component.
*/
get maximumWidth(): number {
return this.component.maximumWidth;
}
/**
* Invoked when any layout change occures, an aggregation of many events.
*/
get onDidLayoutChange(): Event<void> {
return this.component.onDidLayoutChange;
}
/**
* Invoked when a panel is added. May be called multiple times when moving panels.
*/
get onDidAddPanel(): Event<IGridviewPanel> {
return this.component.onDidAddGroup;
}
/**
* Invoked when a panel is removed. May be called multiple times when moving panels.
*/
get onDidRemovePanel(): Event<IGridviewPanel> {
return this.component.onDidRemoveGroup;
}
/**
* Invoked when the active panel changes. May be undefined if no panel is active.
*/
get onDidActivePanelChange(): Event<IGridviewPanel | undefined> {
return this.component.onDidActiveGroupChange;
}
/**
* Invoked after a layout is deserialzied using the `fromJSON` method.
*/
get onDidLayoutFromJSON(): Event<void> {
return this.component.onDidLayoutFromJSON;
}
/**
* All panel objects.
*/
get panels(): IGridviewPanel[] {
return this.component.groups;
}
/**
* Current orientation. Can be changed after initialization.
*/
get orientation(): Orientation {
return this.component.orientation;
}
set orientation(value: Orientation) {
this.component.updateOptions({ orientation: value });
}
constructor(private readonly component: IGridviewComponent) {}
/**
* Focus the component. Will try to focus an active panel if one exists.
*/
focus(): void {
this.component.focus();
}
/**
* Force resize the component to an exact width and height. Read about auto-resizing before using.
*/
layout(width: number, height: number, force = false): void {
this.component.layout(width, height, force);
}
/**
* Add a panel and return the created object.
*/
addPanel<T extends object = Parameters>(
options: AddComponentOptions<T>
): IGridviewPanel {
return this.component.addPanel(options);
}
/**
* Remove a panel given the panel object.
*/
removePanel(panel: IGridviewPanel, sizing?: Sizing): void {
this.component.removePanel(panel, sizing);
}
/**
* Move a panel in a particular direction relative to another panel.
*/
movePanel(
panel: IGridviewPanel,
options: { direction: Direction; reference: string; size?: number }
): void {
this.component.movePanel(panel, options);
}
/**
* Get a panel object given a `string` id. May return `undefined`.
*/
getPanel(id: string): IGridviewPanel | undefined {
return this.component.getPanel(id);
}
/**
* Create a component from a serialized object.
*/
fromJSON(data: SerializedGridviewComponent): void {
return this.component.fromJSON(data);
}
/**
* Create a serialized object of the current component.
*/
toJSON(): SerializedGridviewComponent {
return this.component.toJSON();
}
/**
* Reset the component back to an empty and default state.
*/
clear(): void {
this.component.clear();
}
updateOptions(options: Partial<GridviewComponentOptions>) {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
}
export class DockviewApi implements CommonApi<SerializedDockview> {
/**
* The unique identifier for this instance. Used to manage scope of Drag'n'Drop events.
*/
get id(): string {
return this.component.id;
}
/**
* Width of the component.
*/
get width(): number {
return this.component.width;
}
/**
* Height of the component.
*/
get height(): number {
return this.component.height;
}
/**
* Minimum height of the component.
*/
get minimumHeight(): number {
return this.component.minimumHeight;
}
/**
* Maximum height of the component.
*/
get maximumHeight(): number {
return this.component.maximumHeight;
}
/**
* Minimum width of the component.
*/
get minimumWidth(): number {
return this.component.minimumWidth;
}
/**
* Maximum width of the component.
*/
get maximumWidth(): number {
return this.component.maximumWidth;
}
/**
* Total number of groups.
*/
get size(): number {
return this.component.size;
}
/**
* Total number of panels.
*/
get totalPanels(): number {
return this.component.totalPanels;
}
/**
* Invoked when the active group changes. May be undefined if no group is active.
*/
get onDidActiveGroupChange(): Event<DockviewGroupPanel | undefined> {
return this.component.onDidActiveGroupChange;
}
/**
* Invoked when a group is added. May be called multiple times when moving groups.
*/
get onDidAddGroup(): Event<DockviewGroupPanel> {
return this.component.onDidAddGroup;
}
/**
* Invoked when a group is removed. May be called multiple times when moving groups.
*/
get onDidRemoveGroup(): Event<DockviewGroupPanel> {
return this.component.onDidRemoveGroup;
}
/**
* Invoked when the active panel changes. May be undefined if no panel is active.
*/
get onDidActivePanelChange(): Event<IDockviewPanel | undefined> {
return this.component.onDidActivePanelChange;
}
/**
* Invoked when a panel is added. May be called multiple times when moving panels.
*/
get onDidAddPanel(): Event<IDockviewPanel> {
return this.component.onDidAddPanel;
}
/**
* Invoked when a panel is removed. May be called multiple times when moving panels.
*/
get onDidRemovePanel(): Event<IDockviewPanel> {
return this.component.onDidRemovePanel;
}
get onDidMovePanel(): Event<MovePanelEvent> {
return this.component.onDidMovePanel;
}
/**
* Invoked after a layout is deserialzied using the `fromJSON` method.
*/
get onDidLayoutFromJSON(): Event<void> {
return this.component.onDidLayoutFromJSON;
}
/**
* Invoked when any layout change occures, an aggregation of many events.
*/
get onDidLayoutChange(): Event<void> {
return this.component.onDidLayoutChange;
}
/**
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
*/
get onDidDrop(): Event<DockviewDidDropEvent> {
return this.component.onDidDrop;
}
/**
* Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and
* prevent the event from occuring using the standard `preventDefault()` syntax.
*
* Preventing certain events may causes unexpected behaviours, use carefully.
*/
get onWillDrop(): Event<DockviewWillDropEvent> {
return this.component.onWillDrop;
}
/**
* Invoked before an overlay is shown indicating a drop target.
*
* Calling `event.preventDefault()` will prevent the overlay being shown and prevent
* the any subsequent drop event.
*/
get onWillShowOverlay(): Event<WillShowOverlayLocationEvent> {
return this.component.onWillShowOverlay;
}
/**
* Invoked before a group is dragged.
*
* Calling `event.nativeEvent.preventDefault()` will prevent the group drag starting.
*
*/
get onWillDragGroup(): Event<GroupDragEvent> {
return this.component.onWillDragGroup;
}
/**
* Invoked before a panel is dragged.
*
* Calling `event.nativeEvent.preventDefault()` will prevent the panel drag starting.
*/
get onWillDragPanel(): Event<TabDragEvent> {
return this.component.onWillDragPanel;
}
get onUnhandledDragOverEvent(): Event<DockviewDndOverlayEvent> {
return this.component.onUnhandledDragOverEvent;
}
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
return this.component.onDidPopoutGroupSizeChange;
}
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
return this.component.onDidPopoutGroupPositionChange;
}
/**
* All panel objects.
*/
get panels(): IDockviewPanel[] {
return this.component.panels;
}
/**
* All group objects.
*/
get groups(): DockviewGroupPanel[] {
return this.component.groups;
}
/**
* Active panel object.
*/
get activePanel(): IDockviewPanel | undefined {
return this.component.activePanel;
}
/**
* Active group object.
*/
get activeGroup(): DockviewGroupPanel | undefined {
return this.component.activeGroup;
}
constructor(private readonly component: IDockviewComponent) {}
/**
* Focus the component. Will try to focus an active panel if one exists.
*/
focus(): void {
this.component.focus();
}
/**
* Get a panel object given a `string` id. May return `undefined`.
*/
getPanel(id: string): IDockviewPanel | undefined {
return this.component.getGroupPanel(id);
}
/**
* Force resize the component to an exact width and height. Read about auto-resizing before using.
*/
layout(width: number, height: number, force = false): void {
this.component.layout(width, height, force);
}
/**
* Add a panel and return the created object.
*/
addPanel<T extends object = Parameters>(
options: AddPanelOptions<T>
): IDockviewPanel {
return this.component.addPanel(options);
}
/**
* Remove a panel given the panel object.
*/
removePanel(panel: IDockviewPanel): void {
this.component.removePanel(panel);
}
/**
* Add a group and return the created object.
*/
addGroup(options?: AddGroupOptions): DockviewGroupPanel {
return this.component.addGroup(options);
}
/**
* Close all groups and panels.
*/
closeAllGroups(): void {
return this.component.closeAllGroups();
}
/**
* Remove a group and any panels within the group.
*/
removeGroup(group: IDockviewGroupPanel): void {
this.component.removeGroup(<DockviewGroupPanel>group);
}
/**
* Get a group object given a `string` id. May return undefined.
*/
getGroup(id: string): DockviewGroupPanel | undefined {
return this.component.getPanel(id);
}
/**
* Add a floating group
*/
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: FloatingGroupOptions
): void {
return this.component.addFloatingGroup(item, options);
}
/**
* Create a component from a serialized object.
*/
fromJSON(data: SerializedDockview): void {
this.component.fromJSON(data);
}
/**
* Create a serialized object of the current component.
*/
toJSON(): SerializedDockview {
return this.component.toJSON();
}
/**
* Reset the component back to an empty and default state.
*/
clear(): void {
this.component.clear();
}
/**
* Move the focus progmatically to the next panel or group.
*/
moveToNext(options?: MovementOptions): void {
this.component.moveToNext(options);
}
/**
* Move the focus progmatically to the previous panel or group.
*/
moveToPrevious(options?: MovementOptions): void {
this.component.moveToPrevious(options);
}
maximizeGroup(panel: IDockviewPanel): void {
this.component.maximizeGroup(panel.group);
}
hasMaximizedGroup(): boolean {
return this.component.hasMaximizedGroup();
}
exitMaximizedGroup(): void {
this.component.exitMaximizedGroup();
}
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> {
return this.component.onDidMaximizedGroupChange;
}
/**
* Add a popout group in a new Window
*/
addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: {
position?: Box;
popoutUrl?: string;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
}
): Promise<boolean> {
return this.component.addPopoutGroup(item, options);
}
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
}

View File

@ -1,140 +0,0 @@
import { Position, positionToDirection } from '../dnd/droptarget';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import {
DockviewGroupChangeEvent,
DockviewGroupLocation,
} from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupMoveParams {
group?: DockviewGroupPanel;
position?: Position;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
}
export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent>;
readonly location: DockviewGroupLocation;
/**
* If you require the Window object
*/
getWindow(): Window;
moveTo(options: DockviewGroupMoveParams): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
close(): void;
}
export interface DockviewGroupPanelFloatingChangeEvent {
readonly location: DockviewGroupLocation;
}
const NOT_INITIALIZED_MESSAGE =
'dockview: DockviewGroupPanelApiImpl not initialized';
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined;
readonly _onDidLocationChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
get location(): DockviewGroupLocation {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
return this._group.model.location;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id, '__dockviewgroup__');
this.addDisposables(
this._onDidLocationChange,
this._onDidActivePanelChange
);
}
close(): void {
if (!this._group) {
return;
}
return this.accessor.removeGroup(this._group);
}
getWindow(): Window {
return this.location.type === 'popout'
? this.location.getWindow()
: window;
}
moveTo(options: DockviewGroupMoveParams): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
const group =
options.group ??
this.accessor.addGroup({
direction: positionToDirection(options.position ?? 'right'),
skipSetActive: true,
});
this.accessor.moveGroupOrPanel({
from: { groupId: this._group.id },
to: {
group,
position: options.group
? options.position ?? 'center'
: 'center',
index: options.index,
},
});
}
maximize(): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
if (this.location.type !== 'grid') {
// only grid groups can be maximized
return;
}
this.accessor.maximizeGroup(this._group);
}
isMaximized(): boolean {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
return this.accessor.isMaximizedGroup(this._group);
}
exitMaximized(): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
if (this.isMaximized()) {
this.accessor.exitMaximizedGroup();
}
}
initialize(group: DockviewGroupPanel): void {
this._group = group;
}
}

View File

@ -1,236 +0,0 @@
import { Emitter, Event } from '../events';
import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import {
DockviewGroupMoveParams,
DockviewGroupPanelFloatingChangeEvent,
} from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
export interface TitleEvent {
readonly title: string;
}
export interface RendererChangedEvent {
readonly renderer: DockviewPanelRenderer;
}
export interface ActiveGroupEvent {
readonly isActive: boolean;
}
export interface GroupChangedEvent {
// empty
}
export type DockviewPanelMoveParams = DockviewGroupMoveParams;
export interface DockviewPanelApi
extends Omit<
GridviewPanelApi,
// omit properties that do not make sense here
'setVisible' | 'onDidConstraintsChange' | 'setConstraints'
> {
/**
* The id of the tab component renderer
*
* Undefined if no custom tab renderer is provided
*/
readonly tabComponent: string | undefined;
readonly group: DockviewGroupPanel;
readonly isGroupActive: boolean;
readonly renderer: DockviewPanelRenderer;
readonly title: string | undefined;
readonly onDidActiveGroupChange: Event<ActiveGroupEvent>;
readonly onDidGroupChange: Event<GroupChangedEvent>;
readonly onDidTitleChange: Event<TitleEvent>;
readonly onDidRendererChange: Event<RendererChangedEvent>;
readonly location: DockviewGroupLocation;
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
close(): void;
setTitle(title: string): void;
setRenderer(renderer: DockviewPanelRenderer): void;
moveTo(options: DockviewPanelMoveParams): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
/**
* If you require the Window object
*/
getWindow(): Window;
}
export class DockviewPanelApiImpl
extends GridviewPanelApiImpl
implements DockviewPanelApi
{
private _group: DockviewGroupPanel;
private readonly _tabComponent: string | undefined;
readonly _onDidTitleChange = new Emitter<TitleEvent>();
readonly onDidTitleChange = this._onDidTitleChange.event;
private readonly _onDidActiveGroupChange = new Emitter<ActiveGroupEvent>();
readonly onDidActiveGroupChange = this._onDidActiveGroupChange.event;
private readonly _onDidGroupChange = new Emitter<GroupChangedEvent>();
readonly onDidGroupChange = this._onDidGroupChange.event;
readonly _onDidRendererChange = new Emitter<RendererChangedEvent>();
readonly onDidRendererChange = this._onDidRendererChange.event;
private readonly _onDidLocationChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
private readonly groupEventsDisposable = new MutableDisposable();
get location(): DockviewGroupLocation {
return this.group.api.location;
}
get title(): string | undefined {
return this.panel.title;
}
get isGroupActive(): boolean {
return this.group.isActive;
}
get renderer(): DockviewPanelRenderer {
return this.panel.renderer;
}
set group(value: DockviewGroupPanel) {
const oldGroup = this._group;
if (this._group !== value) {
this._group = value;
this._onDidGroupChange.fire({});
this.setupGroupEventListeners(oldGroup);
this._onDidLocationChange.fire({
location: this.group.api.location,
});
}
}
get group(): DockviewGroupPanel {
return this._group;
}
get tabComponent(): string | undefined {
return this._tabComponent;
}
constructor(
private readonly panel: DockviewPanel,
group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
component: string,
tabComponent?: string
) {
super(panel.id, component);
this._tabComponent = tabComponent;
this.initialize(panel);
this._group = group;
this.setupGroupEventListeners();
this.addDisposables(
this.groupEventsDisposable,
this._onDidRendererChange,
this._onDidTitleChange,
this._onDidGroupChange,
this._onDidActiveGroupChange,
this._onDidLocationChange
);
}
getWindow(): Window {
return this.group.api.getWindow();
}
moveTo(options: DockviewPanelMoveParams): void {
this.accessor.moveGroupOrPanel({
from: { groupId: this._group.id, panelId: this.panel.id },
to: {
group: options.group ?? this._group,
position: options.group
? options.position ?? 'center'
: 'center',
index: options.index,
},
});
}
setTitle(title: string): void {
this.panel.setTitle(title);
}
setRenderer(renderer: DockviewPanelRenderer): void {
this.panel.setRenderer(renderer);
}
close(): void {
this.group.model.closePanel(this.panel);
}
maximize(): void {
this.group.api.maximize();
}
isMaximized(): boolean {
return this.group.api.isMaximized();
}
exitMaximized(): void {
this.group.api.exitMaximized();
}
private setupGroupEventListeners(previousGroup?: DockviewGroupPanel) {
let _trackGroupActive = previousGroup?.isActive ?? false; // prevent duplicate events with same state
this.groupEventsDisposable.value = new CompositeDisposable(
this.group.api.onDidVisibilityChange((event) => {
const hasBecomeHidden = !event.isVisible && this.isVisible;
const hasBecomeVisible = event.isVisible && !this.isVisible;
const isActivePanel = this.group.model.isPanelActive(
this.panel
);
if (hasBecomeHidden || (hasBecomeVisible && isActivePanel)) {
this._onDidVisibilityChange.fire(event);
}
}),
this.group.api.onDidLocationChange((event) => {
if (this.group !== this.panel.group) {
return;
}
this._onDidLocationChange.fire(event);
}),
this.group.api.onDidActiveChange(() => {
if (this.group !== this.panel.group) {
return;
}
if (_trackGroupActive !== this.isGroupActive) {
_trackGroupActive = this.isGroupActive;
this._onDidActiveGroupChange.fire({
isActive: this.isGroupActive,
});
}
})
);
}
}

View File

@ -1,46 +0,0 @@
import {
DockviewApi,
GridviewApi,
PaneviewApi,
SplitviewApi,
} from '../api/component.api';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewComponentOptions } from '../dockview/options';
import { GridviewComponent } from '../gridview/gridviewComponent';
import { GridviewComponentOptions } from '../gridview/options';
import { PaneviewComponentOptions } from '../paneview/options';
import { PaneviewComponent } from '../paneview/paneviewComponent';
import { SplitviewComponentOptions } from '../splitview/options';
import { SplitviewComponent } from '../splitview/splitviewComponent';
export function createDockview(
element: HTMLElement,
options: DockviewComponentOptions
): DockviewApi {
const component = new DockviewComponent(element, options);
return component.api;
}
export function createSplitview(
element: HTMLElement,
options: SplitviewComponentOptions
): SplitviewApi {
const component = new SplitviewComponent(element, options);
return new SplitviewApi(component);
}
export function createGridview(
element: HTMLElement,
options: GridviewComponentOptions
): GridviewApi {
const component = new GridviewComponent(element, options);
return new GridviewApi(component);
}
export function createPaneview(
element: HTMLElement,
options: PaneviewComponentOptions
): PaneviewApi {
const component = new PaneviewComponent(element, options);
return new PaneviewApi(component);
}

View File

@ -1,68 +0,0 @@
import { Emitter, Event } from '../events';
import { IPanel } from '../panel/types';
import { FunctionOrValue } from '../types';
import { PanelApiImpl, PanelApi } from './panelApi';
export interface GridConstraintChangeEvent {
readonly minimumWidth?: number;
readonly minimumHeight?: number;
readonly maximumWidth?: number;
readonly maximumHeight?: number;
}
interface GridConstraintChangeEvent2 {
readonly minimumWidth?: FunctionOrValue<number>;
readonly minimumHeight?: FunctionOrValue<number>;
readonly maximumWidth?: FunctionOrValue<number>;
readonly maximumHeight?: FunctionOrValue<number>;
}
export interface SizeEvent {
readonly width?: number;
readonly height?: number;
}
export interface GridviewPanelApi extends PanelApi {
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent>;
setConstraints(value: GridConstraintChangeEvent2): void;
setSize(event: SizeEvent): void;
}
export class GridviewPanelApiImpl
extends PanelApiImpl
implements GridviewPanelApi
{
private readonly _onDidConstraintsChangeInternal =
new Emitter<GridConstraintChangeEvent2>();
readonly onDidConstraintsChangeInternal: Event<GridConstraintChangeEvent2> =
this._onDidConstraintsChangeInternal.event;
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>();
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent> =
this._onDidConstraintsChange.event;
private readonly _onDidSizeChange = new Emitter<SizeEvent>();
readonly onDidSizeChange: Event<SizeEvent> = this._onDidSizeChange.event;
constructor(id: string, component: string, panel?: IPanel) {
super(id, component);
this.addDisposables(
this._onDidConstraintsChangeInternal,
this._onDidConstraintsChange,
this._onDidSizeChange
);
if (panel) {
this.initialize(panel);
}
}
public setConstraints(value: GridConstraintChangeEvent): void {
this._onDidConstraintsChangeInternal.fire(value);
}
public setSize(event: SizeEvent): void {
this._onDidSizeChange.fire(event);
}
}

View File

@ -1,188 +0,0 @@
import { DockviewEvent, Emitter, Event } from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
export interface FocusEvent {
readonly isFocused: boolean;
}
export interface PanelDimensionChangeEvent {
readonly width: number;
readonly height: number;
}
export interface VisibilityEvent {
readonly isVisible: boolean;
}
export interface ActiveEvent {
readonly isActive: boolean;
}
export interface PanelApi {
// events
readonly onDidDimensionsChange: Event<PanelDimensionChangeEvent>;
readonly onDidFocusChange: Event<FocusEvent>;
readonly onDidVisibilityChange: Event<VisibilityEvent>;
readonly onDidActiveChange: Event<ActiveEvent>;
readonly onDidParametersChange: Event<Parameters>;
setActive(): void;
setVisible(isVisible: boolean): void;
updateParameters(parameters: Parameters): void;
/**
* The id of the component renderer
*/
readonly component: string;
/**
* The id of the panel that would have been assigned when the panel was created
*/
readonly id: string;
/**
* Whether the panel holds the current focus
*/
readonly isFocused: boolean;
/**
* Whether the panel is the actively selected panel
*/
readonly isActive: boolean;
/**
* Whether the panel is visible
*/
readonly isVisible: boolean;
/**
* The panel width in pixels
*/
readonly width: number;
/**
* The panel height in pixels
*/
readonly height: number;
readonly onWillFocus: Event<WillFocusEvent>;
getParameters<T extends Parameters = Parameters>(): T;
}
export class WillFocusEvent extends DockviewEvent {
constructor() {
super();
}
}
/**
* A core api implementation that should be used across all panel-like objects
*/
export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _isFocused = false;
private _isActive = false;
private _isVisible = true;
private _width = 0;
private _height = 0;
private _parameters: Parameters = {};
private readonly panelUpdatesDisposable = new MutableDisposable();
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>();
readonly onDidDimensionsChange = this._onDidDimensionChange.event;
readonly _onDidChangeFocus = new Emitter<FocusEvent>();
readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event;
//
readonly _onWillFocus = new Emitter<WillFocusEvent>();
readonly onWillFocus: Event<WillFocusEvent> = this._onWillFocus.event;
//
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>();
readonly onDidVisibilityChange: Event<VisibilityEvent> =
this._onDidVisibilityChange.event;
readonly _onWillVisibilityChange = new Emitter<VisibilityEvent>();
readonly onWillVisibilityChange: Event<VisibilityEvent> =
this._onWillVisibilityChange.event;
readonly _onDidActiveChange = new Emitter<ActiveEvent>();
readonly onDidActiveChange: Event<ActiveEvent> =
this._onDidActiveChange.event;
readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event;
readonly _onDidParametersChange = new Emitter<Parameters>();
readonly onDidParametersChange: Event<Parameters> =
this._onDidParametersChange.event;
get isFocused(): boolean {
return this._isFocused;
}
get isActive(): boolean {
return this._isActive;
}
get isVisible(): boolean {
return this._isVisible;
}
get width(): number {
return this._width;
}
get height(): number {
return this._height;
}
constructor(readonly id: string, readonly component: string) {
super();
this.addDisposables(
this.onDidFocusChange((event) => {
this._isFocused = event.isFocused;
}),
this.onDidActiveChange((event) => {
this._isActive = event.isActive;
}),
this.onDidVisibilityChange((event) => {
this._isVisible = event.isVisible;
}),
this.onDidDimensionsChange((event) => {
this._width = event.width;
this._height = event.height;
}),
this.panelUpdatesDisposable,
this._onDidDimensionChange,
this._onDidChangeFocus,
this._onDidVisibilityChange,
this._onDidActiveChange,
this._onWillFocus,
this._onActiveChange,
this._onWillFocus,
this._onWillVisibilityChange,
this._onDidParametersChange
);
}
getParameters<T extends Parameters = Parameters>(): T {
return this._parameters as T;
}
public initialize(panel: IPanel): void {
this.panelUpdatesDisposable.value = this._onDidParametersChange.event(
(parameters) => {
this._parameters = parameters;
panel.update({
params: parameters,
});
}
);
}
setVisible(isVisible: boolean): void {
this._onWillVisibilityChange.fire({ isVisible });
}
setActive(): void {
this._onActiveChange.fire();
}
updateParameters(parameters: Parameters): void {
this._onDidParametersChange.fire(parameters);
}
}

View File

@ -1,55 +0,0 @@
import { Emitter, Event } from '../events';
import { PaneviewPanel } from '../paneview/paneviewPanel';
import { SplitviewPanelApi, SplitviewPanelApiImpl } from './splitviewPanelApi';
export interface ExpansionEvent {
readonly isExpanded: boolean;
}
export interface PaneviewPanelApi extends SplitviewPanelApi {
readonly isExpanded: boolean;
readonly onDidExpansionChange: Event<ExpansionEvent>;
readonly onMouseEnter: Event<MouseEvent>;
readonly onMouseLeave: Event<MouseEvent>;
setExpanded(isExpanded: boolean): void;
}
export class PaneviewPanelApiImpl
extends SplitviewPanelApiImpl
implements PaneviewPanelApi
{
readonly _onDidExpansionChange = new Emitter<ExpansionEvent>({
replay: true,
});
readonly onDidExpansionChange: Event<ExpansionEvent> =
this._onDidExpansionChange.event;
readonly _onMouseEnter = new Emitter<MouseEvent>({});
readonly onMouseEnter: Event<MouseEvent> = this._onMouseEnter.event;
readonly _onMouseLeave = new Emitter<MouseEvent>({});
readonly onMouseLeave: Event<MouseEvent> = this._onMouseLeave.event;
private _pane: PaneviewPanel | undefined;
set pane(pane: PaneviewPanel) {
this._pane = pane;
}
constructor(id: string, component: string) {
super(id, component);
this.addDisposables(
this._onDidExpansionChange,
this._onMouseEnter,
this._onMouseLeave
);
}
setExpanded(isExpanded: boolean): void {
this._pane?.setExpanded(isExpanded);
}
get isExpanded(): boolean {
return !!this._pane?.isExpanded();
}
}

View File

@ -1,65 +0,0 @@
import { Emitter, Event } from '../events';
import { IDisposable } from '../lifecycle';
import { FunctionOrValue } from '../types';
import { PanelApiImpl, PanelApi } from './panelApi';
interface PanelConstraintChangeEvent2 {
readonly minimumSize?: FunctionOrValue<number>;
readonly maximumSize?: FunctionOrValue<number>;
}
export interface PanelConstraintChangeEvent {
readonly minimumSize?: number;
readonly maximumSize?: number;
}
export interface PanelSizeEvent {
readonly size: number;
}
export interface SplitviewPanelApi extends PanelApi {
readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent>;
setConstraints(value: PanelConstraintChangeEvent2): void;
setSize(event: PanelSizeEvent): void;
}
export class SplitviewPanelApiImpl
extends PanelApiImpl
implements SplitviewPanelApi, IDisposable
{
readonly _onDidConstraintsChangeInternal =
new Emitter<PanelConstraintChangeEvent2>();
readonly onDidConstraintsChangeInternal: Event<PanelConstraintChangeEvent2> =
this._onDidConstraintsChangeInternal.event;
//
readonly _onDidConstraintsChange = new Emitter<PanelConstraintChangeEvent>({
replay: true,
});
readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent> =
this._onDidConstraintsChange.event;
//
readonly _onDidSizeChange = new Emitter<PanelSizeEvent>();
readonly onDidSizeChange: Event<PanelSizeEvent> =
this._onDidSizeChange.event;
//
constructor(id: string, component: string) {
super(id, component);
this.addDisposables(
this._onDidConstraintsChangeInternal,
this._onDidConstraintsChange,
this._onDidSizeChange
);
}
setConstraints(value: PanelConstraintChangeEvent2) {
this._onDidConstraintsChangeInternal.fire(value);
}
setSize(event: PanelSizeEvent) {
this._onDidSizeChange.fire(event);
}
}

View File

@ -1,3 +0,0 @@
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };

View File

@ -1,84 +0,0 @@
import { disableIframePointEvents } from '../dom';
import { addDisposableListener, Emitter } from '../events';
import {
CompositeDisposable,
IDisposable,
MutableDisposable,
} from '../lifecycle';
export abstract class DragHandler extends CompositeDisposable {
private readonly dataDisposable = new MutableDisposable();
private readonly pointerEventsDisposable = new MutableDisposable();
private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event;
constructor(protected readonly el: HTMLElement) {
super();
this.addDisposables(
this._onDragStart,
this.dataDisposable,
this.pointerEventsDisposable
);
this.configure();
}
abstract getData(event: DragEvent): IDisposable;
protected isCancelled(_event: DragEvent): boolean {
return false;
}
private configure(): void {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
if (event.defaultPrevented || this.isCancelled(event)) {
event.preventDefault();
return;
}
const iframes = disableIframePointEvents();
this.pointerEventsDisposable.value = {
dispose: () => {
iframes.release();
},
};
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.dataDisposable.value = this.getData(event);
this._onDragStart.fire(event);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
const hasData = event.dataTransfer.items.length > 0;
if (!hasData) {
/**
* Although this is not used by dockview many third party dnd libraries will check
* dataTransfer.types to determine valid drag events.
*
* For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled
* through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
* dnd logic. You can see the code at
P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/
event.dataTransfer.setData('text/plain', '');
}
}
}),
addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose();
setTimeout(() => {
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
})
);
}
}

View File

@ -1,88 +0,0 @@
class TransferObject {
// intentionally empty class
}
export class PanelTransfer extends TransferObject {
constructor(
public readonly viewId: string,
public readonly groupId: string,
public readonly panelId: string | null
) {
super();
}
}
export class PaneTransfer extends TransferObject {
constructor(
public readonly viewId: string,
public readonly paneId: string
) {
super();
}
}
/**
* A singleton to store transfer data during drag & drop operations that are only valid within the application.
*/
export class LocalSelectionTransfer<T> {
private static readonly INSTANCE = new LocalSelectionTransfer();
private data?: T[];
private proto?: T;
private constructor() {
// protect against external instantiation
}
static getInstance<T>(): LocalSelectionTransfer<T> {
return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T>;
}
hasData(proto: T): boolean {
return proto && proto === this.proto;
}
clearData(proto: T): void {
if (this.hasData(proto)) {
this.proto = undefined;
this.data = undefined;
}
}
getData(proto: T): T[] | undefined {
if (this.hasData(proto)) {
return this.data;
}
return undefined;
}
setData(data: T[], proto: T): void {
if (proto) {
this.data = data;
this.proto = proto;
}
}
}
export function getPanelData(): PanelTransfer | undefined {
const panelTransfer = LocalSelectionTransfer.getInstance<PanelTransfer>();
const isPanelEvent = panelTransfer.hasData(PanelTransfer.prototype);
if (!isPanelEvent) {
return undefined;
}
return panelTransfer.getData(PanelTransfer.prototype)![0];
}
export function getPaneData(): PaneTransfer | undefined {
const paneTransfer = LocalSelectionTransfer.getInstance<PaneTransfer>();
const isPanelEvent = paneTransfer.hasData(PaneTransfer.prototype);
if (!isPanelEvent) {
return undefined;
}
return paneTransfer.getData(PaneTransfer.prototype)![0];
}

View File

@ -1,109 +0,0 @@
import { addDisposableListener } from '../events';
import { CompositeDisposable } from '../lifecycle';
export interface IDragAndDropObserverCallbacks {
onDragEnter: (e: DragEvent) => void;
onDragLeave: (e: DragEvent) => void;
onDrop: (e: DragEvent) => void;
onDragEnd: (e: DragEvent) => void;
onDragOver?: (e: DragEvent) => void;
}
export class DragAndDropObserver extends CompositeDisposable {
private target: EventTarget | null = null;
constructor(
private readonly element: HTMLElement,
private readonly callbacks: IDragAndDropObserverCallbacks
) {
super();
this.registerListeners();
}
onDragEnter(e: DragEvent): void {
this.target = e.target;
this.callbacks.onDragEnter(e);
}
onDragOver(e: DragEvent): void {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
}
onDragLeave(e: DragEvent): void {
if (this.target === e.target) {
this.target = null;
this.callbacks.onDragLeave(e);
}
}
onDragEnd(e: DragEvent): void {
this.target = null;
this.callbacks.onDragEnd(e);
}
onDrop(e: DragEvent): void {
this.callbacks.onDrop(e);
}
private registerListeners(): void {
this.addDisposables(
addDisposableListener(
this.element,
'dragenter',
(e: DragEvent) => {
this.onDragEnter(e);
},
true
)
);
this.addDisposables(
addDisposableListener(
this.element,
'dragover',
(e: DragEvent) => {
this.onDragOver(e);
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.onDragLeave(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.onDragEnd(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.onDrop(e);
})
);
}
}
export interface IDraggedCompositeData {
eventData: DragEvent;
dragAndDropData: any;
}
export interface ICompositeDragAndDropObserverCallbacks {
onDragEnter?: (e: IDraggedCompositeData) => void;
onDragLeave?: (e: IDraggedCompositeData) => void;
onDrop?: (e: IDraggedCompositeData) => void;
onDragOver?: (e: IDraggedCompositeData) => void;
onDragStart?: (e: IDraggedCompositeData) => void;
onDragEnd?: (e: IDraggedCompositeData) => void;
}

View File

@ -1,23 +0,0 @@
.dv-drop-target-container {
position: absolute;
z-index: 9999;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
pointer-events: none;
overflow: hidden;
--dv-transition-duration: 300ms;
.dv-drop-target-anchor {
position: relative;
border: var(--dv-drag-over-border);
transition: opacity var(--dv-transition-duration) ease-in,
top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out;
background-color: var(--dv-drag-over-background-color);
opacity: 1;
}
}

View File

@ -1,102 +0,0 @@
import { CompositeDisposable, Disposable } from '../lifecycle';
import { DropTargetTargetModel } from './droptarget';
export class DropTargetAnchorContainer extends CompositeDisposable {
private _model:
| { root: HTMLElement; overlay: HTMLElement; changed: boolean }
| undefined;
private _outline: HTMLElement | undefined;
private _disabled = false;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
if (this.disabled === value) {
return;
}
this._disabled = value;
if (value) {
this.model?.clear();
}
}
get model(): DropTargetTargetModel | undefined {
if (this.disabled) {
return undefined;
}
return {
clear: () => {
if (this._model) {
this._model.root.parentElement?.removeChild(
this._model.root
);
}
this._model = undefined;
},
exists: () => {
return !!this._model;
},
getElements: (event?: DragEvent, outline?: HTMLElement) => {
const changed = this._outline !== outline;
this._outline = outline;
if (this._model) {
this._model.changed = changed;
return this._model;
}
const container = this.createContainer();
const anchor = this.createAnchor();
this._model = { root: container, overlay: anchor, changed };
container.appendChild(anchor);
this.element.appendChild(container);
if (event?.target instanceof HTMLElement) {
const targetBox = event.target.getBoundingClientRect();
const box = this.element.getBoundingClientRect();
anchor.style.left = `${targetBox.left - box.left}px`;
anchor.style.top = `${targetBox.top - box.top}px`;
}
return this._model;
},
};
}
constructor(readonly element: HTMLElement, options: { disabled: boolean }) {
super();
this._disabled = options.disabled;
this.addDisposables(
Disposable.from(() => {
this.model?.clear();
})
);
}
private createContainer(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-container';
return el;
}
private createAnchor(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-anchor';
el.style.visibility = 'hidden';
return el;
}
}

View File

@ -1,54 +0,0 @@
.dv-drop-target {
position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone {
position: absolute;
left: 0px;
top: 0px;
height: 100%;
width: 100%;
z-index: 1000;
pointer-events: none;
> .dv-drop-target-selection {
position: relative;
box-sizing: border-box;
height: 100%;
width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color);
transition: top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out,
opacity var(--dv-transition-duration) ease-out;
will-change: transform;
pointer-events: none;
&.dv-drop-target-top {
&.dv-drop-target-small-vertical {
border-top: 1px solid var(--dv-drag-over-border-color);
}
}
&.dv-drop-target-bottom {
&.dv-drop-target-small-vertical {
border-bottom: 1px solid var(--dv-drag-over-border-color);
}
}
&.dv-drop-target-left {
&.dv-drop-target-small-horizontal {
border-left: 1px solid var(--dv-drag-over-border-color);
}
}
&.dv-drop-target-right {
&.dv-drop-target-small-horizontal {
border-right: 1px solid var(--dv-drag-over-border-color);
}
}
}
}
}

View File

@ -1,644 +0,0 @@
import { toggleClass } from '../dom';
import { DockviewEvent, Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd';
import { clamp } from '../math';
import { Direction } from '../gridview/baseComponentGridview';
export interface DroptargetEvent {
readonly position: Position;
readonly nativeEvent: DragEvent;
}
export class WillShowOverlayEvent
extends DockviewEvent
implements DroptargetEvent
{
get nativeEvent(): DragEvent {
return this.options.nativeEvent;
}
get position(): Position {
return this.options.position;
}
constructor(
private readonly options: {
nativeEvent: DragEvent;
position: Position;
}
) {
super();
}
}
export function directionToPosition(direction: Direction): Position {
switch (direction) {
case 'above':
return 'top';
case 'below':
return 'bottom';
case 'left':
return 'left';
case 'right':
return 'right';
case 'within':
return 'center';
default:
throw new Error(`invalid direction '${direction}'`);
}
}
export function positionToDirection(position: Position): Direction {
switch (position) {
case 'top':
return 'above';
case 'bottom':
return 'below';
case 'left':
return 'left';
case 'right':
return 'right';
case 'center':
return 'within';
default:
throw new Error(`invalid position '${position}'`);
}
}
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center';
export type CanDisplayOverlay = (
dragEvent: DragEvent,
state: Position
) => boolean;
export type MeasuredValue = { value: number; type: 'pixels' | 'percentage' };
export type DroptargetOverlayModel = {
size?: MeasuredValue;
activationSize?: MeasuredValue;
};
const DEFAULT_ACTIVATION_SIZE: MeasuredValue = {
value: 20,
type: 'percentage',
};
const DEFAULT_SIZE: MeasuredValue = {
value: 50,
type: 'percentage',
};
const SMALL_WIDTH_BOUNDARY = 100;
const SMALL_HEIGHT_BOUNDARY = 100;
export interface DropTargetTargetModel {
getElements(
event?: DragEvent,
outline?: HTMLElement
): {
root: HTMLElement;
overlay: HTMLElement;
changed: boolean;
};
exists(): boolean;
clear(): void;
}
export interface DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
}
export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined;
private _state: Position | undefined;
private _acceptedTargetZonesSet: Set<Position>;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private readonly _onWillShowOverlay = new Emitter<WillShowOverlayEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayEvent> =
this._onWillShowOverlay.event;
readonly dnd: DragAndDropObserver;
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
private static ACTUAL_TARGET: Droptarget | undefined;
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
get state(): Position | undefined {
return this._state;
}
constructor(
private readonly element: HTMLElement,
private readonly options: DroptargetOptions
) {
super();
this._disabled = false;
// use a set to take advantage of #<set>.has
this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => {
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
const target =
this.options.getOverlayOutline?.() ?? this.element;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = (e.clientX ?? 0) - rect.left;
const y = (e.clientY ?? 0) - rect.top;
const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet,
x,
y,
width,
height
);
/**
* If the event has already been used by another DropTarget instance
* then don't show a second drop target, only one target should be
* active at any one time
*/
if (this.isAlreadyUsed(e) || quadrant === null) {
// no drop target should be displayed
this.removeDropTarget();
return;
}
if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
const willShowOverlayEvent = new WillShowOverlayEvent({
nativeEvent: e,
position: quadrant,
});
/**
* Provide an opportunity to prevent the overlay appearing and in turn
* any dnd behaviours
*/
this._onWillShowOverlay.fire(willShowOverlayEvent);
if (willShowOverlayEvent.defaultPrevented) {
this.removeDropTarget();
return;
}
this.markAsUsed(e);
if (overrideTraget) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'dv-drop-target-dropzone';
this.overlayElement = document.createElement('div');
this.overlayElement.className = 'dv-drop-target-selection';
this._state = 'center';
this.targetElement.appendChild(this.overlayElement);
target.classList.add('dv-drop-target');
target.append(this.targetElement);
// this.overlayElement.style.opacity = '0';
// requestAnimationFrame(() => {
// if (this.overlayElement) {
// this.overlayElement.style.opacity = '';
// }
// });
}
this.toggleClasses(quadrant, width, height);
this._state = quadrant;
},
onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget();
},
onDragEnd: (e) => {
const target = this.options.getOverrideTarget?.();
if (target && Droptarget.ACTUAL_TARGET === this) {
if (this._state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({
position: this._state,
nativeEvent: e,
});
}
}
this.removeDropTarget();
target?.clear();
},
onDrop: (e) => {
e.preventDefault();
const state = this._state;
this.removeDropTarget();
this.options.getOverrideTarget?.()?.clear();
if (state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({ position: state, nativeEvent: e });
}
},
});
this.addDisposables(this._onDrop, this._onWillShowOverlay, this.dnd);
}
setTargetZones(acceptedTargetZones: Position[]): void {
this._acceptedTargetZonesSet = new Set(acceptedTargetZones);
}
setOverlayModel(model: DroptargetOverlayModel): void {
this.options.overlayModel = model;
}
dispose(): void {
this.removeDropTarget();
super.dispose();
}
/**
* Add a property to the event object for other potential listeners to check
*/
private markAsUsed(event: DragEvent): void {
(event as any)[Droptarget.USED_EVENT_ID] = true;
}
/**
* Check is the event has already been used by another instance of DropTarget
*/
private isAlreadyUsed(event: DragEvent): boolean {
const value = (event as any)[Droptarget.USED_EVENT_ID];
return typeof value === 'boolean' && value;
}
private toggleClasses(
quadrant: Position,
width: number,
height: number
): void {
const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
return;
}
const isSmallX = width < SMALL_WIDTH_BOUNDARY;
const isSmallY = height < SMALL_HEIGHT_BOUNDARY;
const isLeft = quadrant === 'left';
const isRight = quadrant === 'right';
const isTop = quadrant === 'top';
const isBottom = quadrant === 'bottom';
const rightClass = !isSmallX && isRight;
const leftClass = !isSmallX && isLeft;
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
let size = 1;
const sizeOptions = this.options.overlayModel?.size ?? DEFAULT_SIZE;
if (sizeOptions.type === 'percentage') {
size = clamp(sizeOptions.value, 0, 100) / 100;
} else {
if (rightClass || leftClass) {
size = clamp(0, sizeOptions.value, width) / width;
}
if (topClass || bottomClass) {
size = clamp(0, sizeOptions.value, height) / height;
}
}
if (target) {
const outlineEl =
this.options.getOverlayOutline?.() ?? this.element;
const elBox = outlineEl.getBoundingClientRect();
const ta = target.getElements(undefined, outlineEl);
const el = ta.root;
const overlay = ta.overlay;
const bigbox = el.getBoundingClientRect();
const rootTop = elBox.top - bigbox.top;
const rootLeft = elBox.left - bigbox.left;
const box = {
top: rootTop,
left: rootLeft,
width: width,
height: height,
};
if (rightClass) {
box.left = rootLeft + width * (1 - size);
box.width = width * size;
} else if (leftClass) {
box.width = width * size;
} else if (topClass) {
box.height = height * size;
} else if (bottomClass) {
box.top = rootTop + height * (1 - size);
box.height = height * size;
}
if (isSmallX && isLeft) {
box.width = 4;
}
if (isSmallX && isRight) {
box.left = rootLeft + width - 4;
box.width = 4;
}
const topPx = `${Math.round(box.top)}px`;
const leftPx = `${Math.round(box.left)}px`;
const widthPx = `${Math.round(box.width)}px`;
const heightPx = `${Math.round(box.height)}px`;
if (
overlay.style.top === topPx &&
overlay.style.left === leftPx &&
overlay.style.width === widthPx &&
overlay.style.height === heightPx
) {
return;
}
overlay.style.top = topPx;
overlay.style.left = leftPx;
overlay.style.width = widthPx;
overlay.style.height = heightPx;
overlay.style.visibility = 'visible';
overlay.className = `dv-drop-target-anchor${
this.options.className ? ` ${this.options.className}` : ''
}`;
toggleClass(overlay, 'dv-drop-target-left', isLeft);
toggleClass(overlay, 'dv-drop-target-right', isRight);
toggleClass(overlay, 'dv-drop-target-top', isTop);
toggleClass(overlay, 'dv-drop-target-bottom', isBottom);
toggleClass(
overlay,
'dv-drop-target-center',
quadrant === 'center'
);
if (ta.changed) {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
true
);
setTimeout(() => {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
false
);
}, 10);
}
return;
}
if (!this.overlayElement) {
return;
}
const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
/**
* You can also achieve the overlay placement using the transform CSS property
* to translate and scale the element however this has the undesired effect of
* 'skewing' the element. Comment left here for anybody that ever revisits this.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/transform
*
* right
* translateX(${100 * (1 - size) / 2}%) scaleX(${scale})
*
* left
* translateX(-${100 * (1 - size) / 2}%) scaleX(${scale})
*
* top
* translateY(-${100 * (1 - size) / 2}%) scaleY(${scale})
*
* bottom
* translateY(${100 * (1 - size) / 2}%) scaleY(${scale})
*/
if (rightClass) {
box.left = `${100 * (1 - size)}%`;
box.width = `${100 * size}%`;
} else if (leftClass) {
box.width = `${100 * size}%`;
} else if (topClass) {
box.height = `${100 * size}%`;
} else if (bottomClass) {
box.top = `${100 * (1 - size)}%`;
box.height = `${100 * size}%`;
}
this.overlayElement.style.top = box.top;
this.overlayElement.style.left = box.left;
this.overlayElement.style.width = box.width;
this.overlayElement.style.height = box.height;
toggleClass(
this.overlayElement,
'dv-drop-target-small-vertical',
isSmallY
);
toggleClass(
this.overlayElement,
'dv-drop-target-small-horizontal',
isSmallX
);
toggleClass(this.overlayElement, 'dv-drop-target-left', isLeft);
toggleClass(this.overlayElement, 'dv-drop-target-right', isRight);
toggleClass(this.overlayElement, 'dv-drop-target-top', isTop);
toggleClass(this.overlayElement, 'dv-drop-target-bottom', isBottom);
toggleClass(
this.overlayElement,
'dv-drop-target-center',
quadrant === 'center'
);
}
private calculateQuadrant(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number
): Position | null {
const activationSizeOptions =
this.options.overlayModel?.activationSize ??
DEFAULT_ACTIVATION_SIZE;
const isPercentage = activationSizeOptions.type === 'percentage';
if (isPercentage) {
return calculateQuadrantAsPercentage(
overlayType,
x,
y,
width,
height,
activationSizeOptions.value
);
}
return calculateQuadrantAsPixels(
overlayType,
x,
y,
width,
height,
activationSizeOptions.value
);
}
private removeDropTarget(): void {
if (this.targetElement) {
this._state = undefined;
this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined;
this.overlayElement = undefined;
}
}
}
export function calculateQuadrantAsPercentage(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
const xp = (100 * x) / width;
const yp = (100 * y) / height;
if (overlayType.has('left') && xp < threshold) {
return 'left';
}
if (overlayType.has('right') && xp > 100 - threshold) {
return 'right';
}
if (overlayType.has('top') && yp < threshold) {
return 'top';
}
if (overlayType.has('bottom') && yp > 100 - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}
export function calculateQuadrantAsPixels(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
if (overlayType.has('left') && x < threshold) {
return 'left';
}
if (overlayType.has('right') && x > width - threshold) {
return 'right';
}
if (overlayType.has('top') && y < threshold) {
return 'top';
}
if (overlayType.has('bottom') && y > height - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}

View File

@ -1,21 +0,0 @@
import { addClasses, removeClasses } from '../dom';
export function addGhostImage(
dataTransfer: DataTransfer,
ghostElement: HTMLElement,
options?: { x?: number; y?: number }
): void {
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
addClasses(ghostElement, 'dv-dragged');
// move the element off-screen initially otherwise it may in some cases be rendered at (0,0) momentarily
ghostElement.style.top = '-9999px';
document.body.appendChild(ghostElement);
dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0);
setTimeout(() => {
removeClasses(ghostElement, 'dv-dragged');
ghostElement.remove();
}, 0);
}

View File

@ -1,88 +0,0 @@
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events';
import { IDisposable } from '../lifecycle';
import { DragHandler } from './abstractDragHandler';
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
import { addGhostImage } from './ghost';
export class GroupDragHandler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
constructor(
element: HTMLElement,
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super(element);
this.addDisposables(
addDisposableListener(
element,
'pointerdown',
(e) => {
if (e.shiftKey) {
/**
* You cannot call e.preventDefault() because that will prevent drag events from firing
* but we also need to stop any group overlay drag events from occuring
* Use a custom event marker that can be checked by the overlay drag events
*/
quasiPreventDefault(e);
}
},
true
)
);
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.api.location.type === 'floating' && !_event.shiftKey) {
return true;
}
return false;
}
getData(dragEvent: DragEvent): IDisposable {
const dataTransfer = dragEvent.dataTransfer;
this.panelTransfer.setData(
[new PanelTransfer(this.accessor.id, this.group.id, null)],
PanelTransfer.prototype
);
const style = window.getComputedStyle(this.el);
const bgColor = style.getPropertyValue(
'--dv-activegroup-visiblepanel-tab-background-color'
);
const color = style.getPropertyValue(
'--dv-activegroup-visiblepanel-tab-color'
);
if (dataTransfer) {
const ghostElement = document.createElement('div');
ghostElement.style.backgroundColor = bgColor;
ghostElement.style.color = color;
ghostElement.style.padding = '2px 8px';
ghostElement.style.height = '24px';
ghostElement.style.fontSize = '11px';
ghostElement.style.lineHeight = '20px';
ghostElement.style.borderRadius = '12px';
ghostElement.style.position = 'absolute';
ghostElement.style.pointerEvents = 'none';
ghostElement.style.top = '-9999px';
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 });
}
return {
dispose: () => {
this.panelTransfer.clearData(PanelTransfer.prototype);
},
};
}
}

View File

@ -1,198 +0,0 @@
import {
CompositeDisposable,
IDisposable,
MutableDisposable,
} from '../../../lifecycle';
import { Emitter, Event } from '../../../events';
import { trackFocus } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { Droptarget } from '../../../dnd/droptarget';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
export interface IContentContainer extends IDisposable {
readonly dropTarget: Droptarget;
onDidFocus: Event<void>;
onDidBlur: Event<void>;
element: HTMLElement;
layout(width: number, height: number): void;
openPanel: (panel: IDockviewPanel) => void;
closePanel: () => void;
show(): void;
hide(): void;
renderPanel(panel: IDockviewPanel, options: { asActive: boolean }): void;
}
export class ContentContainer
extends CompositeDisposable
implements IContentContainer
{
private readonly _element: HTMLElement;
private panel: IDockviewPanel | undefined;
private readonly disposable = new MutableDisposable();
private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
private readonly _onDidBlur = new Emitter<void>();
readonly onDidBlur: Event<void> = this._onDidBlur.event;
get element(): HTMLElement {
return this._element;
}
readonly dropTarget: Droptarget;
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanelModel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-content-container';
this._element.tabIndex = -1;
this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, {
getOverlayOutline: () => {
return accessor.options.theme?.dndPanelOverlay === 'group'
? this.element.parentElement
: null;
},
className: 'dv-drop-target-content',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (
this.group.locked === 'no-drop-target' ||
(this.group.locked && position === 'center')
) {
return false;
}
const data = getPanelData();
if (
!data &&
event.shiftKey &&
this.group.location.type !== 'floating'
) {
return false;
}
if (data && data.viewId === this.accessor.id) {
return true;
}
return this.group.canDisplayOverlay(event, position, 'content');
},
getOverrideTarget: target ? () => target.model : undefined,
});
this.addDisposables(this.dropTarget);
}
show(): void {
this.element.style.display = '';
}
hide(): void {
this.element.style.display = 'none';
}
renderPanel(
panel: IDockviewPanel,
options: { asActive: boolean } = { asActive: true }
): void {
const doRender =
options.asActive ||
(this.panel && this.group.isPanelActive(this.panel));
if (
this.panel &&
this.panel.view.content.element.parentElement === this._element
) {
/**
* If the currently attached panel is mounted directly to the content then remove it
*/
this._element.removeChild(this.panel.view.content.element);
}
this.panel = panel;
let container: HTMLElement;
switch (panel.api.renderer) {
case 'onlyWhenVisible':
this.group.renderContainer.detatch(panel);
if (this.panel) {
if (doRender) {
this._element.appendChild(
this.panel.view.content.element
);
}
}
container = this._element;
break;
case 'always':
if (
panel.view.content.element.parentElement === this._element
) {
this._element.removeChild(panel.view.content.element);
}
container = this.group.renderContainer.attach({
panel,
referenceContainer: this,
});
break;
default:
throw new Error(
`dockview: invalid renderer type '${panel.api.renderer}'`
);
}
if (doRender) {
const focusTracker = trackFocus(container);
const disposable = new CompositeDisposable();
disposable.addDisposables(
focusTracker,
focusTracker.onDidFocus(() => this._onDidFocus.fire()),
focusTracker.onDidBlur(() => this._onDidBlur.fire())
);
this.disposable.value = disposable;
}
}
public openPanel(panel: IDockviewPanel): void {
if (this.panel === panel) {
return;
}
this.renderPanel(panel);
}
public layout(_width: number, _height: number): void {
// noop
}
public closePanel(): void {
if (this.panel) {
if (this.panel.api.renderer === 'onlyWhenVisible') {
this.panel.view.content.element.parentElement?.removeChild(
this.panel.view.content.element
);
}
}
this.panel = undefined;
}
public dispose(): void {
this.disposable.dispose();
super.dispose();
}
}

View File

@ -1,87 +0,0 @@
import { shiftAbsoluteElementIntoView } from '../../dom';
import { addDisposableListener } from '../../events';
import {
CompositeDisposable,
Disposable,
MutableDisposable,
} from '../../lifecycle';
export class PopupService extends CompositeDisposable {
private readonly _element: HTMLElement;
private _active: HTMLElement | null = null;
private readonly _activeDisposable = new MutableDisposable();
constructor(private readonly root: HTMLElement) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-popover-anchor';
this._element.style.position = 'relative';
this.root.prepend(this._element);
this.addDisposables(
Disposable.from(() => {
this.close();
}),
this._activeDisposable
);
}
openPopover(
element: HTMLElement,
position: { x: number; y: number; zIndex?: string }
): void {
this.close();
const wrapper = document.createElement('div');
wrapper.style.position = 'absolute';
wrapper.style.zIndex = position.zIndex ?? 'var(--dv-overlay-z-index)';
wrapper.appendChild(element);
const anchorBox = this._element.getBoundingClientRect();
const offsetX = anchorBox.left;
const offsetY = anchorBox.top;
wrapper.style.top = `${position.y - offsetY}px`;
wrapper.style.left = `${position.x - offsetX}px`;
this._element.appendChild(wrapper);
this._active = wrapper;
this._activeDisposable.value = new CompositeDisposable(
addDisposableListener(window, 'pointerdown', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
let el: HTMLElement | null = target;
while (el && el !== wrapper) {
el = el?.parentElement ?? null;
}
if (el) {
return; // clicked within popover
}
this.close();
})
);
requestAnimationFrame(() => {
shiftAbsoluteElementIntoView(wrapper, this.root);
});
}
close(): void {
if (this._active) {
this._active.remove();
this._activeDisposable.dispose();
this._active = null;
}
}
}

View File

@ -1,83 +0,0 @@
.dv-dragged {
transform: translate3d(
0px,
0px,
0px
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
}
.dv-tab {
flex-shrink: 0;
&:focus-within,
&:focus {
position: relative;
&::after {
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 0px;
left: 0px;
pointer-events: none;
outline: 1px solid var(--dv-tab-divider-color) !important;
outline-offset: -1px;
z-index: 5;
}
}
&.dv-tab-dragging {
.dv-default-tab-action {
background-color: var(--dv-activegroup-visiblepanel-tab-color);
}
}
&.dv-active-tab {
.dv-default-tab {
.dv-default-tab-action {
visibility: visible;
}
}
}
&.dv-inactive-tab {
.dv-default-tab {
.dv-default-tab-action {
visibility: hidden;
}
&:hover {
.dv-default-tab-action {
visibility: visible;
}
}
}
}
.dv-default-tab {
position: relative;
height: 100%;
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
.dv-default-tab-content {
flex-grow: 1;
margin-right: 4px;
}
.dv-default-tab-action {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More