Compare commits

..

No commits in common. "master" and "v1.8.0" have entirely different histories.

756 changed files with 17876 additions and 94107 deletions

View File

@ -1,8 +1,6 @@
{
"packages": [
"packages/dockview-core",
"packages/dockview-vue",
"packages/dockview-react",
"packages/dockview"
],
"sandboxes": [
@ -11,29 +9,18 @@
"/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",
@ -42,5 +29,5 @@
"/packages/docs/sandboxes/javascript/tabheight-dockview",
"/packages/docs/sandboxes/javascript/vanilla-dockview"
],
"node": "18"
}
"node": "16"
}

View File

@ -30,11 +30,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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.
@ -45,7 +45,7 @@ jobs:
# 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
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -59,4 +59,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v1

View File

@ -1,20 +1,21 @@
name: Deploy Docs
on:
workflow_dispatch:
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@v4
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: '20.x'
node-version: '16.x'
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
@ -26,10 +27,6 @@ jobs:
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

View File

@ -7,16 +7,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# might be required for sonar to work correctly
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: '20.x'
node-version: '16.x'
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
@ -24,10 +24,11 @@ jobs:
${{ runner.os }}-node-
- run: yarn
- run: npm run bootstrap
- run: npm run build
- run: npm run test:cov
- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v5
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_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

1
.gitignore vendored
View File

@ -14,4 +14,3 @@ test-report.xml
yarn-error.log
/build
/docs/
/generated/

View File

@ -7,8 +7,7 @@
"esbenp.prettier-vscode",
"redhat.vscode-yaml",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"vue.volar"
"editorconfig.editorconfig"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []

View File

@ -1,18 +1,17 @@
<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 written in TypeScript</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)
[![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)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview)](https://bundlephobia.com/result?p=dockview)
##
@ -22,17 +21,33 @@ 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
- Simple splitviews, nested splitviews (i.e. gridviews) supporting full layout managment with
dockable and tabular views
- Extensive API support at the component level and view level
- Themable and customizable
- Serialization / deserialization support
- Tabular docking and Drag and Drop support
- Floating groups, customized header bars and tab
- Documentation and examples
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#user-content-provenance).
Want to inspect the latest deployment? Go to https://unpkg.com/browse/dockview@latest/
## 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,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';

View File

@ -8,7 +8,7 @@ const config: JestConfigWithTsJest = {
collectCoverageFrom: ['<rootDir>/packages/*/src/**/*.{js,jsx,ts,tsx}'],
coveragePathIgnorePatterns: [
'/node_modules/',
'<rootDir>/packages/*/src/__tests__/',
'<rootDir>packages/*/src/__tests__/',
],
coverageDirectory: 'coverage',
testResultsProcessor: 'jest-sonar-reporter',

View File

@ -2,11 +2,12 @@
"packages": [
"packages/*"
],
"version": "4.2.5",
"useWorkspaces": true,
"version": "1.8.0",
"npmClient": "yarn",
"command": {
"publish": {
"message": "chore(release): publish %s"
}
}
}
}

View File

@ -1,81 +1,71 @@
{
"name": "dockview-monorepo-root",
"private": true,
"workspaces": [
"packages/*"
],
"nohoist": [
"**/babel-jest",
"**/babel-jest/**"
],
"description": "Monorepo for https://github.com/mathuo/dockview",
"homepage": "https://github.com/mathuo/dockview#readme",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
"scripts": {
"test": "jest",
"lint": "eslint packages/**/src/** --ext .ts,.tsx,.js,.jsx",
"package": "node scripts/package.js",
"package-all": "lerna run docs --scope '{dockview-core,dockview}' && node scripts/package.js",
"build": "lerna run build --scope '{dockview-core,dockview}'",
"clean": "lerna run clean",
"bootstrap": "lerna bootstrap",
"test:cov": "jest --coverage",
"version-beta-build": "lerna version prerelease --preid beta",
"publish-app": "lerna publish",
"docs": "typedoc",
"package-docs": "node scripts/package-docs.js"
},
"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"
"license": "MIT",
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"homepage": "https://github.com/mathuo/dockview#readme",
"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",
"@testing-library/dom": "^8.20.0",
"@testing-library/jest-dom": "^5.16.5",
"@types/jest": "^29.4.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"codecov": "^3.8.3",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"fs-extra": "^11.2.0",
"css-loader": "^6.7.3",
"eslint": "^8.34.0",
"fs-extra": "^11.1.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",
"gulp-dart-sass": "^1.0.2",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.4.3",
"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"
"jsdom": "^21.1.0",
"lerna": "^6.5.1",
"merge2": "^1.4.1",
"rimraf": "^4.1.2",
"sass": "^1.58.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typedoc": "^0.24.7",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"engines": {
"node": ">=18.0"
}
"dependencies": {}
}

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,14 +1,13 @@
<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, groups, grids and splitviews 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)
@ -16,23 +15,37 @@
##
![](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
- Simple splitviews, nested splitviews (i.e. gridviews) supporting full layout managment with
dockable and tabular views
- Extensive API support at the component level and view level
- Themable and customizable
- Serialization / deserialization support
- Tabular docking and Drag and Drop support
- Floating groups, customized header bars and tab
- Documentation and examples
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
Want to inspect the latest deployment? Go to https://unpkg.com/browse/dockview-core@latest/
## Quick start
You can install dockview-core from [npm](https://www.npmjs.com/package/dockview-core).
```
npm install --save dockview-core
```
Within your project you must import or reference the stylesheet at `dockview-core/dist/styles/dockview.css` and attach a theme.
```css
@import '~dockview-core/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,13 +1,6 @@
const gulp = require('gulp');
const gulpSass = require('gulp-dart-sass');
const concat = require('gulp-concat');
const buildfile = require('../../scripts/build');
gulp.task('sass', () => {
return gulp
.src('./src/**/*.scss')
.pipe(gulpSass().on('error', gulpSass.logError))
.pipe(concat('dockview.css'))
.pipe(gulp.dest('./dist/styles/'));
});
buildfile.init();
gulp.task('run', gulp.series(['sass']));

View File

@ -12,7 +12,6 @@ const config: JestConfigWithTsJest = {
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__',

View File

@ -1,7 +1,37 @@
{
"name": "dockview-core",
"version": "4.2.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"version": "1.8.0",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
"module": "./dist/esm/index.js",
"repository": {
"type": "git",
"url": "https://github.com/mathuo/dockview.git"
},
"bugs": {
"url": "https://github.com/mathuo/dockview/issues"
},
"homepage": "https://github.com/mathuo/dockview",
"scripts": {
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
"build:cjs": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.json --extendedDiagnostics",
"build:css": "gulp sass",
"build:esm": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.esm.json --extendedDiagnostics",
"build:bundles": "rollup -c",
"build": "npm run build:package && npm run build:bundles",
"clean": "rimraf dist/ .build/ .rollup.cache/",
"prepublishOnly": "npm run rebuild && npm run test",
"docs": "typedoc",
"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",
"dev-publish": "node ./scripts/publishExperimental.js"
},
"files": [
"dist",
"README.md"
],
"keywords": [
"splitview",
"split-view",
@ -23,34 +53,16 @@
"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"
"license": "MIT",
"devDependencies": {
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.0.0",
"cross-env": "^7.0.3",
"postcss": "^8.4.21",
"rimraf": "^4.1.2",
"rollup": "^3.15.0",
"rollup-plugin-postcss": "^4.0.2",
"typedoc": "^0.23.25"
}
}

View File

@ -0,0 +1,63 @@
const cp = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const rootDir = path.join(__dirname, '..');
const publishDir = path.join(rootDir, '__publish__');
cp.execSync('npm run clean', { cwd: rootDir, stdio: 'inherit' });
cp.execSync('npm run test', { cwd: __dirname, stdio: 'inherit' });
cp.execSync('npm run build', { cwd: rootDir, stdio: 'inherit' });
if (fs.existsSync(publishDir)) {
fs.removeSync(publishDir);
}
fs.mkdirSync(publishDir);
if (!fs.existsSync(path.join(publishDir, 'dist'))) {
fs.mkdirSync(path.join(publishDir, 'dist'));
}
const package = JSON.parse(
fs.readFileSync(path.join(rootDir, 'package.json')).toString()
);
for (const file of package.files) {
fs.copySync(path.join(rootDir, file), path.join(publishDir, file));
}
const result = cp
.execSync('git rev-parse --short HEAD', {
cwd: rootDir,
})
.toString()
.replace(/\n/g, '');
function formatDate() {
const date = new Date();
function pad(value) {
if (value.toString().length === 1) {
return `0${value}`;
}
return value;
}
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
date.getDate()
)}`;
}
package.version = `0.0.0-experimental-${result}-${formatDate()}`;
package.scripts = {};
fs.writeFileSync(
path.join(publishDir, 'package.json'),
JSON.stringify(package, null, 4)
);
const command = 'npm publish --tag experimental';
cp.execSync(command, { cwd: publishDir, stdio: 'inherit' });
fs.removeSync(publishDir);

View File

@ -1,28 +1,23 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import {
TabPartInitParameters,
GroupPanelPartInitParameters,
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
readonly tabComponent?: string,
readonly tab?: ITabRenderer
) {
//
}
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return this.tab;
}
init(params: TabPartInitParameters): void {
init(params: GroupPanelPartInitParameters): 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 +1,4 @@
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;
import * as React from 'react';
export function setMockRefElement(node: Partial<HTMLElement>): void {
const mockRef = {
@ -21,53 +12,3 @@ export function setMockRefElement(node: Partial<HTMLElement>): void {
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

@ -5,7 +5,7 @@ describe('api', () => {
let api: PanelApiImpl;
beforeEach(() => {
api = new PanelApiImpl('dummy_id', 'fake-component');
api = new PanelApiImpl('dummy_id');
});
test('updateParameters', () => {

View File

@ -1,17 +1,15 @@
import { DockviewPanelApiImpl } from '../../api/dockviewPanelApi';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('groupPanelApi', () => {
test('title', () => {
const accessor = fromPartial<DockviewComponent>({
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
};
const panelMock = jest.fn<DockviewPanel, []>(() => {
return {
@ -19,21 +17,17 @@ describe('groupPanelApi', () => {
setTitle: jest.fn(),
} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panel = new panelMock();
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
onDidLocationChange: jest.fn(),
onDidActiveChange: jest.fn(),
},
});
const group = new groupMock();
const cut = new DockviewPanelApiImpl(
panel,
group,
<DockviewComponent>accessor,
'fake-component'
<DockviewComponent>accessor
);
cut.setTitle('test_title');
@ -42,18 +36,16 @@ describe('groupPanelApi', () => {
});
test('updateParameters', () => {
const groupPanel: Partial<DockviewPanel> = {
const groupPanel: Partial<IDockviewPanel> = {
id: 'test_id',
update: jest.fn(),
};
const accessor = fromPartial<DockviewComponent>({
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
};
const groupViewPanel = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
@ -61,10 +53,9 @@ describe('groupPanelApi', () => {
);
const cut = new DockviewPanelApiImpl(
<DockviewPanel>groupPanel,
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor,
'fake-component'
<DockviewComponent>accessor
);
cut.updateParameters({ keyA: 'valueA' });
@ -76,17 +67,15 @@ describe('groupPanelApi', () => {
});
test('onDidGroupChange', () => {
const groupPanel: Partial<DockviewPanel> = {
const groupPanel: Partial<IDockviewPanel> = {
id: 'test_id',
};
const accessor = fromPartial<DockviewComponent>({
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
};
const groupViewPanel = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
@ -94,10 +83,9 @@ describe('groupPanelApi', () => {
);
const cut = new DockviewPanelApiImpl(
<DockviewPanel>groupPanel,
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor,
'fake-component'
<DockviewComponent>accessor
);
let events = 0;

View File

@ -70,8 +70,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragEnd(element);
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy();
handler.dispose();
@ -114,8 +114,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy();
handler.dispose();
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy();
});
@ -172,7 +172,7 @@ describe('abstractDragHandler', () => {
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0);
expect(spy).toBeCalledTimes(0);
handler.dispose();
});

View File

@ -7,7 +7,19 @@ import {
positionToDirection,
} from '../../dnd/droptarget';
import { fireEvent } from '@testing-library/dom';
import { createOffsetDragOverEvent } from '../__test_utils__/utils';
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;
}
describe('droptarget', () => {
let element: HTMLElement;
@ -16,10 +28,10 @@ describe('droptarget', () => {
beforeEach(() => {
element = document.createElement('div');
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {
@ -53,7 +65,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
@ -61,7 +73,7 @@ describe('droptarget', () => {
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();
expect(element.querySelector('.drop-target-dropzone')).toBeNull();
});
test('directionToPosition', () => {
@ -102,7 +114,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
@ -124,7 +136,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.dv-drop-target-dropzone'
'.drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -155,12 +167,12 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
const target = element.querySelector(
'.dv-drop-target-dropzone'
'.drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -171,37 +183,18 @@ describe('droptarget', () => {
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'
'.drop-target > .drop-target-dropzone > .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%',
}
);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(-25%) scaleX(0.5)');
fireEvent(
target,
@ -209,21 +202,17 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
'.drop-target > .drop-target-dropzone > .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%',
}
);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(-25%) scaleY(0.5)');
fireEvent(
target,
@ -231,21 +220,17 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
'.drop-target > .drop-target-dropzone > .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%',
}
);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(25%) scaleY(0.5)');
fireEvent(
target,
@ -253,21 +238,18 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
'.drop-target > .drop-target-dropzone > .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%',
}
);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(25%) scaleX(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
@ -276,14 +258,14 @@ describe('droptarget', () => {
expect(
(
element
.getElementsByClassName('dv-drop-target-selection')
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target);
expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.dv-drop-target');
viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0);
});

View File

@ -2,7 +2,6 @@ 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', () => {
@ -11,17 +10,13 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id',
api: { location: { type: 'grid' } } as any,
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'test_accessor_id' } as DockviewComponent,
group
);
const cut = new GroupDragHandler(element, 'test_accessor_id', group);
fireEvent.dragStart(element, new Event('dragstart'));
@ -48,22 +43,18 @@ describe('groupDragHandler', () => {
cut.dispose();
});
test('that the event is cancelled when floating and shiftKey=true', () => {
test('that the event is cancelled when isFloating and shiftKey=true', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { location: { type: 'floating' } } as any,
api: { isFloating: true } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'accessor_id' } as DockviewComponent,
group
);
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
@ -85,17 +76,13 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { location: { type: 'grid' } } as any,
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(
element,
{ id: 'accessor_id' } as DockviewComponent,
group
);
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });

View File

@ -0,0 +1,156 @@
import { Overlay } from '../../dnd/overlay';
describe('overlay', () => {
test('toJSON', () => {
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 { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('that out-of-bounds dimensions are fixed', () => {
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 { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('setBounds', () => {
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 { left: 300, top: 400, width: 1000, height: 1000 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 0, top: 0, width: 1000, height: 1000 } as any;
}
);
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');
});
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();
});
});

View File

@ -1,17 +1,14 @@
import { fireEvent } from '@testing-library/dom';
import { Emitter, Event } from '../../../../events';
import { ContentContainer } from '../../../../dockview/components/panel/content';
import {
GroupPanelPartInitParameters,
GroupPanelContentPartInitParameters,
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
@ -19,13 +16,17 @@ class TestContentRenderer
{
readonly element: HTMLElement;
readonly _onDidFocus = new Emitter<void>();
readonly _onDidBlur = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
readonly onDidBlur: Event<void> = this._onDidBlur.event;
constructor(public id: string) {
super();
this.element = document.createElement('div');
this.element.id = id;
}
init(parameters: GroupPanelPartInitParameters): void {
init(parameters: GroupPanelContentPartInitParameters): void {
//
}
@ -55,21 +56,7 @@ describe('contentContainer', () => {
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,
})
);
const cut = new ContentContainer();
disposable.addDisposables(
cut.onDidFocus(() => {
@ -82,12 +69,11 @@ describe('contentContainer', () => {
const contentRenderer = new TestContentRenderer('id-1');
const panel = fromPartial<IDockviewPanel>({
const panel = {
view: {
content: contentRenderer,
},
api: { renderer: 'onlyWhenVisible' },
});
} as Partial<IDockviewPanelModel>,
} as Partial<IDockviewPanel>;
cut.openPanel(panel as IDockviewPanel);
@ -105,70 +91,45 @@ describe('contentContainer', () => {
expect(focus).toBe(1);
expect(blur).toBe(1);
// renderer explicitly asks for focus
contentRenderer._onDidFocus.fire();
expect(focus).toBe(2);
expect(blur).toBe(1);
// renderer explicitly looses focus
contentRenderer._onDidBlur.fire();
expect(focus).toBe(2);
expect(blur).toBe(2);
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);
expect(focus).toBe(2);
expect(blur).toBe(2);
// previous renderer events should no longer be attached to container
contentRenderer._onDidFocus.fire();
contentRenderer._onDidBlur.fire();
expect(focus).toBe(2);
expect(blur).toBe(2);
// new panel recieves focus
fireEvent.focus(contentRenderer2.element);
expect(focus).toBe(2);
expect(blur).toBe(1);
expect(focus).toBe(3);
expect(blur).toBe(2);
// new panel looses focus
fireEvent.blur(contentRenderer2.element);
jest.runAllTimers();
expect(focus).toBe(2);
expect(blur).toBe(2);
expect(focus).toBe(3);
expect(blur).toBe(3);
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,44 +1,31 @@
import { fireEvent } from '@testing-library/dom';
import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../dnd/dataTransfer';
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()
);
const cut = new Tab('panelId', new accessorMock(), new groupMock());
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
expect(cut.element.className).toBe('tab 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()
);
const cut = new Tab('panelId', new accessorMock(), new groupMock());
cut.setActive(true);
expect(cut.element.className).toBe('dv-tab dv-active-tab');
expect(cut.element.className).toBe('tab active-tab');
cut.setActive(false);
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
expect(cut.element.className).toBe('tab inactive-tab');
});
test('that an external event does not render a drop target and calls through to the group model', () => {
@ -47,10 +34,15 @@ describe('tab', () => {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -62,39 +54,40 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
groupPanel
);
const cut = new Tab('panelId', accessor, groupPanel);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(groupView.canDisplayOverlay).toBeCalled();
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that if you drag over yourself a drop target is shown', () => {
test('that if you drag over yourself no 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 = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -106,16 +99,12 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
const cut = new Tab('panel1', accessor, groupPanel);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -127,11 +116,11 @@ describe('tab', () => {
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that if you drag over another tab a drop target is shown', () => {
@ -160,16 +149,12 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
const cut = new Tab('panel1', accessor, groupPanel);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -184,7 +169,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
});
@ -214,16 +199,12 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
const cut = new Tab('panel1', accessor, groupPanel);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -244,7 +225,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
@ -274,16 +255,12 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
groupPanel
);
const cut = new Tab('panel1', accessor, groupPanel);
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -304,7 +281,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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

@ -9,18 +9,16 @@ import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanel
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 accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -37,52 +35,54 @@ describe('tabsContainer', () => {
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace!) {
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(groupView.canDisplayOverlay).toBeCalled();
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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 accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const dropTargetContainer = document.createElement('div');
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -92,22 +92,23 @@ describe('tabsContainer', () => {
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace!) {
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -122,29 +123,25 @@ describe('tabsContainer', () => {
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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 accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -163,6 +160,7 @@ describe('tabsContainer', () => {
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
@ -171,17 +169,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace!) {
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -190,25 +188,25 @@ describe('tabsContainer', () => {
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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 accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -227,6 +225,7 @@ describe('tabsContainer', () => {
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
@ -235,17 +234,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace!) {
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -254,25 +253,25 @@ describe('tabsContainer', () => {
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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 accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -290,6 +289,7 @@ describe('tabsContainer', () => {
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
@ -298,17 +298,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace!) {
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
@ -323,35 +323,36 @@ describe('tabsContainer', () => {
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace!);
fireEvent.dragOver(emptySpace!);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1);
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
cut.element.getElementsByClassName('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 accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
@ -364,7 +365,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -379,7 +380,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left2);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -391,7 +392,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(undefined);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container'
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
@ -399,24 +400,25 @@ describe('tabsContainer', () => {
});
test('right actions', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
@ -429,7 +431,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -444,7 +446,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right2);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -456,7 +458,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(undefined);
query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container'
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
@ -464,27 +466,28 @@ describe('tabsContainer', () => {
});
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 accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'grid' } } as any,
api: { isFloating: false } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!;
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -499,49 +502,52 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const event = new KeyboardEvent('mousedown', { 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);
expect(accessor.addFloatingGroup).toBeCalledWith(
groupPanel,
{
x: 100,
y: 60,
},
{ inDragMode: true }
);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy2).toBeCalledTimes(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 accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'floating' } } as any,
api: { isFloating: true } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!;
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -556,312 +562,80 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const event = new KeyboardEvent('mousedown', { 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);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy2).toBeCalledTimes(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 accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { location: { type: 'floating' } } as any,
api: { isFloating: true } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const createPanel = (id: string) =>
fromPartial<IDockviewPanel>({
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 = createPanel('test_id');
const panel = new panelMock('test_id');
cut.openPanel(panel);
const el = cut.element.querySelector('.dv-tab')!;
const el = cut.element.querySelector('.tab')!;
expect(el).toBeTruthy();
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const event = new KeyboardEvent('mousedown', { 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);
expect(preventDefaultSpy).toBeCalledTimes(0);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
const panel2 = createPanel('test_id_2');
const panel2 = new panelMock('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();
expect(preventDefaultSpy).toBeCalledTimes(1);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
});
});

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

@ -0,0 +1,27 @@
import { DockviewApi } from '../../../../api/component.api';
import { Watermark } from '../../../../dockview/components/watermark/watermark';
describe('watermark', () => {
test('that the group is closed when the close button is clicked', () => {
const cut = new Watermark();
const mockApi = jest.fn<Partial<DockviewApi>, any[]>(() => {
return {
removeGroup: jest.fn(),
};
});
const api = <DockviewApi>new mockApi();
const group = jest.fn() as any;
cut.init({ containerApi: api });
cut.updateParentGroup(group, true);
const closeEl = cut.element.querySelector('.close-action')!;
expect(closeEl).toBeTruthy();
closeEl.dispatchEvent(new Event('click'));
expect(api.removeGroup).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

@ -7,7 +7,7 @@ import {
ITabRenderer,
IWatermarkRenderer,
} from '../../dockview/types';
import { PanelUpdateEvent, Parameters } from '../../panel/types';
import { PanelUpdateEvent } from '../../panel/types';
import {
DockviewGroupPanelModel,
GroupOptions,
@ -20,11 +20,6 @@ import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { WatermarkRendererInitParameters } from '../../dockview/types';
import { createOffsetDragOverEvent } from '../__test_utils__/utils';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
import { Emitter } from '../../events';
import { fromPartial } from '@total-typescript/shoehorn';
import { TabLocation } from '../../dockview/framework';
enum GroupChangeKind2 {
ADD_PANEL,
@ -37,16 +32,12 @@ class TestModel implements IDockviewPanelModel {
readonly contentComponent: string;
readonly tab: ITabRenderer;
constructor(readonly id: string) {
constructor(id: string) {
this.content = new TestHeaderPart(id);
this.contentComponent = id;
this.tab = new TestContentPart(id);
}
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return new TestHeaderPart(this.id);
}
update(event: PanelUpdateEvent): void {
//
}
@ -102,6 +93,10 @@ class Watermark implements IWatermarkRenderer {
return {};
}
updateParentGroup() {
//
}
dispose() {
//
}
@ -183,7 +178,7 @@ export class TestPanel implements IDockviewPanel {
return this._group!;
}
get params(): Parameters {
get params(): Record<string, any> {
return {};
}
@ -199,11 +194,7 @@ export class TestPanel implements IDockviewPanel {
this._params = params;
}
updateParentGroup(group: DockviewGroupPanel): void {
//
}
runEvents(): void {
updateParentGroup(group: DockviewGroupPanel, isGroupActive: boolean): void {
//
}
@ -235,7 +226,7 @@ export class TestPanel implements IDockviewPanel {
}
}
describe('dockviewGroupPanelModel', () => {
describe('groupview', () => {
let groupview: DockviewGroupPanel;
let dockview: DockviewComponent;
let options: GroupOptions;
@ -243,21 +234,13 @@ describe('dockviewGroupPanelModel', () => {
let removePanelMock: jest.Mock;
let removeGroupMock: jest.Mock;
let panelApi: DockviewPanelApi;
beforeEach(() => {
removePanelMock = jest.fn();
removeGroupMock = jest.fn();
options = {};
panelApi = fromPartial<DockviewPanelApi>({
renderer: 'onlyWhenVisible',
onDidTitleChange: new Emitter().event,
onDidParametersChange: new Emitter().event,
});
dockview = fromPartial<DockviewComponent>({
dockview = (<Partial<DockviewComponent>>{
options: {},
createWatermarkComponent: () => new Watermark(),
doSetGroupActive: jest.fn(),
@ -266,21 +249,16 @@ describe('dockviewGroupPanelModel', () => {
removeGroup: removeGroupMock,
onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: () => ({ dispose: jest.fn() }),
});
}) as DockviewComponent;
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
groupview.initialize();
});
test('panel events are captured during de-serialization', () => {
const panel1 = new TestPanel('panel1', panelApi);
const panel2 = new TestPanel('panel2', panelApi);
const panel3 = new TestPanel('panel3', panelApi);
const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any);
const panel3 = new TestPanel('panel3', jest.fn() as any);
const groupview2 = new DockviewGroupPanel(dockview, 'groupview-2', {
panels: [panel1, panel2, panel3],
@ -364,9 +342,9 @@ describe('dockviewGroupPanelModel', () => {
})
);
const panel1 = new TestPanel('panel1', panelApi);
const panel2 = new TestPanel('panel2', panelApi);
const panel3 = new TestPanel('panel3', panelApi);
const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any);
const panel3 = new TestPanel('panel3', jest.fn() as any);
expect(events.length).toBe(0);
@ -444,9 +422,9 @@ describe('dockviewGroupPanelModel', () => {
});
test('moveToPrevious and moveToNext', () => {
const panel1 = new TestPanel('panel1', panelApi);
const panel2 = new TestPanel('panel2', panelApi);
const panel3 = new TestPanel('panel3', panelApi);
const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any);
const panel3 = new TestPanel('panel3', jest.fn() as any);
groupview.model.openPanel(panel1);
groupview.model.openPanel(panel2);
@ -479,20 +457,20 @@ describe('dockviewGroupPanelModel', () => {
test('default', () => {
let viewQuery = groupview.element.querySelectorAll(
'.dv-groupview > .dv-tabs-and-actions-container'
'.groupview > .tabs-and-actions-container'
);
expect(viewQuery).toBeTruthy();
viewQuery = groupview.element.querySelectorAll(
'.dv-groupview > .dv-content-container'
'.groupview > .content-container'
);
expect(viewQuery).toBeTruthy();
});
test('closeAllPanels with panels', () => {
const panel1 = new TestPanel('panel1', panelApi);
const panel2 = new TestPanel('panel2', panelApi);
const panel3 = new TestPanel('panel3', panelApi);
const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any);
const panel3 = new TestPanel('panel3', jest.fn() as any);
groupview.model.openPanel(panel1);
groupview.model.openPanel(panel2);
@ -500,25 +478,21 @@ describe('dockviewGroupPanelModel', () => {
groupview.model.closeAllPanels();
expect(removePanelMock).toHaveBeenCalledWith(panel1, undefined);
expect(removePanelMock).toHaveBeenCalledWith(panel2, undefined);
expect(removePanelMock).toHaveBeenCalledWith(panel3, undefined);
expect(removePanelMock).toBeCalledWith(panel1);
expect(removePanelMock).toBeCalledWith(panel2);
expect(removePanelMock).toBeCalledWith(panel3);
});
test('closeAllPanels with no panels', () => {
groupview.model.closeAllPanels();
expect(removeGroupMock).toHaveBeenCalledWith(groupview);
expect(removeGroupMock).toBeCalledWith(groupview);
});
test('that group is set on panel during onDidAddPanel event', () => {
const cut = new DockviewComponent(document.createElement('div'), {
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
const cut = new DockviewComponent({
parentElement: document.createElement('div'),
components: {
component: TestContentPart,
},
});
@ -531,19 +505,12 @@ describe('dockviewGroupPanelModel', () => {
});
test('toJSON() default', () => {
const dockviewComponent = new DockviewComponent(
document.createElement('div'),
{
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
}
);
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
components: {
component: TestContentPart,
},
});
const cut = new DockviewGroupPanelModel(
document.createElement('div'),
@ -561,19 +528,12 @@ describe('dockviewGroupPanelModel', () => {
});
test('toJSON() locked and hideHeader', () => {
const dockviewComponent = new DockviewComponent(
document.createElement('div'),
{
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
}
);
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
components: {
component: TestContentPart,
},
});
const cut = new DockviewGroupPanelModel(
document.createElement('div'),
@ -596,19 +556,12 @@ describe('dockviewGroupPanelModel', () => {
});
test("that openPanel with skipSetActive doesn't set panel to active", () => {
const dockviewComponent = new DockviewComponent(
document.createElement('div'),
{
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
}
);
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
components: {
component: TestContentPart,
},
});
const groupviewContainer = document.createElement('div');
const cut = new DockviewGroupPanelModel(
@ -619,24 +572,24 @@ describe('dockviewGroupPanelModel', () => {
null as any
);
const contentContainer = groupviewContainer
.getElementsByClassName('dv-content-container')
.getElementsByClassName('content-container')
.item(0)!.childNodes;
const panel1 = new TestPanel('id_1', panelApi);
const panel1 = new TestPanel('id_1', null as any);
cut.openPanel(panel1);
expect(contentContainer.length).toBe(1);
expect(contentContainer.item(0)).toBe(panel1.view.content.element);
const panel2 = new TestPanel('id_2', panelApi);
const panel2 = new TestPanel('id_2', null as any);
cut.openPanel(panel2);
expect(contentContainer.length).toBe(1);
expect(contentContainer.item(0)).toBe(panel2.view.content.element);
const panel3 = new TestPanel('id_2', panelApi);
const panel3 = new TestPanel('id_2', null as any);
cut.openPanel(panel3, { skipSetActive: true });
cut.openPanel(panel3, { skipSetPanelActive: true });
expect(contentContainer.length).toBe(1);
expect(contentContainer.item(0)).toBe(panel2.view.content.element);
@ -646,15 +599,18 @@ describe('dockviewGroupPanelModel', () => {
});
test('that should not show drop target is external event', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
options: {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -683,205 +639,39 @@ describe('dockviewGroupPanelModel', () => {
new groupPanelMock() as DockviewGroupPanel
);
let counter = 0;
cut.onUnhandledDragOverEvent(() => {
counter++;
});
const element = container
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
.getElementsByClassName('content-container')
.item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
expect(counter).toBe(1);
expect(accessor.options.showDndOverlay).toBeCalledTimes(1);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that the .locked behaviour is as', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
id: 'testgroupid',
model: groupView,
};
}
);
const container = document.createElement('div');
const cut = new DockviewGroupPanelModel(
container,
accessor,
'groupviewid',
{},
new groupPanelMock() as DockviewGroupPanel
);
cut.onUnhandledDragOverEvent((e) => {
e.accept();
});
const element = container
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
function run(value: number) {
fireEvent.dragEnter(element);
fireEvent(
element,
createOffsetDragOverEvent({ clientX: value, clientY: value })
);
}
// base case - not locked
cut.locked = false;
run(10);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
fireEvent.dragEnd(element);
// special case - locked with no possible target
cut.locked = 'no-drop-target';
run(10);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
fireEvent.dragEnd(element);
// standard locked - only show if not center target
cut.locked = true;
run(10);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
fireEvent.dragEnd(element);
// standard locked but for center target - expect not shown
cut.locked = true;
run(25);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
fireEvent.dragEnd(element);
});
test('that should show drop target if dropping on self', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
test('that should not show drop target if dropping on self', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
id: 'testcomponentid',
options: {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const container = document.createElement('div');
const cut = new DockviewGroupPanelModel(
container,
accessor,
'groupviewid',
{},
new groupPanelMock() as DockviewGroupPanel
);
let counter = 0;
cut.onUnhandledDragOverEvent(() => {
counter++;
});
cut.openPanel(new TestPanel('panel1', panelApi));
const element = container
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
expect(counter).toBe(0);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that should allow drop when dropping on self for same component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const accessor = new accessorMock() as DockviewComponent;
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -908,23 +698,16 @@ describe('dockviewGroupPanelModel', () => {
new groupPanelMock() as DockviewGroupPanel
);
let counter = 0;
cut.onUnhandledDragOverEvent(() => {
counter++;
});
cut.openPanel(new TestPanel('panel1', panelApi));
cut.openPanel(new TestPanel('panel2', panelApi));
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
const element = container
.getElementsByClassName('dv-content-container')
.item(0) as HTMLElement;
.getElementsByClassName('content-container')
.item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -934,28 +717,94 @@ describe('dockviewGroupPanelModel', () => {
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
expect(counter).toBe(0);
expect(accessor.options.showDndOverlay).toBeCalledTimes(0);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that should not allow drop when dropping on self for same component id', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
options: {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
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 container = document.createElement('div');
const cut = new DockviewGroupPanelModel(
container,
accessor,
'groupviewid',
{},
new groupPanelMock() as DockviewGroupPanel
);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const element = container
.getElementsByClassName('content-container')
.item(0)!;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
expect(accessor.options.showDndOverlay).toBeCalledTimes(0);
expect(
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that should not allow drop when not dropping for different component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
options: {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
@ -982,23 +831,17 @@ describe('dockviewGroupPanelModel', () => {
new groupPanelMock() as DockviewGroupPanel
);
let counter = 0;
cut.onUnhandledDragOverEvent(() => {
counter++;
});
cut.openPanel(new TestPanel('panel1', panelApi));
cut.openPanel(new TestPanel('panel2', panelApi));
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const element = container
.getElementsByClassName('dv-content-container')
.item(0) as HTMLElement;
.getElementsByClassName('content-container')
.item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')],
@ -1008,10 +851,10 @@ describe('dockviewGroupPanelModel', () => {
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
expect(counter).toBe(1);
expect(accessor.options.showDndOverlay).toBeCalledTimes(1);
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
@ -1090,17 +933,17 @@ describe('dockviewGroupPanelModel', () => {
container.getElementsByClassName('watermark-test-container').length
).toBe(1);
cut.openPanel(new TestPanel('panel1', panelApi));
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
expect(
container.getElementsByClassName('watermark-test-container').length
).toBe(0);
expect(
container.getElementsByClassName('dv-tabs-and-actions-container')
container.getElementsByClassName('tabs-and-actions-container')
.length
).toBe(1);
cut.openPanel(new TestPanel('panel2', panelApi));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
expect(
container.getElementsByClassName('watermark-test-container').length
@ -1118,7 +961,7 @@ describe('dockviewGroupPanelModel', () => {
container.getElementsByClassName('watermark-test-container').length
).toBe(1);
cut.openPanel(new TestPanel('panel1', panelApi));
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
expect(
container.getElementsByClassName('watermark-test-container').length

View File

@ -3,37 +3,33 @@ 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(),
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
onDidActiveChange: jest.fn(),
},
} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
let latestTitle: string | undefined = undefined;
@ -55,32 +51,30 @@ describe('dockviewPanel', () => {
});
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(),
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
onDidActiveChange: jest.fn(),
},
} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
cut.init({ title: 'myTitle', params: {} });
expect(cut.title).toBe('myTitle');
@ -93,39 +87,29 @@ describe('dockviewPanel', () => {
});
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 dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
cut.init({ params: {}, title: 'title' });
@ -135,33 +119,29 @@ describe('dockviewPanel', () => {
});
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 dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
expect(cut.params).toEqual(undefined);
@ -171,72 +151,64 @@ describe('dockviewPanel', () => {
});
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 dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {
api: {
setSize: jest.fn(),
},
} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
cut.api.setSize({ height: 123, width: 456 });
expect(group.api.setSize).toHaveBeenCalledWith({
height: 123,
width: 456,
});
expect(group.api.setSize).toHaveBeenCalledTimes(1);
expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledTimes(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 dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const cut = new DockviewPanel(
'fake-id',
'fake-component',
undefined,
accessor,
api,
group,
model,
{
renderer: 'onlyWhenVisible',
}
);
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
cut.init({ params: { a: '1', b: '2' }, title: 'A title' });
expect(cut.params).toEqual({ a: '1', b: '2' });
@ -244,9 +216,6 @@ describe('dockviewPanel', () => {
// 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({
@ -257,9 +226,6 @@ describe('dockviewPanel', () => {
e: '5',
f: '6',
});
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '4', e: '5', f: '6' },
});
cut.update({
params: {
@ -280,8 +246,5 @@ describe('dockviewPanel', () => {
g: '',
h: null,
});
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '', e: null, g: '', h: null },
});
});
});

View File

@ -1,13 +1,16 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import {
DockviewComponent,
IDockviewComponent,
} from '../../dockview/dockviewComponent';
import { DockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
import { GroupPanelFrameworkComponentFactory } from '../../dockview/options';
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;
let accessorMock: jest.Mock<IDockviewComponent>;
beforeEach(() => {
contentMock = jest.fn<IContentRenderer, []>(() => {
@ -15,6 +18,8 @@ describe('dockviewGroupPanel', () => {
element: document.createElement('div'),
dispose: jest.fn(),
update: jest.fn(),
onGroupChange: jest.fn(),
onPanelVisibleChange: jest.fn(),
};
return partial as IContentRenderer;
});
@ -24,35 +29,31 @@ describe('dockviewGroupPanel', () => {
element: document.createElement('div'),
dispose: jest.fn(),
update: jest.fn(),
onGroupChange: jest.fn(),
onPanelVisibleChange: 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`);
}
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: contentMock,
},
tabComponents: {
tabComponent: tabMock,
},
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return new tabMock(options.id, options.name);
default:
throw new Error(`unsupported`);
}
},
},
};
return partial as DockviewComponent;
});
});
test('that dispose is called on content and tab renderers when present', () => {
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent',
'tabComponent'
@ -66,7 +67,7 @@ describe('dockviewGroupPanel', () => {
test('that update is called on content and tab renderers when present', () => {
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent',
'tabComponent'
@ -80,30 +81,72 @@ describe('dockviewGroupPanel', () => {
expect(cut.tab.update).toHaveBeenCalled();
});
test('that events are fired', () => {
const cut = new DockviewPanelModel(
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent',
'tabComponent'
);
const group1 = jest.fn() as any;
const group2 = jest.fn() as any;
cut.updateParentGroup(group1, false);
expect(cut.content.onGroupChange).toHaveBeenNthCalledWith(1, group1);
expect(cut.tab.onGroupChange).toHaveBeenNthCalledWith(1, group1);
expect(cut.content.onPanelVisibleChange).toHaveBeenNthCalledWith(
1,
false
);
expect(cut.tab.onPanelVisibleChange).toHaveBeenNthCalledWith(1, false);
expect(cut.content.onGroupChange).toHaveBeenCalledTimes(1);
expect(cut.tab.onGroupChange).toHaveBeenCalledTimes(1);
expect(cut.content.onPanelVisibleChange).toHaveBeenCalledTimes(1);
expect(cut.tab.onPanelVisibleChange).toHaveBeenCalledTimes(1);
cut.updateParentGroup(group1, true);
expect(cut.content.onPanelVisibleChange).toHaveBeenNthCalledWith(
2,
true
);
expect(cut.tab.onPanelVisibleChange).toHaveBeenNthCalledWith(2, true);
expect(cut.content.onGroupChange).toHaveBeenCalledTimes(1);
expect(cut.tab.onGroupChange).toHaveBeenCalledTimes(1);
expect(cut.content.onPanelVisibleChange).toHaveBeenCalledTimes(2);
expect(cut.tab.onPanelVisibleChange).toHaveBeenCalledTimes(2);
cut.updateParentGroup(group2, true);
expect(cut.content.onGroupChange).toHaveBeenNthCalledWith(2, group2);
expect(cut.tab.onGroupChange).toHaveBeenNthCalledWith(2, group2);
expect(cut.content.onGroupChange).toHaveBeenCalledTimes(2);
expect(cut.tab.onGroupChange).toHaveBeenCalledTimes(2);
expect(cut.content.onPanelVisibleChange).toHaveBeenCalledTimes(2);
expect(cut.tab.onPanelVisibleChange).toHaveBeenCalledTimes(2);
});
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`);
}
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: contentMock,
},
tabComponents: {
tabComponent: jest
.fn()
.mockImplementation(() => tabMock),
},
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent',
'tabComponent'
@ -113,30 +156,26 @@ describe('dockviewGroupPanel', () => {
});
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`);
}
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: contentMock,
},
tabComponents: {
tabComponent: jest
.fn()
.mockImplementation(() => tabMock),
},
defaultTabComponent: 'tabComponent',
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent'
);
@ -144,22 +183,56 @@ describe('dockviewGroupPanel', () => {
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`);
}
test('that the framework tab is created when provided tab is a framework tab', () => {
const tab = jest.fn();
const tabFactory = jest.fn().mockImplementation(() => tab);
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: contentMock,
},
frameworkTabComponents: {
tabComponent: tabMock,
},
frameworkComponentFactory: (<
Partial<GroupPanelFrameworkComponentFactory>
>{
tab: { createComponent: tabFactory },
}) as GroupPanelFrameworkComponentFactory,
},
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent',
'tabComponent'
);
expect(tabFactory).toHaveBeenCalledWith('id', 'tabComponent', tabMock);
expect(cut.tab).toEqual(tab);
});
test('that is library default tab instance is created when no alternative exists', () => {
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: contentMock,
},
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent'
);
@ -168,33 +241,61 @@ describe('dockviewGroupPanel', () => {
});
test('that the default content is created', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
createComponent(options) {
switch (options.name) {
case 'contentComponent':
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
components: {
contentComponent: jest.fn().mockImplementation(() => {
return contentMock;
default:
throw new Error(`unsupported`);
}
}),
},
},
createTabComponent(options) {
switch (options.name) {
case 'tabComponent':
return tabMock;
default:
throw new Error(`unsupported`);
}
},
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
accessorMock,
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent'
);
expect(cut.content).toEqual(contentMock);
});
test('that the framework content is created', () => {
const content = jest.fn();
const contentFactory = jest.fn().mockImplementation(() => content);
accessorMock = jest.fn<DockviewComponent, []>(() => {
const partial: Partial<DockviewComponent> = {
options: {
frameworkComponents: {
contentComponent: contentMock,
},
frameworkComponentFactory: (<
Partial<GroupPanelFrameworkComponentFactory>
>{
content: { createComponent: contentFactory },
}) as GroupPanelFrameworkComponentFactory,
},
};
return partial as DockviewComponent;
});
const cut = new DockviewPanelModel(
<IDockviewComponent>new accessorMock(),
'id',
'contentComponent'
);
expect(contentFactory).toHaveBeenCalledWith(
'id',
'contentComponent',
contentMock
);
expect(cut.content).toEqual(content);
});
});

View File

@ -1,9 +1,4 @@
import {
disableIframePointEvents,
isInDocument,
quasiDefaultPrevented,
quasiPreventDefault,
} from '../dom';
import { quasiDefaultPrevented, quasiPreventDefault } from '../dom';
describe('dom', () => {
test('quasiPreventDefault', () => {
@ -23,61 +18,4 @@ describe('dom', () => {
(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,8 +1,8 @@
import {
AsapEvent,
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
describe('events', () => {
@ -82,41 +82,6 @@ describe('events', () => {
});
});
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>();
@ -142,7 +107,7 @@ describe('events', () => {
expect(value).toBe(3);
});
it('addDisposableListener with capture options', () => {
it('addDisposableWindowListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
@ -150,16 +115,16 @@ describe('events', () => {
const handler = jest.fn();
const disposable = addDisposableListener(
const disposable = addDisposableWindowListener(
element as any,
'pointerdown',
'mousedown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
'mousedown',
handler,
true
);
@ -170,13 +135,13 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
'mousedown',
handler,
true
);
});
it('addDisposableListener without capture options', () => {
it('addDisposableWindowListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
@ -184,15 +149,15 @@ describe('events', () => {
const handler = jest.fn();
const disposable = addDisposableListener(
const disposable = addDisposableWindowListener(
element as any,
'pointerdown',
'mousedown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
'mousedown',
handler,
undefined
);
@ -203,7 +168,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
'mousedown',
handler,
undefined
);
@ -219,14 +184,14 @@ describe('events', () => {
const disposable = addDisposableListener(
element as any,
'pointerdown',
'mousedown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
'mousedown',
handler,
true
);
@ -237,7 +202,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
'mousedown',
handler,
true
);
@ -253,13 +218,13 @@ describe('events', () => {
const disposable = addDisposableListener(
element as any,
'pointerdown',
'mousedown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
'mousedown',
handler,
undefined
);
@ -270,7 +235,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
'mousedown',
handler,
undefined
);

View File

@ -17,9 +17,13 @@ class TestPanel implements IGridPanelView {
_onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event;
isVisible: boolean = true;
isActive: boolean = true;
params: Parameters = {};
get isActive(): boolean {
return true;
}
get params(): Record<string, any> {
return {};
}
constructor(
public readonly id: string,
@ -66,10 +70,8 @@ class TestPanel implements IGridPanelView {
}
class ClassUnderTest extends BaseGrid<TestPanel> {
readonly gridview = this.gridview;
constructor(parentElement: HTMLElement, options: BaseGridOptions) {
super(parentElement, options);
constructor(options: BaseGridOptions) {
super(options);
}
doRemoveGroup(
@ -105,62 +107,24 @@ class ClassUnderTest extends BaseGrid<TestPanel> {
}
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'), {
const cut = new ClassUnderTest({
parentElement: document.createElement('div'),
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
const events: { type: string; panel: TestPanel | undefined }[] = [];
const events: (TestPanel | undefined)[] = [];
const disposable = new CompositeDisposable(
cut.onDidAdd((event) => {
events.push({ type: 'add', panel: event });
cut.onDidAddGroup((event) => {
events.push(event);
}),
cut.onDidRemove((event) => {
events.push({ type: 'remove', panel: event });
cut.onDidRemoveGroup((event) => {
events.push(event);
}),
cut.onDidActiveChange((event) => {
events.push({ type: 'active', panel: event });
cut.onDidActiveGroupChange((event) => {
events.push(event);
})
);
@ -177,8 +141,9 @@ describe('baseComponentGridview', () => {
cut.doAddGroup(panel1);
expect(events.length).toBe(1);
expect(events[0]).toEqual({ type: 'add', panel: panel1 });
expect(events.length).toBe(2);
expect(events[0]).toBe(panel1);
expect(events[1]).toBe(panel1);
const panel2 = new TestPanel(
'id',
@ -193,12 +158,12 @@ describe('baseComponentGridview', () => {
cut.doAddGroup(panel2);
expect(events.length).toBe(2);
expect(events[1]).toEqual({ type: 'add', panel: panel2 });
expect(events.length).toBe(4);
expect(events[2]).toBe(panel2);
cut.doRemoveGroup(panel1);
expect(events.length).toBe(3);
expect(events[2]).toEqual({ type: 'remove', panel: panel1 });
expect(events.length).toBe(5);
expect(events[4]).toBe(panel1);
disposable.dispose();
cut.dispose();

View File

@ -4,8 +4,6 @@ import {
Gridview,
IGridView,
IViewSize,
SerializedGridview,
getGridLocation,
orthogonal,
} from '../../gridview/gridview';
import { Orientation, Sizing } from '../../splitview/splitview';
@ -19,24 +17,16 @@ class MockGridview implements IGridView {
IViewSize | undefined
>().event;
element: HTMLElement = document.createElement('div');
isVisible: boolean = true;
width: number = 0;
height: number = 0;
constructor(private id?: string) {
constructor() {
this.element.className = 'mock-grid-view';
this.element.id = `${id ?? ''}`;
}
layout(width: number, height: number): void {
this.width = width;
this.height = height;
//
}
toJSON(): object {
if (this.id) {
return { id: this.id };
}
return {};
}
}
@ -733,475 +723,4 @@ describe('gridview', () => {
width: 1000,
});
});
test('re-structuring deep gridivew where a branchnode becomes of length one and is coverted to a leaf node', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 0]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
let el = gridview.element.querySelectorAll('.mock-grid-view');
expect(el.length).toBe(6);
gridview.remove(view2);
el = gridview.element.querySelectorAll('.mock-grid-view');
expect(el.length).toBe(5);
});
test('gridview nested proportional layouts', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
const views = [view1, view2, view3, view4, view5, view6];
const dimensions = [
{ width: 500, height: 1000 },
{ width: 500, height: 500 },
{ width: 250, height: 500 },
{ width: 250, height: 250 },
{ width: 125, height: 250 },
{ width: 125, height: 250 },
];
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(dimensions);
gridview.layout(2000, 1500);
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(
dimensions.map(({ width, height }) => ({
width: width * 2,
height: height * 1.5,
}))
);
gridview.layout(200, 2000);
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(
dimensions.map(({ width, height }) => ({
width: width * 0.2,
height: height * 2,
}))
);
});
test('that maximizeView retains original dimensions when restored', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
let counter = 0;
const subscription = gridview.onDidMaximizedNodeChange(() => {
counter++;
});
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
/**
* _____________________________________________
* | | |
* | | 2 |
* | | |
* | 1 |_______________________|
* | | | 4 |
* | | 3 |_____________|
* | | | 5 | 6 |
* |_____________________|_________|______|______|
*/
const views = [view1, view2, view3, view4, view5, view6];
const dimensions = [
{ width: 500, height: 1000 },
{ width: 500, height: 500 },
{ width: 250, height: 500 },
{ width: 250, height: 250 },
{ width: 125, height: 250 },
{ width: 125, height: 250 },
];
function assertLayout() {
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(dimensions);
}
// base case assertions
assertLayout();
expect(gridview.hasMaximizedView()).toBeFalsy();
expect(counter).toBe(0);
/**
* maximize each view individually and then return to the standard view
* checking on each iteration that the original layout dimensions
* are restored
*/
for (let i = 0; i < views.length; i++) {
const view = views[i];
gridview.maximizeView(view);
expect(counter).toBe(i * 2 + 1);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.exitMaximizedView();
expect(counter).toBe(i * 2 + 2);
assertLayout();
}
subscription.dispose();
});
test('that maximizedView is exited when a views visibility is changed', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.setViewVisible([0], true);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is moved', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.moveView([1, 1], 0, 1);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is added', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is removed', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.removeView([1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is cleared when layout is cleared', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.clear();
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is cleared when layout is disposed', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.dispose();
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is cleared when layout is reset', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.deserialize(
{
height: 1000,
width: 1000,
root: {
type: 'leaf',
data: [],
},
orientation: Orientation.HORIZONTAL,
},
{
fromJSON: (data) => {
return new MockGridview('');
},
}
);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('visibility check', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
/**
* _____________________________________________
* | | |
* | | 2 |
* | | |
* | 1 |_______________________|
* | | | 4 |
* | | 3 |_____________|
* | | | 5 | 6 |
* |_____________________|_________|______|______|
*/
function assertVisibility(visibility: boolean[]) {
expect(gridview.isViewVisible(getGridLocation(view1.element))).toBe(
visibility[0]
);
expect(gridview.isViewVisible(getGridLocation(view2.element))).toBe(
visibility[1]
);
expect(gridview.isViewVisible(getGridLocation(view3.element))).toBe(
visibility[2]
);
expect(gridview.isViewVisible(getGridLocation(view4.element))).toBe(
visibility[3]
);
expect(gridview.isViewVisible(getGridLocation(view5.element))).toBe(
visibility[4]
);
expect(gridview.isViewVisible(getGridLocation(view6.element))).toBe(
visibility[5]
);
}
// hide each view one by one
assertVisibility([true, true, true, true, true, true]);
gridview.setViewVisible(getGridLocation(view5.element), false);
assertVisibility([true, true, true, true, false, true]);
gridview.setViewVisible(getGridLocation(view4.element), false);
assertVisibility([true, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view1.element), false);
assertVisibility([false, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view2.element), false);
assertVisibility([false, false, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view3.element), false);
assertVisibility([false, false, false, false, false, true]);
gridview.setViewVisible(getGridLocation(view6.element), false);
assertVisibility([false, false, false, false, false, false]);
// un-hide each view one by one
gridview.setViewVisible(getGridLocation(view1.element), true);
assertVisibility([true, false, false, false, false, false]);
gridview.setViewVisible(getGridLocation(view5.element), true);
assertVisibility([true, false, false, false, true, false]);
gridview.setViewVisible(getGridLocation(view6.element), true);
assertVisibility([true, false, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view2.element), true);
assertVisibility([true, true, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view3.element), true);
assertVisibility([true, true, true, false, true, true]);
gridview.setViewVisible(getGridLocation(view4.element), true);
assertVisibility([true, true, true, true, true, true]);
});
});

View File

@ -32,40 +32,12 @@ describe('gridview', () => {
container = document.createElement('div');
});
test('update className', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
className: 'test-a test-b',
});
expect(gridview.element.className).toBe('test-a test-b');
gridview.updateOptions({ className: 'test-b test-c' });
expect(gridview.element.className).toBe('test-b test-c');
});
test('added views are visible by default', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -81,17 +53,11 @@ describe('gridview', () => {
});
test('remove panel', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -118,17 +84,11 @@ describe('gridview', () => {
});
test('active panel', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -185,21 +145,13 @@ describe('gridview', () => {
});
test('deserialize and serialize a layout', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
@ -244,9 +196,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400, true);
const panel1 = gridview.getPanel('panel_1')!;
@ -319,22 +268,16 @@ describe('gridview', () => {
],
},
},
activePanel: 'panel_2',
activePanel: 'panel_1',
});
});
test('toJSON shouldnt fire any layout events', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(1000, 1000);
@ -367,17 +310,11 @@ describe('gridview', () => {
});
test('gridview events', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -497,17 +434,11 @@ describe('gridview', () => {
test('dispose of gridviewComponent', () => {
expect(container.childNodes.length).toBe(0);
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -529,21 +460,15 @@ describe('gridview', () => {
gridview.dispose();
expect(container.children.length).toBe(0);
expect(container.childNodes.length).toBe(0);
});
test('#1/VERTICAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -598,17 +523,11 @@ describe('gridview', () => {
});
test('#2/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -663,17 +582,11 @@ describe('gridview', () => {
});
test('#3/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -746,17 +659,11 @@ describe('gridview', () => {
});
test('#4/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -847,17 +754,11 @@ describe('gridview', () => {
});
test('#5/VERTICAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -948,17 +849,11 @@ describe('gridview', () => {
});
test('#5/VERTICAL/proportional/false', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1049,17 +944,11 @@ describe('gridview', () => {
});
test('#6/VERTICAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1180,17 +1069,11 @@ describe('gridview', () => {
});
test('#7/VERTICAL layout first', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1311,17 +1194,11 @@ describe('gridview', () => {
});
test('#8/VERTICAL layout after', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1444,17 +1321,11 @@ describe('gridview', () => {
});
test('#9/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1575,17 +1446,11 @@ describe('gridview', () => {
});
test('#9/HORIZONTAL/proportional/false', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(800, 400);
@ -1706,17 +1571,11 @@ describe('gridview', () => {
});
test('#10/HORIZONTAL scale x:1.5 y:2', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.fromJSON({
@ -1840,17 +1699,11 @@ describe('gridview', () => {
});
test('panel is disposed of when component is disposed', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(1000, 1000);
@ -1877,17 +1730,11 @@ describe('gridview', () => {
});
test('panel is disposed of when removed', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(1000, 1000);
@ -1913,17 +1760,11 @@ describe('gridview', () => {
});
test('panel is disposed of when fromJSON is called', () => {
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(1000, 1000);
@ -1958,17 +1799,11 @@ describe('gridview', () => {
test('fromJSON events should still fire', () => {
jest.useFakeTimers();
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
let addGroup: GridviewPanel[] = [];
@ -2087,17 +1922,11 @@ describe('gridview', () => {
test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(1600, 800);
@ -2220,17 +2049,11 @@ describe('gridview', () => {
test('that a deep HORIZONTAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(6000, 5000);
@ -2502,17 +2325,11 @@ describe('gridview', () => {
test('that a deep VERTICAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent(container, {
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
components: { default: TestGridview },
});
gridview.layout(5000, 6000);
@ -2780,160 +2597,4 @@ describe('gridview', () => {
activePanel: 'panel_1',
});
});
test('that loading a corrupt layout throws an error and leaves a clean gridview behind', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error(`unsupported panel '${options.name}'`);
}
},
});
let el = gridview.element.querySelector('.dv-view-container');
expect(el).toBeTruthy();
expect(el!.childNodes.length).toBe(0);
expect(() => {
gridview.fromJSON({
grid: {
height: 400,
width: 800,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 200,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 250,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 150,
data: {
id: 'panel_1',
component: 'somethingBad',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 200,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
}).toThrow("unsupported panel 'somethingBad'");
expect(gridview.groups.length).toBe(0);
el = gridview.element.querySelector('.dv-view-container');
expect(el).toBeTruthy();
expect(el!.childNodes.length).toBe(0);
});
test('that disableAutoResizing is false by default', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(gridview.disableResizing).toBeFalsy();
});
test('that disableAutoResizing can be enabled', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
expect(gridview.disableResizing).toBeTruthy();
});
test('that setVisible toggles visiblity', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
gridview.layout(1000, 1000);
const panel1 = gridview.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = gridview.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();
});
});

View File

@ -8,7 +8,6 @@ describe('gridviewPanel', () => {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
} as any;
});

View File

@ -1,8 +1,4 @@
import {
CompositeDisposable,
Disposable,
MutableDisposable,
} from '../lifecycle';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
describe('lifecycle', () => {
test('mutable disposable', () => {
@ -68,16 +64,4 @@ describe('lifecycle', () => {
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

@ -8,8 +8,10 @@ describe('math', () => {
expect(clamp(55, 40, 50)).toBe(50);
});
it('if min > max return min', () => {
expect(clamp(55, 50, 40)).toBe(50);
it('should throw an error if min > max', () => {
expect(() => clamp(55, 50, 40)).toThrow(
'50 > 40 is an invalid condition'
);
});
});

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

@ -0,0 +1,102 @@
import { createComponent } from '../../panel/componentFactory';
describe('componentFactory', () => {
describe('createComponent', () => {
test('valid component and framework component', () => {
const mock = jest.fn();
const mock2 = jest.fn();
expect(() =>
createComponent(
'id-1',
'component-1',
{ 'component-1': mock },
{ 'component-1': mock2 }
)
).toThrow(
"Cannot create 'id-1'. component 'component-1' registered as both a component and frameworkComponent"
);
});
test('valid framework component but no factory', () => {
const mock = jest.fn();
expect(() =>
createComponent(
'id-1',
'component-1',
{},
{ 'component-1': mock }
)
).toThrow(
"Cannot create 'id-1' for framework component 'component-1'. you must register a frameworkPanelWrapper to use framework components"
);
});
test('valid framework component', () => {
const component = jest.fn();
const createComponentFn = jest
.fn()
.mockImplementation(() => component);
const frameworkComponent = jest.fn();
expect(
createComponent(
'id-1',
'component-1',
{},
{ 'component-1': frameworkComponent },
{
createComponent: createComponentFn,
}
)
).toBe(component);
expect(createComponentFn).toHaveBeenCalledWith(
'id-1',
'component-1',
frameworkComponent
);
});
test('no valid component with fallback', () => {
const mock = jest.fn();
expect(
createComponent(
'id-1',
'component-1',
{},
{},
{
createComponent: () => null,
},
() => mock
)
).toBe(mock);
});
test('no valid component', () => {
expect(() =>
createComponent('id-1', 'component-1', {}, {})
).toThrow(
"Cannot create 'id-1', no component 'component-1' provided"
);
});
test('valid component', () => {
const component = jest.fn();
const componentResult = createComponent(
'id-1',
'component-1',
{ 'component-1': component },
{}
);
expect(component).toHaveBeenCalled();
expect(componentResult instanceof component).toBeTruthy();
});
});
});

View File

@ -1,10 +1,14 @@
import { CompositeDisposable } from '../../lifecycle';
import { Paneview } from '../../paneview/paneview';
import { IPanePart, PaneviewPanel } from '../../paneview/paneviewPanel';
import {
IPaneBodyPart,
IPaneHeaderPart,
PaneviewPanel,
} from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel {
protected getBodyComponent(): IPanePart {
protected getBodyComponent(): IPaneBodyPart {
return {
element: document.createElement('div'),
update: () => {
@ -19,7 +23,7 @@ class TestPanel extends PaneviewPanel {
};
}
protected getHeaderComponent(): IPanePart {
protected getHeaderComponent(): IPaneHeaderPart {
return {
element: document.createElement('div'),
update: () => {
@ -56,28 +60,22 @@ describe('paneview', () => {
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,
});
const view1 = new TestPanel(
'id',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view2 = new TestPanel(
'id2',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
expect(added.length).toBe(0);
expect(removed.length).toBe(0);
@ -112,28 +110,22 @@ describe('paneview', () => {
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,
});
const view1 = new TestPanel(
'id',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view2 = new TestPanel(
'id2',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
paneview.addPane(view1);
paneview.addPane(view2);

View File

@ -4,28 +4,19 @@ import { PanelUpdateEvent } from '../../panel/types';
import { PaneviewComponent } from '../../paneview/paneviewComponent';
import {
PaneviewPanel,
IPanePart,
IPaneBodyPart,
IPaneHeaderPart,
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,
});
super(id, component, 'header', Orientation.VERTICAL, false, true);
}
getHeaderComponent() {
return new (class Header implements IPanePart {
return new (class Header implements IPaneHeaderPart {
private _element: HTMLElement = document.createElement('div');
get element() {
@ -47,7 +38,7 @@ class TestPanel extends PaneviewPanel {
}
getBodyComponent() {
return new (class Header implements IPanePart {
return new (class Header implements IPaneBodyPart {
private _element: HTMLElement = document.createElement('div');
get element() {
@ -69,7 +60,7 @@ class TestPanel extends PaneviewPanel {
}
}
describe('paneviewComponent', () => {
describe('componentPaneview', () => {
let container: HTMLElement;
beforeEach(() => {
@ -77,52 +68,26 @@ describe('paneviewComponent', () => {
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(300, 200);
paneview.layout(600, 400);
paneview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel2',
});
@ -143,8 +108,6 @@ describe('paneviewComponent', () => {
})
);
paneview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 600, height: 22 });
expect(panel2Dimensions).toEqual({ width: 600, height: 22 });
@ -179,19 +142,13 @@ describe('paneviewComponent', () => {
});
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.fromJSON({
size: 6,
views: [
@ -199,7 +156,7 @@ describe('paneviewComponent', () => {
size: 1,
data: {
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
@ -208,7 +165,7 @@ describe('paneviewComponent', () => {
size: 2,
data: {
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
},
expanded: false,
@ -217,15 +174,13 @@ describe('paneviewComponent', () => {
size: 3,
data: {
id: 'panel3',
component: 'default',
component: 'testPanel',
title: 'Panel 3',
},
},
],
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.layout(400, 800);
const panel1 = paneview.getPanel('panel1');
@ -265,57 +220,53 @@ describe('paneviewComponent', () => {
size: 756,
data: {
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
headerSize: 22,
minimumSize: 100,
},
{
size: 22,
data: {
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
},
expanded: false,
headerSize: 22,
minimumSize: 100,
},
{
size: 22,
data: {
id: 'panel3',
component: 'default',
component: 'testPanel',
title: 'Panel 3',
},
expanded: false,
headerSize: 22,
minimumSize: 100,
},
],
});
});
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
});
@ -329,15 +280,13 @@ describe('paneviewComponent', () => {
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');
}
test('dispose of paneviewComponent', () => {
expect(container.childNodes.length).toBe(0);
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
@ -345,12 +294,40 @@ describe('paneviewComponent', () => {
paneview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
});
expect(container.childNodes.length).toBeGreaterThan(0);
paneview.dispose();
expect(container.childNodes.length).toBe(0);
});
test('panel is disposed of when component is disposed', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
title: 'Panel 2',
});
@ -367,14 +344,10 @@ describe('paneviewComponent', () => {
});
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
@ -382,12 +355,12 @@ describe('paneviewComponent', () => {
paneview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
});
@ -404,14 +377,10 @@ describe('paneviewComponent', () => {
});
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
@ -419,12 +388,12 @@ describe('paneviewComponent', () => {
paneview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
});
@ -441,14 +410,10 @@ describe('paneviewComponent', () => {
});
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');
}
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
@ -461,17 +426,16 @@ describe('paneviewComponent', () => {
size: 1,
data: {
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
},
minimumSize: 100,
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
@ -480,7 +444,7 @@ describe('paneviewComponent', () => {
size: 3,
data: {
id: 'panel3',
component: 'default',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
@ -496,124 +460,33 @@ describe('paneviewComponent', () => {
size: 122,
data: {
id: 'panel1',
component: 'default',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
size: 122,
data: {
id: 'panel2',
component: 'default',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
headerSize: 22,
minimumSize: 100,
},
{
size: 456,
size: 356,
data: {
id: 'panel3',
component: 'default',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
headerSize: 22,
minimumSize: 100,
},
],
});
});
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

@ -96,7 +96,7 @@ describe('splitview', () => {
expect(splitview.orientation).toBe(Orientation.HORIZONTAL);
const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-horizontal'
'.split-view-container horizontal'
);
expect(viewQuery).toBeTruthy();
@ -111,7 +111,7 @@ describe('splitview', () => {
expect(splitview.orientation).toBe(Orientation.VERTICAL);
const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-vertical'
'.split-view-container vertical'
);
expect(viewQuery).toBeTruthy();
@ -128,48 +128,48 @@ describe('splitview', () => {
splitview.addView(new Testview(50, 50));
let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
'.split-view-container > .view-container > .view'
);
expect(viewQuery.length).toBe(3);
let sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
'.split-view-container > .sash-container > .sash'
);
expect(sashQuery.length).toBe(2);
splitview.removeView(2);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
'.split-view-container > .view-container > .view'
);
expect(viewQuery.length).toBe(2);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
'.split-view-container > .sash-container > .sash'
);
expect(sashQuery.length).toBe(1);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
'.split-view-container > .view-container > .view'
);
expect(viewQuery.length).toBe(1);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
'.split-view-container > .sash-container > .sash'
);
expect(sashQuery.length).toBe(0);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
'.split-view-container > .view-container > .view'
);
expect(viewQuery.length).toBe(0);
sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
'.split-view-container > .sash-container > .sash'
);
expect(sashQuery.length).toBe(0);
@ -188,14 +188,14 @@ describe('splitview', () => {
splitview.addView(view2);
let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
'.split-view-container > .view-container > .view.visible'
);
expect(viewQuery.length).toBe(2);
splitview.setViewVisible(1, false);
viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
'.split-view-container > .view-container > .view.visible'
);
expect(viewQuery.length).toBe(1);
@ -619,7 +619,7 @@ describe('splitview', () => {
);
const sashElement = container
.getElementsByClassName('dv-sash')
.getElementsByClassName('sash')
.item(0) as HTMLElement;
// validate the expected state before drag
@ -676,226 +676,4 @@ describe('splitview', () => {
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

@ -26,52 +26,25 @@ describe('componentSplitview', () => {
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, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
const panel1 = splitview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
});
const panel2 = splitview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
});
splitview.movePanel(0, 1);
@ -93,22 +66,18 @@ describe('componentSplitview', () => {
});
test('remove panel', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
@ -133,15 +102,11 @@ describe('componentSplitview', () => {
});
test('horizontal dimensions', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
@ -151,15 +116,11 @@ describe('componentSplitview', () => {
});
test('vertical dimensions', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
@ -169,22 +130,18 @@ describe('componentSplitview', () => {
});
test('api resize', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(400, 600);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
@ -226,20 +183,16 @@ describe('componentSplitview', () => {
});
test('api', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1');
@ -250,7 +203,7 @@ describe('componentSplitview', () => {
// expect(panel1?.api.isFocused).toBeFalsy();
expect(panel1!.api.isVisible).toBeTruthy();
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel2 = splitview.getPanel('panel2');
@ -272,22 +225,18 @@ describe('componentSplitview', () => {
test('vertical panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(300, 200);
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -306,8 +255,6 @@ describe('componentSplitview', () => {
})
);
splitview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 600, height: 200 });
expect(panel2Dimensions).toEqual({ width: 600, height: 200 });
@ -328,22 +275,18 @@ describe('componentSplitview', () => {
test('horizontal panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(300, 200);
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -362,8 +305,6 @@ describe('componentSplitview', () => {
})
);
splitview.layout(600, 400);
expect(panel1Dimensions).toEqual({ width: 300, height: 400 });
expect(panel2Dimensions).toEqual({ width: 300, height: 400 });
@ -382,63 +323,51 @@ describe('componentSplitview', () => {
});
test('serialization', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(400, 6);
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'default' },
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'default' } },
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
],
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' },
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{
size: 3,
data: { id: 'panel3', component: 'default' },
data: { id: 'panel3', component: 'testPanel' },
snap: false,
},
],
@ -449,15 +378,11 @@ describe('componentSplitview', () => {
});
test('toJSON shouldnt fire any layout events', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
@ -465,11 +390,11 @@ describe('componentSplitview', () => {
splitview.addPanel({
id: 'panel1',
component: 'default',
component: 'testPanel',
});
splitview.addPanel({
id: 'panel2',
component: 'default',
component: 'testPanel',
});
const disposable = splitview.onDidLayoutChange(() => {
@ -482,16 +407,41 @@ describe('componentSplitview', () => {
disposable.dispose();
});
test('panel is disposed of when component is disposed', () => {
const splitview = new SplitviewComponent(container, {
test('dispose of splitviewComponent', () => {
expect(container.childNodes.length).toBe(0);
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(1000, 1000);
splitview.addPanel({
id: 'panel1',
component: 'testPanel',
});
splitview.addPanel({
id: 'panel2',
component: 'testPanel',
});
expect(container.childNodes.length).toBeGreaterThan(0);
splitview.dispose();
expect(container.childNodes.length).toBe(0);
});
test('panel is disposed of when component is disposed', () => {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
},
});
@ -519,15 +469,11 @@ describe('componentSplitview', () => {
});
test('panel is disposed of when removed', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
default: TestPanel,
},
});
@ -555,15 +501,11 @@ describe('componentSplitview', () => {
});
test('panel is disposed of when fromJSON is called', () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
default: TestPanel,
},
});
@ -595,15 +537,11 @@ describe('componentSplitview', () => {
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent(container, {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
components: {
testPanel: TestPanel,
},
});
splitview.layout(400, 600);
@ -612,15 +550,15 @@ describe('componentSplitview', () => {
views: [
{
size: 1,
data: { id: 'panel1', component: 'default' },
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'default' },
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'default' } },
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
],
size: 6,
orientation: Orientation.VERTICAL,
@ -631,17 +569,17 @@ describe('componentSplitview', () => {
views: [
{
size: 100,
data: { id: 'panel1', component: 'default' },
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 200,
data: { id: 'panel2', component: 'default' },
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{
size: 300,
data: { id: 'panel3', component: 'default' },
data: { id: 'panel3', component: 'testPanel' },
snap: false,
},
],
@ -650,94 +588,4 @@ describe('componentSplitview', () => {
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,20 +1,13 @@
import {
DockviewMaximizedGroupChanged,
FloatingGroupOptions,
DockviewDropEvent,
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,
@ -33,6 +26,7 @@ import {
AddSplitviewComponentOptions,
ISplitviewComponent,
SerializedSplitview,
SplitviewComponentUpdateOptions,
} from '../splitview/splitviewComponent';
import { IView, Orientation, Sizing } from '../splitview/splitview';
import { ISplitviewPanel } from '../splitview/splitviewPanel';
@ -40,25 +34,9 @@ import {
DockviewGroupPanel,
IDockviewGroupPanel,
} from '../dockview/dockviewGroupPanel';
import { Event } from '../events';
import { Emitter, 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';
import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel';
export interface CommonApi<T = any> {
readonly height: number;
@ -70,413 +48,236 @@ export interface CommonApi<T = any> {
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 height(): number {
return this.component.height;
}
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.
*/
updateOptions(options: SplitviewComponentUpdateOptions): void {
this.component.updateOptions(options);
}
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 {
addPanel(options: AddSplitviewComponentOptions): 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 width(): number {
return this.component.width;
}
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 onDidDrop(): Event<PaneviewDropEvent> {
const emitter = new Emitter<PaneviewDropEvent>();
get onUnhandledDragOverEvent(): Event<PaneviewDndOverlayEvent> {
return this.component.onUnhandledDragOverEvent;
const disposable = this.component.onDidDrop((e) => {
emitter.fire({ ...e, api: this });
});
emitter.dispose = () => {
disposable.dispose();
emitter.dispose();
};
return emitter.event;
}
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 {
addPanel(options: AddPaneviewComponentOptions): 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 width(): number {
return this.component.width;
}
get height(): number {
return this.component.height;
}
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;
}
@ -487,39 +288,22 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
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 {
addPanel(options: AddComponentOptions): 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 }
@ -527,407 +311,174 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
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> {
get onDidDrop(): Event<DockviewDropEvent> {
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 {
addPanel(options: AddPanelOptions): 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);
closeAllGroups(): void {
return this.component.closeAllGroups();
}
hasMaximizedGroup(): boolean {
return this.component.hasMaximizedGroup();
removeGroup(group: IDockviewGroupPanel): void {
this.component.removeGroup(<DockviewGroupPanel>group);
}
exitMaximizedGroup(): void {
this.component.exitMaximizedGroup();
getGroup(id: string): DockviewGroupPanel | undefined {
return this.component.getPanel(id);
}
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> {
return this.component.onDidMaximizedGroupChange;
}
/**
* Add a popout group in a new Window
*/
addPopoutGroup(
addFloatingGroup(
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);
coord?: { x: number; y: number }
): void {
return this.component.addFloatingGroup(item, coord);
}
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
fromJSON(data: SerializedDockview): void {
this.component.fromJSON(data);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
toJSON(): SerializedDockview {
return this.component.toJSON();
}
clear(): void {
this.component.clear();
}
}

View File

@ -1,139 +1,53 @@
import { Position, positionToDirection } from '../dnd/droptarget';
import { Position } 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;
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
}
export interface DockviewGroupPanelFloatingChangeEvent {
readonly location: DockviewGroupLocation;
readonly isFloating: boolean;
}
const NOT_INITIALIZED_MESSAGE =
'dockview: DockviewGroupPanelApiImpl not initialized';
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined;
readonly _onDidLocationChange =
readonly _onDidFloatingStateChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event;
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
get location(): DockviewGroupLocation {
get isFloating() {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
return this._group.model.location;
return this._group.model.isFloating;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id, '__dockviewgroup__');
super(id);
this.addDisposables(
this._onDidLocationChange,
this._onDidActivePanelChange
this.addDisposables(this._onDidFloatingStateChange);
}
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
undefined,
options.position ?? 'center'
);
}
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,67 +1,36 @@
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 { MutableDisposable } from '../lifecycle';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import {
DockviewGroupMoveParams,
DockviewGroupPanelFloatingChangeEvent,
} from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
import { Position } from '../dnd/droptarget';
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;
/*
* omit visibility modifiers since the visibility of a single group doesn't make sense
* because it belongs to a groupview
*/
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>;
readonly onDidActiveGroupChange: Event<void>;
readonly onDidGroupChange: Event<void>;
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;
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void;
}
export class DockviewPanelApiImpl
@ -69,56 +38,41 @@ export class DockviewPanelApiImpl
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>();
private readonly _onDidActiveGroupChange = new Emitter<void>();
readonly onDidActiveGroupChange = this._onDidActiveGroupChange.event;
private readonly _onDidGroupChange = new Emitter<GroupChangedEvent>();
private readonly _onDidGroupChange = new Emitter<void>();
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;
}
private readonly disposable = new MutableDisposable();
get title(): string | undefined {
return this.panel.title;
}
get isGroupActive(): boolean {
return this.group.isActive;
}
get renderer(): DockviewPanelRenderer {
return this.panel.renderer;
return !!this.group?.isActive;
}
set group(value: DockviewGroupPanel) {
const oldGroup = this._group;
const isOldGroupActive = this.isGroupActive;
if (this._group !== value) {
this._group = value;
this._group = value;
this._onDidGroupChange.fire({});
this._onDidGroupChange.fire();
this.setupGroupEventListeners(oldGroup);
this._onDidLocationChange.fire({
location: this.group.api.location,
if (this._group) {
this.disposable.value = this._group.api.onDidActiveChange(() => {
this._onDidActiveGroupChange.fire();
});
if (this.isGroupActive !== isOldGroupActive) {
this._onDidActiveGroupChange.fire();
}
}
}
@ -126,111 +80,44 @@ export class DockviewPanelApiImpl
return this._group;
}
get tabComponent(): string | undefined {
return this._tabComponent;
}
constructor(
private readonly panel: DockviewPanel,
private panel: IDockviewPanel,
group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
component: string,
tabComponent?: string
private readonly accessor: DockviewComponent
) {
super(panel.id, component);
this._tabComponent = tabComponent;
super(panel.id);
this.initialize(panel);
this._group = group;
this.setupGroupEventListeners();
this.addDisposables(
this.groupEventsDisposable,
this._onDidRendererChange,
this.disposable,
this._onDidTitleChange,
this._onDidGroupChange,
this._onDidActiveGroupChange,
this._onDidLocationChange
this._onDidActiveGroupChange
);
}
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,
},
});
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void {
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
this.panel.id,
options.position ?? 'center',
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

@ -37,15 +37,17 @@ export class GridviewPanelApiImpl
readonly onDidConstraintsChangeInternal: Event<GridConstraintChangeEvent2> =
this._onDidConstraintsChangeInternal.event;
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>();
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>({
replay: true,
});
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);
constructor(id: string, panel?: IPanel) {
super(id);
this.addDisposables(
this._onDidConstraintsChangeInternal,

View File

@ -1,4 +1,4 @@
import { DockviewEvent, Emitter, Event } from '../events';
import { Emitter, Event } from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
@ -24,14 +24,9 @@ export interface PanelApi {
readonly onDidFocusChange: Event<FocusEvent>;
readonly onDidVisibilityChange: Event<VisibilityEvent>;
readonly onDidActiveChange: Event<ActiveEvent>;
readonly onDidParametersChange: Event<Parameters>;
setActive(): void;
setVisible(isVisible: boolean): void;
setActive(): 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
*/
@ -56,16 +51,6 @@ export interface PanelApi {
* 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();
}
}
/**
@ -77,59 +62,67 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _isVisible = true;
private _width = 0;
private _height = 0;
private _parameters: Parameters = {};
private readonly panelUpdatesDisposable = new MutableDisposable();
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>();
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>({
replay: true,
});
readonly onDidDimensionsChange = this._onDidDimensionChange.event;
readonly _onDidChangeFocus = new Emitter<FocusEvent>();
//
readonly _onDidChangeFocus = new Emitter<FocusEvent>({
replay: true,
});
readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event;
//
readonly _onWillFocus = new Emitter<WillFocusEvent>();
readonly onWillFocus: Event<WillFocusEvent> = this._onWillFocus.event;
readonly _onFocusEvent = new Emitter<void>();
readonly onFocusEvent: Event<void> = this._onFocusEvent.event;
//
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>();
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>({
replay: true,
});
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 _onVisibilityChange = new Emitter<VisibilityEvent>();
readonly onVisibilityChange: Event<VisibilityEvent> =
this._onVisibilityChange.event;
//
readonly _onDidActiveChange = new Emitter<ActiveEvent>({
replay: true,
});
readonly onDidActiveChange: Event<ActiveEvent> =
this._onDidActiveChange.event;
//
readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event;
//
readonly _onUpdateParameters = new Emitter<Parameters>();
readonly onUpdateParameters: Event<Parameters> =
this._onUpdateParameters.event;
//
readonly _onDidParametersChange = new Emitter<Parameters>();
readonly onDidParametersChange: Event<Parameters> =
this._onDidParametersChange.event;
get isFocused(): boolean {
get isFocused() {
return this._isFocused;
}
get isActive(): boolean {
get isActive() {
return this._isActive;
}
get isVisible(): boolean {
get isVisible() {
return this._isVisible;
}
get width(): number {
get width() {
return this._width;
}
get height(): number {
get height() {
return this._height;
}
constructor(readonly id: string, readonly component: string) {
constructor(readonly id: string) {
super();
this.addDisposables(
@ -151,22 +144,16 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onDidChangeFocus,
this._onDidVisibilityChange,
this._onDidActiveChange,
this._onWillFocus,
this._onFocusEvent,
this._onActiveChange,
this._onWillFocus,
this._onWillVisibilityChange,
this._onDidParametersChange
this._onVisibilityChange,
this._onUpdateParameters
);
}
getParameters<T extends Parameters = Parameters>(): T {
return this._parameters as T;
}
public initialize(panel: IPanel): void {
this.panelUpdatesDisposable.value = this._onDidParametersChange.event(
this.panelUpdatesDisposable.value = this._onUpdateParameters.event(
(parameters) => {
this._parameters = parameters;
panel.update({
params: parameters,
});
@ -174,8 +161,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
);
}
setVisible(isVisible: boolean): void {
this._onWillVisibilityChange.fire({ isVisible });
setVisible(isVisible: boolean) {
this._onVisibilityChange.fire({ isVisible });
}
setActive(): void {
@ -183,6 +170,10 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
}
updateParameters(parameters: Parameters): void {
this._onDidParametersChange.fire(parameters);
this._onUpdateParameters.fire(parameters);
}
dispose() {
super.dispose();
}
}

View File

@ -35,8 +35,8 @@ export class PaneviewPanelApiImpl
this._pane = pane;
}
constructor(id: string, component: string) {
super(id, component);
constructor(id: string) {
super(id);
this.addDisposables(
this._onDidExpansionChange,

View File

@ -45,8 +45,8 @@ export class SplitviewPanelApiImpl
this._onDidSizeChange.event;
//
constructor(id: string, component: string) {
super(id, component);
constructor(id: string) {
super(id);
this.addDisposables(
this._onDidConstraintsChangeInternal,

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,4 +1,4 @@
import { disableIframePointEvents } from '../dom';
import { getElementsByTagName } from '../dom';
import { addDisposableListener, Emitter } from '../events';
import {
CompositeDisposable,
@ -10,7 +10,7 @@ export abstract class DragHandler extends CompositeDisposable {
private readonly dataDisposable = new MutableDisposable();
private readonly pointerEventsDisposable = new MutableDisposable();
private readonly _onDragStart = new Emitter<DragEvent>();
private readonly _onDragStart = new Emitter<void>();
readonly onDragStart = this._onDragStart.event;
constructor(protected readonly el: HTMLElement) {
@ -25,7 +25,7 @@ export abstract class DragHandler extends CompositeDisposable {
this.configure();
}
abstract getData(event: DragEvent): IDisposable;
abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
protected isCancelled(_event: DragEvent): boolean {
return false;
@ -35,49 +35,54 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
if (event.defaultPrevented || this.isCancelled(event)) {
if (this.isCancelled(event)) {
event.preventDefault();
return;
}
const iframes = disableIframePointEvents();
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
this.pointerEventsDisposable.value = {
dispose: () => {
iframes.release();
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
};
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.dataDisposable.value = this.getData(event);
this._onDragStart.fire(event);
this.dataDisposable.value = this.getData(event.dataTransfer);
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', '');
}
/**
* 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
* https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/
event.dataTransfer.setData(
'text/plain',
'__dockview_internal_drag_event__'
);
}
}),
addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose();
setTimeout(() => {
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
this.dataDisposable.dispose();
})
);
}

View File

@ -1,5 +1,7 @@
class TransferObject {
// intentionally empty class
constructor() {
//
}
}
export class PanelTransfer extends TransferObject {

View File

@ -13,51 +13,22 @@ export class DragAndDropObserver extends CompositeDisposable {
private target: EventTarget | null = null;
constructor(
private readonly element: HTMLElement,
private readonly callbacks: IDragAndDropObserverCallbacks
private element: HTMLElement,
private 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);
this.target = e.target;
this.callbacks.onDragEnter(e);
},
true
)
@ -68,7 +39,11 @@ export class DragAndDropObserver extends CompositeDisposable {
this.element,
'dragover',
(e: DragEvent) => {
this.onDragOver(e);
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);
}
},
true
)
@ -76,19 +51,24 @@ export class DragAndDropObserver extends CompositeDisposable {
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.onDragLeave(e);
if (this.target === e.target) {
this.target = null;
this.callbacks.onDragLeave(e);
}
})
);
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.onDragEnd(e);
this.target = null;
this.callbacks.onDragEnd(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.onDrop(e);
this.callbacks.onDrop(e);
})
);
}

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,8 +1,7 @@
.dv-drop-target {
.drop-target {
position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone {
> .drop-target-dropzone {
position: absolute;
left: 0px;
top: 0px;
@ -11,43 +10,32 @@
z-index: 1000;
pointer-events: none;
> .dv-drop-target-selection {
> .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;
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s 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);
}
&.small-top {
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);
}
&.small-bottom {
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);
}
&.small-left {
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);
}
&.small-right {
border-right: 1px solid var(--dv-drag-over-border-color);
}
}
}

View File

@ -1,35 +1,12 @@
import { toggleClass } from '../dom';
import { DockviewEvent, Emitter, Event } from '../events';
import { 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();
}
function numberOrFallback(maybeNumber: any, fallback: number): number {
return typeof maybeNumber === 'number' ? maybeNumber : fallback;
}
export function directionToPosition(direction: Direction): Position {
@ -66,54 +43,16 @@ export function positionToDirection(position: Position): Direction {
}
}
export interface DroptargetEvent {
readonly position: Position;
readonly nativeEvent: DragEvent;
}
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 type CanDisplayOverlay =
| boolean
| ((dragEvent: DragEvent, state: Position) => boolean);
export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined;
@ -124,204 +63,131 @@ export class Droptarget extends CompositeDisposable {
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
private readonly options: {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: {
size?: { value: number; type: 'pixels' | 'percentage' };
activationSize?: {
value: number;
type: 'pixels' | 'percentage';
};
};
}
) {
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) {
this.addDisposables(
this._onDrop,
new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragOver: (e) => {
if (this._acceptedTargetZonesSet.size === 0) {
this.removeDropTarget();
return;
}
this.removeDropTarget();
return;
}
const target =
this.options.getOverlayOutline?.() ?? this.element;
const width = this.element.clientWidth;
const height = this.element.clientHeight;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
if (width === 0 || height === 0) {
return; // avoid div!0
}
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
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
);
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) {
/**
* 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 (typeof this.options.canDisplayOverlay === 'boolean') {
if (!this.options.canDisplayOverlay) {
this.removeDropTarget();
return;
}
} else if (!this.options.canDisplayOverlay(e, quadrant)) {
this.removeDropTarget();
return;
}
this.markAsUsed(e);
if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';
this.overlayElement = document.createElement('div');
this.overlayElement.className = 'drop-target-selection';
this._state = 'center';
this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('drop-target');
this.element.append(this.targetElement);
}
this.toggleClasses(quadrant, width, height);
this.setState(quadrant);
},
onDragLeave: () => {
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) {
},
onDragEnd: () => {
this.removeDropTarget();
return;
}
},
onDrop: (e) => {
e.preventDefault();
this.markAsUsed(e);
const state = this._state;
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);
this.removeDropTarget();
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) {
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: this._state,
nativeEvent: e,
});
this._onDrop.fire({ position: 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();
@ -335,7 +201,7 @@ export class Droptarget extends CompositeDisposable {
}
/**
* Check is the event has already been used by another instance of DropTarget
* Check is the event has already been used by another instance od DropTarget
*/
private isAlreadyUsed(event: DragEvent): boolean {
const value = (event as any)[Droptarget.USED_EVENT_ID];
@ -347,14 +213,12 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): void {
const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
if (!this.overlayElement) {
return;
}
const isSmallX = width < SMALL_WIDTH_BOUNDARY;
const isSmallY = height < SMALL_HEIGHT_BOUNDARY;
const isSmallX = width < 100;
const isSmallY = height < 100;
const isLeft = quadrant === 'left';
const isRight = quadrant === 'right';
@ -366,175 +230,68 @@ export class Droptarget extends CompositeDisposable {
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
let size = 1;
let size = 0.5;
const sizeOptions = this.options.overlayModel?.size ?? DEFAULT_SIZE;
if (this.options.overlayModel?.size?.type === 'percentage') {
size = clamp(this.options.overlayModel.size.value, 0, 100) / 100;
}
if (sizeOptions.type === 'percentage') {
size = clamp(sizeOptions.value, 0, 100) / 100;
} else {
if (this.options.overlayModel?.size?.type === 'pixels') {
if (rightClass || leftClass) {
size = clamp(0, sizeOptions.value, width) / width;
size =
clamp(0, this.options.overlayModel.size.value, width) /
width;
}
if (topClass || bottomClass) {
size = clamp(0, sizeOptions.value, height) / height;
size =
clamp(0, this.options.overlayModel.size.value, height) /
height;
}
}
if (target) {
const outlineEl =
this.options.getOverlayOutline?.() ?? this.element;
const elBox = outlineEl.getBoundingClientRect();
const translate = (1 - size) / 2;
const scale = size;
const ta = target.getElements(undefined, outlineEl);
const el = ta.root;
const overlay = ta.overlay;
let transform: string;
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}%`;
transform = `translateX(${100 * translate}%) scaleX(${scale})`;
} else if (leftClass) {
box.width = `${100 * size}%`;
transform = `translateX(-${100 * translate}%) scaleX(${scale})`;
} else if (topClass) {
box.height = `${100 * size}%`;
transform = `translateY(-${100 * translate}%) scaleY(${scale})`;
} else if (bottomClass) {
box.top = `${100 * (1 - size)}%`;
box.height = `${100 * size}%`;
transform = `translateY(${100 * translate}%) scaleY(${scale})`;
} else {
transform = '';
}
this.overlayElement.style.top = box.top;
this.overlayElement.style.left = box.left;
this.overlayElement.style.width = box.width;
this.overlayElement.style.height = box.height;
this.overlayElement.style.transform = transform;
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'
);
toggleClass(this.overlayElement, 'small-right', isSmallX && isRight);
toggleClass(this.overlayElement, 'small-left', isSmallX && isLeft);
toggleClass(this.overlayElement, 'small-top', isSmallY && isTop);
toggleClass(this.overlayElement, 'small-bottom', isSmallY && isBottom);
}
private setState(quadrant: Position): void {
switch (quadrant) {
case 'top':
this._state = 'top';
break;
case 'left':
this._state = 'left';
break;
case 'bottom':
this._state = 'bottom';
break;
case 'right':
this._state = 'right';
break;
case 'center':
this._state = 'center';
break;
}
}
private calculateQuadrant(
@ -544,11 +301,14 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): Position | null {
const activationSizeOptions =
this.options.overlayModel?.activationSize ??
DEFAULT_ACTIVATION_SIZE;
const isPercentage =
this.options.overlayModel?.activationSize === undefined ||
this.options.overlayModel?.activationSize?.type === 'percentage';
const isPercentage = activationSizeOptions.type === 'percentage';
const value = numberOrFallback(
this.options?.overlayModel?.activationSize?.value,
20
);
if (isPercentage) {
return calculateQuadrantAsPercentage(
@ -557,7 +317,7 @@ export class Droptarget extends CompositeDisposable {
y,
width,
height,
activationSizeOptions.value
value
);
}
@ -567,19 +327,17 @@ export class Droptarget extends CompositeDisposable {
y,
width,
height,
activationSizeOptions.value
value
);
}
private removeDropTarget(): void {
if (this.targetElement) {
this._state = undefined;
this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.element.removeChild(this.targetElement);
this.targetElement = undefined;
this.overlayElement = undefined;
this.element.classList.remove('drop-target');
}
}
}

View File

@ -2,17 +2,13 @@ import { addClasses, removeClasses } from '../dom';
export function addGhostImage(
dataTransfer: DataTransfer,
ghostElement: HTMLElement,
options?: { x?: number; y?: number }
ghostElement: HTMLElement
): 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);
dataTransfer.setDragImage(ghostElement, 0, 0);
setTimeout(() => {
removeClasses(ghostElement, 'dv-dragged');

View File

@ -1,4 +1,3 @@
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events';
@ -13,7 +12,7 @@ export class GroupDragHandler extends DragHandler {
constructor(
element: HTMLElement,
private readonly accessor: DockviewComponent,
private readonly accessorId: string,
private readonly group: DockviewGroupPanel
) {
super(element);
@ -21,7 +20,7 @@ export class GroupDragHandler extends DragHandler {
this.addDisposables(
addDisposableListener(
element,
'pointerdown',
'mousedown',
(e) => {
if (e.shiftKey) {
/**
@ -38,17 +37,15 @@ export class GroupDragHandler extends DragHandler {
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.api.location.type === 'floating' && !_event.shiftKey) {
if (this.group.api.isFloating && !_event.shiftKey) {
return true;
}
return false;
}
getData(dragEvent: DragEvent): IDisposable {
const dataTransfer = dragEvent.dataTransfer;
getData(dataTransfer: DataTransfer | null): IDisposable {
this.panelTransfer.setData(
[new PanelTransfer(this.accessor.id, this.group.id, null)],
[new PanelTransfer(this.accessorId, this.group.id, null)],
PanelTransfer.prototype
);
@ -72,11 +69,9 @@ export class GroupDragHandler extends DragHandler {
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 });
addGhostImage(dataTransfer, ghostElement);
}
return {

View File

@ -26,18 +26,16 @@
}
.dv-resize-container {
--dv-overlay-z-index: var(--dv-overlay-z-index, 999);
position: absolute;
z-index: calc(var(--dv-overlay-z-index) - 2);
z-index: 997;
&.dv-bring-to-front {
z-index: 998;
}
border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow);
&.dv-hidden {
display: none;
}
&.dv-resize-container-dragging {
opacity: 0.5;
}
@ -47,7 +45,7 @@
width: calc(100% - 8px);
left: 4px;
top: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: ns-resize;
}
@ -57,7 +55,7 @@
width: calc(100% - 8px);
left: 4px;
bottom: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: ns-resize;
}
@ -67,7 +65,7 @@
width: 4px;
left: -2px;
top: 4px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: ew-resize;
}
@ -77,7 +75,7 @@
width: 4px;
right: -2px;
top: 4px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: ew-resize;
}
@ -87,7 +85,7 @@
width: 4px;
top: -2px;
left: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: nw-resize;
}
@ -97,7 +95,7 @@
width: 4px;
right: -2px;
top: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: ne-resize;
}
@ -107,7 +105,7 @@
width: 4px;
left: -2px;
bottom: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: sw-resize;
}
@ -117,7 +115,7 @@
width: 4px;
right: -2px;
bottom: -2px;
z-index: var(--dv-overlay-z-index);
z-index: 999;
position: absolute;
cursor: se-resize;
}

View File

@ -1,5 +1,5 @@
import {
disableIframePointEvents,
getElementsByTagName,
quasiDefaultPrevented,
toggleClass,
} from '../dom';
@ -7,44 +7,28 @@ import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
import { AnchoredBox } from '../types';
class AriaLevelTracker {
private _orderedList: HTMLElement[] = [];
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
push(element: HTMLElement): void {
this._orderedList = [
...this._orderedList.filter((item) => item !== element),
element,
];
this.update();
}
destroy(element: HTMLElement): void {
this._orderedList = this._orderedList.filter(
(item) => item !== element
);
this.update();
}
private update(): void {
for (let i = 0; i < this._orderedList.length; i++) {
this._orderedList[i].setAttribute('aria-level', `${i}`);
this._orderedList[
i
].style.zIndex = `calc(var(--dv-overlay-z-index, 999) + ${i * 2})`;
function pushToTop(element: HTMLElement) {
if (previous !== element && previous !== null) {
toggleClass(previous, 'dv-bring-to-front', false);
}
}
}
const arialLevelTracker = new AriaLevelTracker();
toggleClass(element, 'dv-bring-to-front', true);
previous = element;
}
return pushToTop;
})();
export class Overlay extends CompositeDisposable {
private readonly _element: HTMLElement = document.createElement('div');
private _element: HTMLElement = document.createElement('div');
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
@ -52,36 +36,19 @@ export class Overlay extends CompositeDisposable {
private readonly _onDidChangeEnd = new Emitter<void>();
readonly onDidChangeEnd: Event<void> = this._onDidChangeEnd.event;
private static readonly MINIMUM_HEIGHT = 20;
private static readonly MINIMUM_WIDTH = 20;
private verticalAlignment: 'top' | 'bottom' | undefined;
private horiziontalAlignment: 'left' | 'right' | undefined;
private _isVisible: boolean;
set minimumInViewportWidth(value: number | undefined) {
this.options.minimumInViewportWidth = value;
}
set minimumInViewportHeight(value: number | undefined) {
this.options.minimumInViewportHeight = value;
}
get element(): HTMLElement {
return this._element;
}
get isVisible(): boolean {
return this._isVisible;
}
private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 20;
constructor(
private readonly options: AnchoredBox & {
private readonly options: {
height: number;
width: number;
left: number;
top: number;
container: HTMLElement;
content: HTMLElement;
minimumInViewportWidth?: number;
minimumInViewportHeight?: number;
minimumInViewportWidth: number;
minimumInViewportHeight: number;
}
) {
super();
@ -89,7 +56,6 @@ export class Overlay extends CompositeDisposable {
this.addDisposables(this._onDidChange, this._onDidChangeEnd);
this._element.className = 'dv-resize-container';
this._isVisible = true;
this.setupResize('top');
this.setupResize('bottom');
@ -107,55 +73,30 @@ export class Overlay extends CompositeDisposable {
this.setBounds({
height: this.options.height,
width: this.options.width,
...('top' in this.options && { top: this.options.top }),
...('bottom' in this.options && { bottom: this.options.bottom }),
...('left' in this.options && { left: this.options.left }),
...('right' in this.options && { right: this.options.right }),
top: this.options.top,
left: this.options.left,
});
arialLevelTracker.push(this._element);
}
setVisible(isVisible: boolean): void {
if (isVisible === this.isVisible) {
return;
}
this._isVisible = isVisible;
toggleClass(this.element, 'dv-hidden', !this.isVisible);
}
bringToFront(): void {
arialLevelTracker.push(this._element);
}
setBounds(bounds: Partial<AnchoredBox> = {}): void {
setBounds(
bounds: Partial<{
height: number;
width: number;
top: number;
left: number;
}> = {}
): void {
if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`;
}
if (typeof bounds.width === 'number') {
this._element.style.width = `${bounds.width}px`;
}
if ('top' in bounds && typeof bounds.top === 'number') {
if (typeof bounds.top === 'number') {
this._element.style.top = `${bounds.top}px`;
this._element.style.bottom = 'auto';
this.verticalAlignment = 'top';
}
if ('bottom' in bounds && typeof bounds.bottom === 'number') {
this._element.style.bottom = `${bounds.bottom}px`;
this._element.style.top = 'auto';
this.verticalAlignment = 'bottom';
}
if ('left' in bounds && typeof bounds.left === 'number') {
if (typeof bounds.left === 'number') {
this._element.style.left = `${bounds.left}px`;
this._element.style.right = 'auto';
this.horiziontalAlignment = 'left';
}
if ('right' in bounds && typeof bounds.right === 'number') {
this._element.style.right = `${bounds.right}px`;
this._element.style.left = 'auto';
this.horiziontalAlignment = 'right';
}
const containerRect = this.options.container.getBoundingClientRect();
@ -164,80 +105,45 @@ export class Overlay extends CompositeDisposable {
// region: ensure bounds within allowable limits
// a minimum width of minimumViewportWidth must be inside the viewport
const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width));
const xOffset = Math.max(
0,
overlayRect.width - this.options.minimumInViewportWidth
);
// a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height));
const yOffset = Math.max(
0,
overlayRect.height - this.options.minimumInViewportHeight
);
if (this.verticalAlignment === 'top') {
const top = clamp(
overlayRect.top - containerRect.top,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
this._element.style.top = `${top}px`;
this._element.style.bottom = 'auto';
}
const left = clamp(
overlayRect.left - containerRect.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
if (this.verticalAlignment === 'bottom') {
const bottom = clamp(
containerRect.bottom - overlayRect.bottom,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
this._element.style.bottom = `${bottom}px`;
this._element.style.top = 'auto';
}
const top = clamp(
overlayRect.top - containerRect.top,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
if (this.horiziontalAlignment === 'left') {
const left = clamp(
overlayRect.left - containerRect.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
this._element.style.left = `${left}px`;
this._element.style.right = 'auto';
}
if (this.horiziontalAlignment === 'right') {
const right = clamp(
containerRect.right - overlayRect.right,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
this._element.style.right = `${right}px`;
this._element.style.left = 'auto';
}
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
this._onDidChange.fire();
}
toJSON(): AnchoredBox {
toJSON(): { top: number; left: number; height: number; width: number } {
const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect();
const result: any = {};
if (this.verticalAlignment === 'top') {
result.top = parseFloat(this._element.style.top);
} else if (this.verticalAlignment === 'bottom') {
result.bottom = parseFloat(this._element.style.bottom);
} else {
result.top = element.top - container.top;
}
if (this.horiziontalAlignment === 'left') {
result.left = parseFloat(this._element.style.left);
} else if (this.horiziontalAlignment === 'right') {
result.right = parseFloat(this._element.style.right);
} else {
result.left = element.left - container.left;
}
result.width = element.width;
result.height = element.height;
return result;
return {
top: element.top - container.top,
left: element.left - container.left,
width: element.width,
height: element.height,
};
}
setupDrag(
@ -249,15 +155,24 @@ export class Overlay extends CompositeDisposable {
const track = () => {
let offset: { x: number; y: number } | null = null;
const iframes = disableIframePointEvents();
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable(
{
dispose: () => {
iframes.release();
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
},
addDisposableListener(window, 'pointermove', (e) => {
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const x = e.clientX - containerRect.left;
@ -279,32 +194,12 @@ export class Overlay extends CompositeDisposable {
const xOffset = Math.max(
0,
this.getMinimumWidth(overlayRect.width)
overlayRect.width - this.options.minimumInViewportWidth
);
const yOffset = Math.max(
0,
this.getMinimumHeight(overlayRect.height)
);
const top = clamp(
y - offset.y,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
);
const bottom = clamp(
offset.y -
y +
containerRect.height -
overlayRect.height,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
overlayRect.height -
this.options.minimumInViewportHeight
);
const left = clamp(
@ -316,34 +211,18 @@ export class Overlay extends CompositeDisposable {
)
);
const right = clamp(
offset.x - x + containerRect.width - overlayRect.width,
-xOffset,
const top = clamp(
y - offset.y,
-yOffset,
Math.max(
0,
containerRect.width - overlayRect.width + xOffset
containerRect.height - overlayRect.height + yOffset
)
);
const bounds: any = {};
// Anchor to top or to bottom depending on which one is closer
if (top <= bottom) {
bounds.top = top;
} else {
bounds.bottom = bottom;
}
// Anchor to left or to right depending on which one is closer
if (left <= right) {
bounds.left = left;
} else {
bounds.right = right;
}
this.setBounds(bounds);
this.setBounds({ top, left });
}),
addDisposableListener(window, 'pointerup', () => {
addDisposableWindowListener(window, 'mouseup', () => {
toggleClass(
this._element,
'dv-resize-container-dragging',
@ -358,7 +237,7 @@ export class Overlay extends CompositeDisposable {
this.addDisposables(
move,
addDisposableListener(dragTarget, 'pointerdown', (event) => {
addDisposableListener(dragTarget, 'mousedown', (event) => {
if (event.defaultPrevented) {
event.preventDefault();
return;
@ -374,7 +253,7 @@ export class Overlay extends CompositeDisposable {
}),
addDisposableListener(
this.options.content,
'pointerdown',
'mousedown',
(event) => {
if (event.defaultPrevented) {
return;
@ -393,14 +272,16 @@ export class Overlay extends CompositeDisposable {
),
addDisposableListener(
this.options.content,
'pointerdown',
'mousedown',
() => {
arialLevelTracker.push(this._element);
bringElementToFront(this._element);
},
true
)
);
bringElementToFront(this._element);
if (options.inDragMode) {
track();
}
@ -425,7 +306,7 @@ export class Overlay extends CompositeDisposable {
this.addDisposables(
move,
addDisposableListener(resizeHandleElement, 'pointerdown', (e) => {
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
e.preventDefault();
let startPosition: {
@ -435,10 +316,17 @@ export class Overlay extends CompositeDisposable {
originalWidth: number;
} | null = null;
const iframes = disableIframePointEvents();
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable(
addDisposableListener(window, 'pointermove', (e) => {
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const overlayRect =
@ -458,22 +346,24 @@ export class Overlay extends CompositeDisposable {
}
let top: number | undefined = undefined;
let bottom: number | undefined = undefined;
let height: number | undefined = undefined;
let left: number | undefined = undefined;
let right: number | undefined = undefined;
let width: number | undefined = undefined;
const moveTop = () => {
const minimumInViewportHeight =
this.options.minimumInViewportHeight;
const minimumInViewportWidth =
this.options.minimumInViewportWidth;
function moveTop(): void {
top = clamp(
y,
-Number.MAX_VALUE,
startPosition!.originalY +
startPosition!.originalHeight >
containerRect.height
? this.getMinimumHeight(
containerRect.height
)
? containerRect.height -
minimumInViewportHeight
: Math.max(
0,
startPosition!.originalY +
@ -481,42 +371,35 @@ export class Overlay extends CompositeDisposable {
Overlay.MINIMUM_HEIGHT
)
);
height =
startPosition!.originalY +
startPosition!.originalHeight -
top;
}
bottom = containerRect.height - top - height;
};
const moveBottom = () => {
function moveBottom(): void {
top =
startPosition!.originalY -
startPosition!.originalHeight;
height = clamp(
y - top,
top < 0 &&
typeof this.options
.minimumInViewportHeight === 'number'
? -top +
this.options.minimumInViewportHeight
top < 0
? -top + minimumInViewportHeight
: Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE
);
}
bottom = containerRect.height - top - height;
};
const moveLeft = () => {
function moveLeft(): void {
left = clamp(
x,
-Number.MAX_VALUE,
startPosition!.originalX +
startPosition!.originalWidth >
containerRect.width
? this.getMinimumWidth(containerRect.width)
? containerRect.width -
minimumInViewportWidth
: Math.max(
0,
startPosition!.originalX +
@ -529,28 +412,21 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalX +
startPosition!.originalWidth -
left;
}
right = containerRect.width - left - width;
};
const moveRight = () => {
function moveRight(): void {
left =
startPosition!.originalX -
startPosition!.originalWidth;
width = clamp(
x - left,
left < 0 &&
typeof this.options
.minimumInViewportWidth === 'number'
? -left +
this.options.minimumInViewportWidth
left < 0
? -left + minimumInViewportWidth
: Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE
);
right = containerRect.width - left - width;
};
}
switch (direction) {
case 'top':
@ -583,33 +459,16 @@ export class Overlay extends CompositeDisposable {
break;
}
const bounds: any = {};
// Anchor to top or to bottom depending on which one is closer
if (top! <= bottom!) {
bounds.top = top;
} else {
bounds.bottom = bottom;
}
// Anchor to left or to right depending on which one is closer
if (left! <= right!) {
bounds.left = left;
} else {
bounds.right = right;
}
bounds.height = height;
bounds.width = width;
this.setBounds(bounds);
this.setBounds({ height, width, top, left });
}),
{
dispose: () => {
iframes.release();
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
},
addDisposableListener(window, 'pointerup', () => {
addDisposableWindowListener(window, 'mouseup', () => {
move.dispose();
this._onDidChangeEnd.fire();
})
@ -618,22 +477,7 @@ export class Overlay extends CompositeDisposable {
);
}
private getMinimumWidth(width: number) {
if (typeof this.options.minimumInViewportWidth === 'number') {
return width - this.options.minimumInViewportWidth;
}
return 0;
}
private getMinimumHeight(height: number) {
if (typeof this.options.minimumInViewportHeight === 'number') {
return height - this.options.minimumInViewportHeight;
}
return 0;
}
override dispose(): void {
arialLevelTracker.destroy(this._element);
this._element.remove();
super.dispose();
}

View File

@ -6,13 +6,8 @@ import {
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;
@ -21,16 +16,15 @@ export interface IContentContainer extends IDisposable {
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 _element: HTMLElement;
private panel: IDockviewPanel | undefined;
private readonly disposable = new MutableDisposable();
private disposable = new MutableDisposable();
private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
@ -42,57 +36,19 @@ export class ContentContainer
return this._element;
}
readonly dropTarget: Droptarget;
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanelModel
) {
constructor() {
super();
this._element = document.createElement('div');
this._element.className = 'dv-content-container';
this._element.className = '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);
// for hosted containers
// 1) register a drop target on the host
// 2) register window dragStart events to disable pointer events
// 3) register dragEnd events
// 4) register mouseMove events (if no buttons are present we take this as a dragEnd event)
}
show(): void {
@ -103,60 +59,25 @@ export class ContentContainer
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);
public openPanel(panel: IDockviewPanel): void {
if (this.panel === panel) {
return;
}
if (this.panel) {
if (this.panel.view?.content) {
this._element.removeChild(this.panel.view.content.element);
}
this.panel = undefined;
}
this.panel = panel;
let container: HTMLElement;
const disposable = new CompositeDisposable();
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 (this.panel.view) {
const _onDidFocus = this.panel.view.content.onDidFocus;
const _onDidBlur = this.panel.view.content.onDidBlur;
if (doRender) {
const focusTracker = trackFocus(container);
const disposable = new CompositeDisposable();
const focusTracker = trackFocus(this._element);
disposable.addDisposables(
focusTracker,
@ -164,16 +85,21 @@ export class ContentContainer
focusTracker.onDidBlur(() => this._onDidBlur.fire())
);
this.disposable.value = disposable;
}
}
if (_onDidFocus) {
disposable.addDisposables(
_onDidFocus(() => this._onDidFocus.fire())
);
}
if (_onDidBlur) {
disposable.addDisposables(
_onDidBlur(() => this._onDidBlur.fire())
);
}
public openPanel(panel: IDockviewPanel): void {
if (this.panel === panel) {
return;
this._element.appendChild(this.panel.view.content.element);
}
this.renderPanel(panel);
this.disposable.value = disposable;
}
public layout(_width: number, _height: number): void {
@ -181,14 +107,10 @@ export class ContentContainer
}
public closePanel(): void {
if (this.panel) {
if (this.panel.api.renderer === 'onlyWhenVisible') {
this.panel.view.content.element.parentElement?.removeChild(
this.panel.view.content.element
);
}
if (this.panel?.view?.content?.element) {
this._element.removeChild(this.panel.view.content.element);
this.panel = undefined;
}
this.panel = undefined;
}
public dispose(): void {

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

@ -6,78 +6,70 @@
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
}
.dv-tab {
.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 {
.tab-action {
background-color: var(--dv-activegroup-visiblepanel-tab-color);
}
}
&.dv-active-tab {
.dv-default-tab {
.dv-default-tab-action {
&.active-tab > .default-tab {
.tab-action {
visibility: visible;
}
}
&.inactive-tab > .default-tab {
.tab-action {
visibility: hidden;
}
&:hover {
.tab-action {
visibility: visible;
}
}
}
&.dv-inactive-tab {
.dv-default-tab {
.dv-default-tab-action {
visibility: hidden;
}
&:hover {
.dv-default-tab-action {
visibility: visible;
.default-tab {
position: relative;
height: 100%;
display: flex;
min-width: 80px;
align-items: center;
padding: 0px 8px;
white-space: nowrap;
text-overflow: elipsis;
.tab-content {
padding: 0px 8px;
flex-grow: 1;
}
.action-container {
text-align: right;
display: flex;
.tab-list {
display: flex;
padding: 0px;
margin: 0px;
justify-content: flex-end;
.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);
}
}
}
}
}
.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);
}
}
}
}

View File

@ -1,13 +1,18 @@
import { CompositeDisposable } from '../../../lifecycle';
import { ITabRenderer, GroupPanelPartInitParameters } from '../../types';
import { addDisposableListener } from '../../../events';
import { PanelUpdateEvent } from '../../../panel/types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { createCloseButton } from '../../../svg';
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private readonly _element: HTMLElement;
private readonly _content: HTMLElement;
private readonly action: HTMLElement;
private _title: string | undefined;
private _element: HTMLElement;
private _content: HTMLElement;
private _actionContainer: HTMLElement;
private _list: HTMLElement;
private action: HTMLElement;
//
private params: GroupPanelPartInitParameters = {} as any;
get element(): HTMLElement {
return this._element;
@ -17,48 +22,70 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
super();
this._element = document.createElement('div');
this._element.className = 'dv-default-tab';
this._element.className = 'default-tab';
//
this._content = document.createElement('div');
this._content.className = 'dv-default-tab-content';
this._content.className = 'tab-content';
//
this._actionContainer = document.createElement('div');
this._actionContainer.className = 'action-container';
//
this._list = document.createElement('ul');
this._list.className = 'tab-list';
//
this.action = document.createElement('div');
this.action.className = 'dv-default-tab-action';
this.action.className = 'tab-action';
this.action.appendChild(createCloseButton());
//
this._element.appendChild(this._content);
this._element.appendChild(this.action);
this.render();
}
init(params: GroupPanelPartInitParameters): void {
this._title = params.title;
this._element.appendChild(this._actionContainer);
this._actionContainer.appendChild(this._list);
this._list.appendChild(this.action);
//
this.addDisposables(
params.api.onDidTitleChange((event) => {
this._title = event.title;
this.render();
}),
addDisposableListener(this.action, 'pointerdown', (ev) => {
addDisposableListener(this._actionContainer, 'mousedown', (ev) => {
ev.preventDefault();
}),
addDisposableListener(this.action, 'click', (ev) => {
if (ev.defaultPrevented) {
return;
}
ev.preventDefault();
params.api.close();
})
);
this.render();
}
public update(event: PanelUpdateEvent): void {
this.params = { ...this.params, ...event.params };
this.render();
}
focus(): void {
//noop
}
public init(params: GroupPanelPartInitParameters): void {
this.params = params;
this._content.textContent = params.title;
addDisposableListener(this.action, 'click', (ev) => {
ev.preventDefault(); //
this.params.api.close();
});
}
onGroupChange(_group: DockviewGroupPanel): void {
this.render();
}
onPanelVisibleChange(_isPanelVisible: boolean): void {
this.render();
}
public layout(_width: number, _height: number): void {
// noop
}
private render(): void {
if (this._content.textContent !== this._title) {
this._content.textContent = this._title ?? '';
if (this._content.textContent !== this.params.title) {
this._content.textContent = this.params.title;
}
}
}

View File

@ -7,88 +7,85 @@ import {
} from '../../../dnd/dataTransfer';
import { toggleClass } from '../../../dom';
import { DockviewComponent } from '../../dockviewComponent';
import { ITabRenderer } from '../../types';
import { DockviewDropTargets, ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import {
DroptargetEvent,
Droptarget,
WillShowOverlayEvent,
} from '../../../dnd/droptarget';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
constructor(
element: HTMLElement,
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel,
private readonly panel: IDockviewPanel
) {
super(element);
}
getData(event: DragEvent): IDisposable {
this.panelTransfer.setData(
[new PanelTransfer(this.accessor.id, this.group.id, this.panel.id)],
PanelTransfer.prototype
);
return {
dispose: () => {
this.panelTransfer.clearData(PanelTransfer.prototype);
},
};
}
export interface ITab extends IDisposable {
readonly panelId: string;
readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void;
onChanged: Event<MouseEvent>;
onDrop: Event<DroptargetEvent>;
setActive(isActive: boolean): void;
}
export class Tab extends CompositeDisposable {
export class Tab extends CompositeDisposable implements ITab {
private readonly _element: HTMLElement;
private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined;
private readonly droptarget: Droptarget;
private content?: ITabRenderer;
private readonly _onPointDown = new Emitter<MouseEvent>();
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event;
readonly onWillShowOverlay: Event<WillShowOverlayEvent>;
public get element(): HTMLElement {
return this._element;
}
constructor(
public readonly panel: IDockviewPanel,
public readonly panelId: string,
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-tab';
this._element.className = 'tab';
this._element.tabIndex = 0;
this._element.draggable = true;
toggleClass(this.element, 'dv-inactive-tab', true);
toggleClass(this.element, 'inactive-tab', true);
const dragHandler = new TabDragHandler(
this._element,
this.accessor,
this.group,
this.panel
this.addDisposables(
this._onChanged,
this._onDropped,
new (class Handler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
getData(): IDisposable {
this.panelTransfer.setData(
[new PanelTransfer(accessor.id, group.id, panelId)],
PanelTransfer.prototype
);
return {
dispose: () => {
this.panelTransfer.clearData(
PanelTransfer.prototype
);
},
};
}
})(this._element)
);
this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['left', 'right'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
this.addDisposables(
addDisposableListener(this._element, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
}
this._onChanged.fire(event);
})
);
this.droptarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
@ -97,58 +94,36 @@ export class Tab extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
return true;
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panelId !== data.panelId;
}
return this.group.model.canDisplayOverlay(
event,
position,
'tab'
DockviewDropTargets.Tab
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables(
this._onPointDown,
this._onDropped,
this._onDragStart,
dragHandler.onDragStart((event) => {
if (event.dataTransfer) {
const style = getComputedStyle(this.element);
const newNode = this.element.cloneNode(true) as HTMLElement;
Array.from(style).forEach((key) =>
newNode.style.setProperty(
key,
style.getPropertyValue(key),
style.getPropertyPriority(key)
)
);
newNode.style.position = 'absolute';
addGhostImage(event.dataTransfer, newNode, {
y: -10,
x: 30,
});
}
this._onDragStart.fire(event);
}),
dragHandler,
addDisposableListener(this._element, 'pointerdown', (event) => {
this._onPointDown.fire(event);
}),
this.dropTarget.onDrop((event) => {
this.droptarget.onDrop((event) => {
this._onDropped.fire(event);
}),
this.dropTarget
this.droptarget
);
}
public setActive(isActive: boolean): void {
toggleClass(this.element, 'dv-active-tab', isActive);
toggleClass(this.element, 'dv-inactive-tab', !isActive);
toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'inactive-tab', !isActive);
}
public setContent(part: ITabRenderer): void {

View File

@ -1,19 +0,0 @@
.dv-tabs-overflow-dropdown-default {
height: 100%;
color: var(--dv-activegroup-hiddenpanel-tab-color);
margin: var(--dv-tab-margin);
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0.25rem 0.5rem;
cursor: pointer;
> span {
padding-left: 0.25rem;
}
> svg {
transform: rotate(90deg);
}
}

View File

@ -1,25 +0,0 @@
import { createChevronRightButton } from '../../../svg';
export type DropdownElement = {
element: HTMLElement;
update: (params: { tabs: number }) => void;
dispose?: () => void;
};
export function createDropdownElementHandle(): DropdownElement {
const el = document.createElement('div');
el.className = 'dv-tabs-overflow-dropdown-default';
const text = document.createElement('span');
text.textContent = ``;
const icon = createChevronRightButton();
el.appendChild(icon);
el.appendChild(text);
return {
element: el,
update: (params: { tabs: number }) => {
text.textContent = `${params.tabs}`;
},
};
}

View File

@ -1,79 +0,0 @@
.dv-tabs-container {
display: flex;
height: 100%;
overflow: auto;
scrollbar-width: thin; // firefox
&.dv-horizontal {
.dv-tab {
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
}
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
}
.dv-scrollable {
> .dv-tabs-container {
overflow: hidden;
}
}
.dv-tab {
-webkit-user-drag: element;
outline: none;
padding: 0.25rem 0.5rem;
cursor: pointer;
position: relative;
box-sizing: border-box;
font-size: var(--dv-tab-font-size);
margin: var(--dv-tab-margin);
}
.dv-tabs-overflow-container {
flex-direction: column;
height: unset;
border: 1px solid var(--dv-tab-divider-color);
background-color: var(--dv-group-view-background-color);
.dv-tab {
&:not(:last-child) {
border-bottom: 1px solid var(--dv-tab-divider-color);
}
}
.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}

View File

@ -1,301 +0,0 @@
import { getPanelData } from '../../../dnd/dataTransfer';
import {
isChildEntirelyVisibleWithinParent,
OverflowObserver,
} from '../../../dom';
import { addDisposableListener, Emitter, Event } from '../../../events';
import {
CompositeDisposable,
Disposable,
IValueDisposable,
MutableDisposable,
} from '../../../lifecycle';
import { Scrollbar } from '../../../scrollbar';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { Tab } from '../tab/tab';
import { TabDragEvent, TabDropIndexEvent } from './tabsContainer';
export class Tabs extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly _tabsList: HTMLElement;
private readonly _observerDisposable = new MutableDisposable();
private _tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private _showTabsOverflowControl = false;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
private readonly _onOverflowTabsChange = new Emitter<{
tabs: string[];
reset: boolean;
}>();
readonly onOverflowTabsChange = this._onOverflowTabsChange.event;
get showTabsOverflowControl(): boolean {
return this._showTabsOverflowControl;
}
set showTabsOverflowControl(value: boolean) {
if (this._showTabsOverflowControl == value) {
return;
}
this._showTabsOverflowControl = value;
if (value) {
const observer = new OverflowObserver(this._tabsList);
this._observerDisposable.value = new CompositeDisposable(
observer,
observer.onDidChange((event) => {
const hasOverflow = event.hasScrollX || event.hasScrollY;
this.toggleDropdown({ reset: !hasOverflow });
}),
addDisposableListener(this._tabsList, 'scroll', () => {
this.toggleDropdown({ reset: false });
})
);
}
}
get element(): HTMLElement {
return this._element;
}
get panels(): string[] {
return this._tabs.map((_) => _.value.panel.id);
}
get size(): number {
return this._tabs.length;
}
get tabs(): Tab[] {
return this._tabs.map((_) => _.value);
}
constructor(
private readonly group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
options: {
showTabsOverflowControl: boolean;
}
) {
super();
this._tabsList = document.createElement('div');
this._tabsList.className = 'dv-tabs-container dv-horizontal';
this.showTabsOverflowControl = options.showTabsOverflowControl;
if (accessor.options.scrollbars === 'native') {
this._element = this._tabsList;
} else {
const scrollbar = new Scrollbar(this._tabsList);
this._element = scrollbar.element;
this.addDisposables(scrollbar);
}
this.addDisposables(
this._onOverflowTabsChange,
this._observerDisposable,
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
addDisposableListener(this.element, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
}),
Disposable.from(() => {
for (const { value, disposable } of this._tabs) {
disposable.dispose();
value.dispose();
}
this._tabs = [];
})
);
}
indexOf(id: string): number {
return this._tabs.findIndex((tab) => tab.value.panel.id === id);
}
isActive(tab: Tab): boolean {
return (
this.selectedIndex > -1 &&
this._tabs[this.selectedIndex].value === tab
);
}
setActivePanel(panel: IDockviewPanel): void {
let runningWidth = 0;
for (const tab of this._tabs) {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
if (isActivePanel) {
const element = tab.value.element;
const parentElement = element.parentElement!;
if (
runningWidth < parentElement.scrollLeft ||
runningWidth + element.clientWidth >
parentElement.scrollLeft + parentElement.clientWidth
) {
parentElement.scrollLeft = runningWidth;
}
}
runningWidth += tab.value.element.clientWidth;
}
}
openPanel(panel: IDockviewPanel, index: number = this._tabs.length): void {
if (this._tabs.find((tab) => tab.value.panel.id === panel.id)) {
return;
}
const tab = new Tab(panel, this.accessor, this.group);
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onPointerDown((event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' &&
this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = tab.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(panel as DockviewPanel, {
x: left - rootLeft,
y: top - rootTop,
inDragMode: true,
});
return;
}
switch (event.button) {
case 0: // left click or touch
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
break;
}
}),
tab.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this._tabs.findIndex((x) => x.value === tab),
});
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
})
);
const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index);
}
delete(id: string): void {
const index = this.indexOf(id);
const tabToRemove = this._tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
private addTab(
tab: IValueDisposable<Tab>,
index: number = this._tabs.length
): void {
if (index < 0 || index > this._tabs.length) {
throw new Error('invalid location');
}
this._tabsList.insertBefore(
tab.value.element,
this._tabsList.children[index]
);
this._tabs = [
...this._tabs.slice(0, index),
tab,
...this._tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
}
private toggleDropdown(options: { reset: boolean }): void {
const tabs = options.reset
? []
: this._tabs
.filter(
(tab) =>
!isChildEntirelyVisibleWithinParent(
tab.value.element,
this._tabsList
)
)
.map((x) => x.value.panel.id);
this._onOverflowTabsChange.fire({ tabs, reset: options.reset });
}
}

View File

@ -1,4 +1,4 @@
.dv-tabs-and-actions-container {
.tabs-and-actions-container {
display: flex;
background-color: var(--dv-tabs-and-actions-container-background-color);
flex-shrink: 0;
@ -6,32 +6,70 @@
height: var(--dv-tabs-and-actions-container-height);
font-size: var(--dv-tabs-and-actions-container-font-size);
&.dv-single-tab.dv-full-width-single-tab {
.dv-scrollable {
flex-grow: 1;
}
.dv-tabs-container {
flex-grow: 1;
.dv-tab {
flex-grow: 1;
padding: 0px;
}
}
.dv-void-container {
flex-grow: 0;
}
&.hidden {
display: none;
}
.dv-void-container {
&.dv-single-tab.dv-full-width-single-tab {
.tabs-container {
flex-grow: 1;
.tab {
flex-grow: 1;
}
}
.void-container {
flex-grow: 0;
}
}
.void-container {
display: flex;
flex-grow: 1;
cursor: grab;
}
.dv-right-actions-container {
.tabs-container {
display: flex;
overflow-x: overlay;
overflow-y: hidden;
scrollbar-width: thin; // firefox
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
.tab {
-webkit-user-drag: element;
outline: none;
min-width: 75px;
cursor: pointer;
position: relative;
box-sizing: border-box;
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
}
}

View File

@ -1,58 +1,36 @@
import {
IDisposable,
CompositeDisposable,
Disposable,
MutableDisposable,
IValueDisposable,
} from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { Tab } from '../tab/tab';
import { ITab, Tab } from '../tab/tab';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer';
import { findRelativeZIndexParent, toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import {
createDropdownElementHandle,
DropdownElement,
} from './tabOverflowControl';
import { toggleClass } from '../../../dom';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
export interface TabDropIndexEvent {
readonly event: DragEvent;
readonly index: number;
}
export interface TabDragEvent {
readonly nativeEvent: DragEvent;
readonly panel: IDockviewPanel;
}
export interface GroupDragEvent {
readonly nativeEvent: DragEvent;
readonly group: DockviewGroupPanel;
}
export interface ITabsContainer extends IDisposable {
readonly element: HTMLElement;
readonly panels: string[];
readonly size: number;
readonly onDrop: Event<TabDropIndexEvent>;
readonly onTabDragStart: Event<TabDragEvent>;
readonly onGroupDragStart: Event<GroupDragEvent>;
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent>;
hidden: boolean;
delete(id: string): void;
indexOf(id: string): number;
setActive(isGroupActive: boolean): void;
setActivePanel(panel: IDockviewPanel): void;
isActive(tab: Tab): boolean;
closePanel(panel: IDockviewPanel): void;
openPanel(panel: IDockviewPanel, index?: number): void;
delete: (id: string) => void;
indexOf: (id: string) => number;
onDrop: Event<TabDropIndexEvent>;
setActive: (isGroupActive: boolean) => void;
setActivePanel: (panel: IDockviewPanel) => void;
isActive: (tab: ITab) => boolean;
closePanel: (panel: IDockviewPanel) => void;
openPanel: (panel: IDockviewPanel, index?: number) => void;
setRightActionsElement(element: HTMLElement | undefined): void;
setLeftActionsElement(element: HTMLElement | undefined): void;
setPrefixActionsElement(element: HTMLElement | undefined): void;
hidden: boolean;
show(): void;
hide(): void;
}
@ -62,44 +40,27 @@ export class TabsContainer
implements ITabsContainer
{
private readonly _element: HTMLElement;
private readonly tabs: Tabs;
private readonly tabContainer: HTMLElement;
private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex = -1;
private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined;
private preActions: HTMLElement | undefined;
private _hidden = false;
private dropdownPart: DropdownElement | null = null;
private _overflowTabs: string[] = [];
private readonly _dropdownDisposable = new MutableDisposable();
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
get onTabDragStart(): Event<TabDragEvent> {
return this.tabs.onTabDragStart;
}
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> =
this._onGroupDragStart.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
get panels(): string[] {
return this.tabs.panels;
return this.tabs.map((_) => _.value.panelId);
}
get size(): number {
return this.tabs.size;
return this.tabs.length;
}
get hidden(): boolean {
@ -111,118 +72,6 @@ export class TabsContainer
this.element.style.display = value ? 'none' : '';
}
get element(): HTMLElement {
return this._element;
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-tabs-and-actions-container';
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'dv-right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'dv-left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'dv-pre-actions-container';
this.tabs = new Tabs(group, accessor, {
showTabsOverflowControl: !accessor.options.disableTabsOverflowList,
});
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabs.element);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
this.addDisposables(
this.tabs.onDrop((e) => this._onDrop.fire(e)),
this.tabs.onWillShowOverlay((e) => this._onWillShowOverlay.fire(e)),
accessor.onDidOptionsChange(() => {
this.tabs.showTabsOverflowControl =
!accessor.options.disableTabsOverflowList;
}),
this.tabs.onOverflowTabsChange((event) => {
this.toggleDropdown(event);
}),
this.tabs,
this._onWillShowOverlay,
this._onDrop,
this._onGroupDragStart,
this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.size,
});
}),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
}),
addDisposableListener(
this.voidContainer.element,
'pointerdown',
(event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating'
) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(this.group, {
x: left - rootLeft + 20,
y: top - rootTop + 20,
inDragMode: true,
});
}
}
)
);
}
show(): void {
if (!this.hidden) {
this.element.style.display = '';
@ -261,143 +110,259 @@ export class TabsContainer
}
}
setPrefixActionsElement(element: HTMLElement | undefined): void {
if (this.preActions === element) {
return;
}
if (this.preActions) {
this.preActions.remove();
this.preActions = undefined;
}
if (element) {
this.preActionsContainer.appendChild(element);
this.preActions = element;
}
get element(): HTMLElement {
return this._element;
}
isActive(tab: Tab): boolean {
return this.tabs.isActive(tab);
public isActive(tab: ITab): boolean {
return (
this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab
);
}
indexOf(id: string): number {
return this.tabs.indexOf(id);
public indexOf(id: string): number {
return this.tabs.findIndex((tab) => tab.value.panelId === id);
}
setActive(_isGroupActive: boolean) {
// noop
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
delete(id: string): void {
this.tabs.delete(id);
this.updateClassnames();
}
this.addDisposables(this._onDrop);
setActivePanel(panel: IDockviewPanel): void {
this.tabs.setActivePanel(panel);
}
this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container';
openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void {
this.tabs.openPanel(panel, index);
this.updateClassnames();
}
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
closePanel(panel: IDockviewPanel): void {
this.delete(panel.id);
}
this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
})
);
private updateClassnames(): void {
toggleClass(this._element, 'dv-single-tab', this.size === 1);
}
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'right-actions-container';
private toggleDropdown(options: { tabs: string[]; reset: boolean }): void {
const tabs = options.reset ? [] : options.tabs;
this._overflowTabs = tabs;
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'left-actions-container';
if (this._overflowTabs.length > 0 && this.dropdownPart) {
this.dropdownPart.update({ tabs: tabs.length });
return;
}
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
if (this._overflowTabs.length === 0) {
this._dropdownDisposable.dispose();
return;
}
this.voidContainer = new VoidContainer(this.accessor, this.group);
const root = document.createElement('div');
root.className = 'dv-tabs-overflow-dropdown-root';
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
const part = createDropdownElementHandle();
part.update({ tabs: tabs.length });
this.dropdownPart = part;
root.appendChild(part.element);
this.rightActionsContainer.prepend(root);
this._dropdownDisposable.value = new CompositeDisposable(
Disposable.from(() => {
root.remove();
this.dropdownPart?.dispose?.();
this.dropdownPart = null;
this.addDisposables(
this.voidContainer,
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.length,
});
}),
addDisposableListener(
root,
'pointerdown',
this.voidContainer.element,
'mousedown',
(event) => {
event.preventDefault();
},
{ capture: true }
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
!this.group.api.isFloating
) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(
this.group,
{
x: left - rootLeft + 20,
y: top - rootTop + 20,
},
{ inDragMode: true }
);
}
}
),
addDisposableListener(root, 'click', (event) => {
const el = document.createElement('div');
el.style.overflow = 'auto';
el.className = 'dv-tabs-overflow-container';
for (const tab of this.tabs.tabs.filter((tab) =>
this._overflowTabs.includes(tab.panel.id)
)) {
const panelObject = this.group.panels.find(
(panel) => panel === tab.panel
)!;
const tabComponent =
panelObject.view.createTabRenderer('headerOverflow');
const child = tabComponent.element;
const wrapper = document.createElement('div');
toggleClass(wrapper, 'dv-tab', true);
toggleClass(
wrapper,
'dv-active-tab',
panelObject.api.isActive
);
toggleClass(
wrapper,
'dv-inactive-tab',
!panelObject.api.isActive
);
wrapper.addEventListener('pointerdown', () => {
this.accessor.popupService.close();
tab.element.scrollIntoView();
tab.panel.api.setActive();
});
wrapper.appendChild(child);
el.appendChild(wrapper);
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
}
const relativeParent = findRelativeZIndexParent(root);
const isLeftClick = event.button === 0;
this.accessor.popupService.openPopover(el, {
x: event.clientX,
y: event.clientY,
zIndex: relativeParent?.style.zIndex
? `calc(${relativeParent.style.zIndex} * 2)`
: undefined,
});
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
})
);
}
public setActive(_isGroupActive: boolean) {
// noop
}
private addTab(
tab: IValueDisposable<ITab>,
index: number = this.tabs.length
): void {
if (index < 0 || index > this.tabs.length) {
throw new Error('invalid location');
}
this.tabContainer.insertBefore(
tab.value.element,
this.tabContainer.children[index]
);
this.tabs = [
...this.tabs.slice(0, index),
tab,
...this.tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
}
public delete(id: string): void {
const index = this.tabs.findIndex((tab) => tab.value.panelId === id);
const tabToRemove = this.tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
public setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.panelId;
tab.value.setActive(isActivePanel);
});
}
public openPanel(
panel: IDockviewPanel,
index: number = this.tabs.length
): void {
if (this.tabs.find((tab) => tab.value.panelId === panel.id)) {
return;
}
const tabToAdd = new Tab(panel.id, this.accessor, this.group);
if (!panel.view?.tab) {
throw new Error('invalid header component');
}
tabToAdd.setContent(panel.view.tab);
const disposable = CompositeDisposable.from(
tabToAdd.onChanged((event) => {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.isFloating && this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);
const { top, left } =
tabToAdd.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(
panel as DockviewPanel,
{
x: left - rootLeft,
y: top - rootTop,
},
{ inDragMode: true }
);
return;
}
const alreadyFocused =
panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused;
const isLeftClick = event.button === 0;
if (!isLeftClick || event.defaultPrevented) {
return;
}
this.group.model.openPanel(panel, {
skipFocus: alreadyFocused,
});
}),
tabToAdd.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.findIndex((x) => x.value === tabToAdd),
});
})
);
const value: IValueDisposable<ITab> = { value: tabToAdd, disposable };
this.addTab(value, index);
}
public closePanel(panel: IDockviewPanel): void {
this.delete(panel.id);
}
public dispose(): void {
super.dispose();
for (const { value, disposable } of this.tabs) {
disposable.dispose();
value.dispose();
}
this.tabs = [];
}
}

View File

@ -1,28 +1,20 @@
import { last } from '../../../array';
import { getPanelData } from '../../../dnd/dataTransfer';
import {
Droptarget,
DroptargetEvent,
WillShowOverlayEvent,
} from '../../../dnd/droptarget';
import { Droptarget, DroptargetEvent } from '../../../dnd/droptarget';
import { GroupDragHandler } from '../../../dnd/groupDragHandler';
import { DockviewComponent } from '../../dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
import { DockviewDropTargets } from '../../types';
export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly dropTraget: Droptarget;
private readonly voidDropTarget: Droptarget;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event;
readonly onWillShowOverlay: Event<WillShowOverlayEvent>;
get element(): HTMLElement {
return this._element;
}
@ -35,48 +27,51 @@ export class VoidContainer extends CompositeDisposable {
this._element = document.createElement('div');
this._element.className = 'dv-void-container';
this._element.className = 'void-container';
this._element.tabIndex = 0;
this._element.draggable = true;
this.addDisposables(
this._onDrop,
this._onDragStart,
addDisposableListener(this._element, 'pointerdown', () => {
addDisposableListener(this._element, 'click', () => {
this.accessor.doSetGroupActive(this.group);
})
);
const handler = new GroupDragHandler(this._element, accessor, group);
const handler = new GroupDragHandler(this._element, accessor.id, group);
this.dropTraget = new Droptarget(this._element, {
this.voidDropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
return true;
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.group.panels)?.id !== data.panelId;
}
return group.model.canDisplayOverlay(
event,
position,
'header_space'
DockviewDropTargets.Panel
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;
this.addDisposables(
handler,
handler.onDragStart((event) => {
this._onDragStart.fire(event);
}),
this.dropTraget.onDrop((event) => {
this.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event);
}),
this.dropTraget
this.voidDropTarget
);
}
}

View File

@ -1,4 +1,45 @@
.dv-watermark {
.watermark {
display: flex;
height: 100%;
width: 100%;
&.has-actions {
.watermark-title {
.actions-container {
display: none;
}
}
}
.watermark-title {
height: 35px;
width: 100%;
display: flex;
}
.watermark-content {
flex-grow: 1;
}
.actions-container {
display: flex;
align-items: center;
padding: 0px 8px;
// padding: 0px;
// margin: 0px;
// justify-content: flex-end;
.close-action {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
color: var(--dv-activegroup-hiddenpanel-tab-color);
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}
}

View File

@ -2,13 +2,21 @@ import {
IWatermarkRenderer,
WatermarkRendererInitParameters,
} from '../../types';
import { addDisposableListener } from '../../../events';
import { toggleClass } from '../../../dom';
import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { PanelUpdateEvent } from '../../../panel/types';
import { createCloseButton } from '../../../svg';
import { DockviewApi } from '../../../api/component.api';
export class Watermark
extends CompositeDisposable
implements IWatermarkRenderer
{
private readonly _element: HTMLElement;
private _element: HTMLElement;
private _group: DockviewGroupPanel | undefined;
private _api: DockviewApi | undefined;
get element(): HTMLElement {
return this._element;
@ -17,10 +25,70 @@ export class Watermark
constructor() {
super();
this._element = document.createElement('div');
this._element.className = 'dv-watermark';
this._element.className = 'watermark';
const title = document.createElement('div');
title.className = 'watermark-title';
const emptySpace = document.createElement('span');
emptySpace.style.flexGrow = '1';
const content = document.createElement('div');
content.className = 'watermark-content';
this._element.appendChild(title);
this._element.appendChild(content);
const actionsContainer = document.createElement('div');
actionsContainer.className = 'actions-container';
const closeAnchor = document.createElement('div');
closeAnchor.className = 'close-action';
closeAnchor.appendChild(createCloseButton());
actionsContainer.appendChild(closeAnchor);
title.appendChild(emptySpace);
title.appendChild(actionsContainer);
this.addDisposables(
addDisposableListener(closeAnchor, 'click', (ev) => {
ev.preventDefault();
if (this._group) {
this._api?.removeGroup(this._group);
}
})
);
}
update(_event: PanelUpdateEvent): void {
// noop
}
focus(): void {
// noop
}
layout(_width: number, _height: number): void {
// noop
}
init(_params: WatermarkRendererInitParameters): void {
// noop
this._api = _params.containerApi;
this.render();
}
updateParentGroup(group: DockviewGroupPanel, _visible: boolean): void {
this._group = group;
this.render();
}
dispose(): void {
super.dispose();
}
private render(): void {
const isOneGroup = !!(this._api && this._api.size <= 1);
toggleClass(this.element, 'has-actions', isOneGroup);
}
}

View File

@ -21,7 +21,7 @@ interface LegacyState extends GroupviewPanelState {
}
export class DefaultDockviewDeserialzier implements IPanelDeserializer {
constructor(private readonly accessor: DockviewComponent) {}
constructor(private readonly layout: DockviewComponent) {}
public fromJSON(
panelData: GroupviewPanelState,
@ -35,13 +35,13 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
const contentComponent = viewData
? viewData.content.id
: panelData.contentComponent ?? 'unknown';
: panelData.contentComponent || 'unknown';
const tabComponent = viewData
? viewData.tab?.id
: panelData.tabComponent;
const view = new DockviewPanelModel(
this.accessor,
this.layout,
panelId,
contentComponent,
tabComponent
@ -49,24 +49,15 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
const panel = new DockviewPanel(
panelId,
contentComponent,
tabComponent,
this.accessor,
new DockviewApi(this.accessor),
this.layout,
new DockviewApi(this.layout),
group,
view,
{
renderer: panelData.renderer,
minimumWidth: panelData.minimumWidth,
minimumHeight: panelData.minimumHeight,
maximumWidth: panelData.maximumWidth,
maximumHeight: panelData.maximumHeight,
}
view
);
panel.init({
title: title ?? panelId,
params: params ?? {},
title: title || panelId,
params: params || {},
});
return panel;

View File

@ -10,46 +10,38 @@
width: 100%;
z-index: 1;
}
.dv-overlay-render-container {
position: relative;
}
}
.dv-groupview {
&.dv-active-group {
> .dv-tabs-and-actions-container {
.dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
.groupview {
&.active-group {
> .tabs-and-actions-container > .tabs-container > .tab {
&.active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
&.inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}
}
&.dv-inactive-group {
> .dv-tabs-and-actions-container {
.dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
}
&.inactive-group {
> .tabs-and-actions-container > .tabs-container > .tab {
&.active-tab {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
}
&.inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
}
}
}
@ -59,7 +51,7 @@
* when a tab is dragged we lose the above stylings because they are conditional on parent elements
* therefore we also set some stylings for the dragging event
**/
.dv-tab {
.tab {
&.dv-tab-dragging {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color

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