Compare commits

..

390 Commits

Author SHA1 Message Date
mathuo
1e63da807b
chore(release): publish v4.2.5 2025-05-01 17:59:15 +01:00
mathuo
68d7947dea
chore: v4.2.5 docs 2025-05-01 17:58:55 +01:00
mathuo
093a034738
Merge pull request #921 from mathuo/883-tab-overflow-dropdown-doesnt-collide-with-edge-of-screen
883 tab overflow dropdown doesnt collide with edge of screen
2025-04-30 20:43:39 +01:00
mathuo
f9ca5b99d5
Merge pull request #920 from mathuo/891-first-tab-does-not-drag-drop-when-there-is-not-enough-space-above-the-dockview-area
bug: fix ghost image rendering
2025-04-30 20:43:16 +01:00
mathuo
420466398d
bug: popout service overflow handle 2025-04-29 22:01:27 +01:00
mathuo
20b5c844f3
Merge branch 'master' of https://github.com/mathuo/dockview into 883-tab-overflow-dropdown-doesnt-collide-with-edge-of-screen 2025-04-29 22:00:44 +01:00
mathuo
6d2f2f1012
bug: fix ghost image rendering 2025-04-29 21:45:23 +01:00
mathuo
3ddea55834
chore(release): publish v4.2.4 2025-04-29 21:01:28 +01:00
mathuo
2f334c63c1
chore: v4.2.4 docs 2025-04-29 21:01:04 +01:00
mathuo
8334f9795c
Merge pull request #917 from mathuo/913-returning-group-in-popout-to-main-window-via-drag-creates-empty-space-ghost-group
bug: ghost groups appearing
2025-04-28 21:32:47 +01:00
mathuo
7ea3154946
Merge pull request #828 from dzek69/patch-1
Fix provenance npm link
2025-04-28 21:27:28 +01:00
mathuo
1ef3dc06ce
Merge pull request #827 from AlexErrant/patch-1
drop extraneous <BrowserHeader />
2025-04-28 21:25:47 +01:00
mathuo
1ff74f2e65
Merge pull request #916 from mathuo/903-popover-overflow-when-hovering-right-aligned-more-icon-in-panel-group
903 popover overflow when hovering right aligned more icon in panel group
2025-04-28 21:24:40 +01:00
mathuo
a82aac09bc
bug: ghost groups appearing 2025-04-23 20:57:35 +01:00
mathuo
d1239a5ee5
bug: popup within floating panel 2025-04-23 20:35:46 +01:00
mathuo
22deb851dc
bug: popup overflow 2025-04-15 19:24:23 +01:00
mathuo
87f257df1e
chore(release): publish v4.2.3 2025-04-08 18:23:57 +01:00
mathuo
a92a056981
chore: correct versions 2025-04-08 18:23:44 +01:00
mathuo
c3e203f420
chore: update lerna 2025-04-08 18:22:33 +01:00
mathuo
8bdd15161c
chore: v4.2.3 docs 2025-04-08 18:19:12 +01:00
mathuo
8a3a7e5063
chore(release): publish v4.2.2 2025-03-26 22:15:46 +00:00
mathuo
65d95f1271
chore: v4.2.2 docs 2025-03-26 22:15:14 +00:00
mathuo
3ec6627013
Merge pull request #892 from mathuo/889-splitviewreact-and-gridviewreact-encounter-error-invalid-operation-resource-is-already-disposed
bug: remove reference to component once disposed preventing further u…
2025-03-26 22:14:59 +00:00
mathuo
c0227d620c
bug: remove reference to component once disposed preventing further usage 2025-03-26 22:08:31 +00:00
mathuo
aad168da84
chore(release): publish v4.2.1 2025-03-18 19:52:39 +00:00
mathuo
dfee4111a2
chore: v4.2.1 docs 2025-03-18 19:46:05 +00:00
mathuo
712f3f860d
Merge pull request #888 from mathuo/886-drag-and-drop-broken-after-latest-version-420
bug: fix options init
2025-03-18 19:45:23 +00:00
mathuo
f545a0a5c7
Merge pull request #887 from mathuo/884-cant-drop-tabs-between-panels-and-some-other-issues-with-v4-1
bug: dnd issues
2025-03-18 19:40:21 +00:00
mathuo
e3d39fd2ed
bug: fix options init 2025-03-18 19:38:32 +00:00
mathuo
dca0eb0df7
bug: dnd issues 2025-03-18 19:32:54 +00:00
mathuo
8188fd9429
chore(release): publish v4.2.0 2025-03-17 19:49:00 +00:00
mathuo
54a51ed10b
Merge branch 'master' of https://github.com/mathuo/dockview 2025-03-17 19:48:49 +00:00
mathuo
1a11a4ba2c
chore: docs 2025-03-17 19:48:37 +00:00
mathuo
b6d83df1aa
chore: v4.2.0 docs 2025-03-17 19:47:34 +00:00
mathuo
6444e4cb7d
Merge pull request #885 from mathuo/884-cant-drop-tabs-between-panels-and-some-other-issues-with-v4
feat: disableCustomScrollbars option
2025-03-17 19:47:13 +00:00
mathuo
ca7bd0a86d
bug: popover z-index 2025-03-17 19:29:06 +00:00
mathuo
dc828b7658
feat: disableCustomScrollbars option 2025-03-17 19:28:36 +00:00
mathuo
6e02e59fec
chore: v4.1.0 docs 2025-03-16 21:09:38 +00:00
mathuo
f79bbe0a6a
chore(release): publish v4.1.0 2025-03-16 21:08:08 +00:00
mathuo
12448e006f
chore: v4.1.0 docs 2025-03-16 21:07:51 +00:00
mathuo
5e380affe7
Merge pull request #882 from mathuo/676-is-it-possiable-that-the-id-and-title-of-group-can-be-assigned
676 is it possiable that the id and title of group can be assigned
2025-03-16 21:01:18 +00:00
mathuo
a44fc01f34
Merge branch 'master' of https://github.com/mathuo/dockview into 676-is-it-possiable-that-the-id-and-title-of-group-can-be-assigned 2025-03-16 20:48:41 +00:00
mathuo
8e03e584a7
Merge pull request #876 from mathuo/857-ondidrepositionpopoutgroup-event
feat: popout group resize events
2025-03-16 20:45:40 +00:00
mathuo
1ddcaefe80
test: add tests 2025-03-16 20:43:04 +00:00
mathuo
d086dfd081
Merge pull request #864 from dqhieuu/master
Make `id` option in addGroup without referenceGroup/referencePanel work
2025-03-16 20:40:54 +00:00
mathuo
b231367e44
Merge pull request #791 from mathuo/774-drag-and-drop-not-working-for-iframe-panels-within-a-shadow-dom
bug: disable iframes within shadowdom during dnd
2025-03-16 20:33:07 +00:00
mathuo
bba22fedd4
feat: popout group resize events 2025-03-16 20:28:35 +00:00
mathuo
3ebcb8eaef
Merge pull request #875 from mathuo/863-headercomponents-for-paneviewreact-is-never-used-and-cannot-set-height-because-harcoded-to-22px
feat: custom paneview header height
2025-03-16 20:26:33 +00:00
mathuo
39dd5f0759
feat: more efficient search 2025-03-16 20:23:39 +00:00
mathuo
e4f07dfbda
Merge pull request #881 from mathuo/721-loading-of-splitview-state-from-json-adds-extra-div-containers-that-break-layout
bug: remove splitview dom element
2025-03-16 20:09:31 +00:00
mathuo
d30263af67
bug: remove splitview dom element 2025-03-14 22:12:22 +00:00
mathuo
54ae457ccd
chore(release): publish v4.0.1 2025-03-14 22:01:56 +00:00
mathuo
416039bc99
chore: v4.0.1 docs 2025-03-14 21:59:59 +00:00
mathuo
e8fdabba08
Merge pull request #880 from mathuo/878-fullwidth-tabs-are-not-rendered-full-width-in-40
bug: fix full-width css
2025-03-14 21:59:08 +00:00
mathuo
9373802115
Merge pull request #879 from mathuo/877-missing-tab-divider-in-40
bug: fix tab divider css
2025-03-14 21:59:03 +00:00
mathuo
759dcc7b05
bug: fix full-width css 2025-03-14 21:57:37 +00:00
mathuo
f8faca731d
bug: fix tab divider css 2025-03-14 21:55:33 +00:00
mathuo
d168356344
Merge branch 'master' of https://github.com/mathuo/dockview into 774-drag-and-drop-not-working-for-iframe-panels-within-a-shadow-dom 2025-03-12 21:23:46 +00:00
mathuo
64e11a880c
bug: disable iframes within shadowdom during dnd 2025-03-12 21:16:21 +00:00
mathuo
a306742508
feat: custom paneview header height 2025-03-12 21:03:05 +00:00
mathuo
bffd2dea89
chore(release): publish v4.0.0 2025-03-12 20:25:13 +00:00
mathuo
ac92d59d0a
chore: v4.0.0 docs 2025-03-12 20:24:59 +00:00
mathuo
21d94b654b
chore: generate docs 2025-03-12 20:18:27 +00:00
mathuo
1030bdb778
feat: rename theme property 2025-03-12 20:16:57 +00:00
mathuo
082b811d20
Merge pull request #870 from mathuo/866-splitviewreact-rendered-twice-in-react-strict-mode
bug: remove element after dispose
2025-03-12 20:06:31 +00:00
mathuo
4eff83e9a0
bug: remove element after dispose 2025-03-10 21:35:17 +00:00
mathuo
090f2d26f8
chore: docs 2025-03-10 21:28:33 +00:00
mathuo
e34fb43913
chore: theme docs 2025-03-10 21:25:11 +00:00
mathuo
c0119d65c0
chore: fixup docs 2025-03-10 21:17:33 +00:00
mathuo
d3d57b62b2
Merge pull request #822 from mathuo/610-feature-request-dropdown-menu-to-handle-overflow-tabs-4
tabs overflow menu
2025-03-03 21:52:23 +00:00
mathuo
96d6947aa6
feat: scrollbars 2025-03-03 21:45:32 +00:00
mathuo
5fb13dd8a2
chore: update sonarqube build version 2025-03-03 20:47:54 +00:00
mathuo
cfe37766a9
feat: scrollbars 2025-03-03 20:47:44 +00:00
mathuo
7a6b2cb26d
feat: tab panel overflow dropdown 2025-02-27 21:15:38 +00:00
Hieu
6511b8d936
Make addGroup wiithout reference panel/group allow id option 2025-02-24 04:54:01 +07:00
mathuo
5754ddc3f4
Merge branch 'master' of https://github.com/mathuo/dockview into 610-feature-request-dropdown-menu-to-handle-overflow-tabs-4 2025-02-23 20:19:12 +00:00
mathuo
eec57c1cde
Merge pull request #850 from mathuo/849-improved-dnd-overlay-model
feat: improved dnd model
2025-02-23 20:16:37 +00:00
mathuo
3530c9896e
Merge branch 'master' of https://github.com/mathuo/dockview into 849-improved-dnd-overlay-model 2025-02-23 20:09:03 +00:00
mathuo
d811ca6554
feat: improved dnd model 2025-02-23 19:53:43 +00:00
mathuo
ab965ca726
chore(release): publish v3.2.0 2025-02-12 21:29:30 +00:00
mathuo
b13c841416
chore: v3.2.0 docs 2025-02-12 21:29:04 +00:00
mathuo
773bf5a53c
Merge pull request #860 from mathuo/859-added-support-for-sash-delay-and-duration-customization
859 added support for sash delay and duration customization
2025-02-12 21:28:34 +00:00
mathuo
837fabac86
feat: active sash animation css properties 2025-02-12 21:22:19 +00:00
mathuo
21caabce60
Merge pull request #858 from amirmamaghani/master
Added support for sash delay and duration customization
2025-02-12 21:15:52 +00:00
mathuo
14b918b7ae
chore: enhance examples page 2025-02-12 21:01:36 +00:00
Amir
f98be640ce Added support for sash delay and duration customization 2025-02-12 14:54:41 +01:00
mathuo
cd1aa81d93
Merge pull request #856 from mathuo/833-vanilla-js-expand-header-buttons
chore: add template example
2025-02-09 22:03:05 +00:00
mathuo
490fc05baf
chore: add template example 2025-02-09 14:49:01 +00:00
mathuo
16373429a8
chore(release): publish v3.1.1 2025-02-09 14:40:58 +00:00
mathuo
59fba25ade
Merge branch 'master' of https://github.com/mathuo/dockview 2025-02-09 14:40:47 +00:00
mathuo
cdd0183fa9
chore: v3.1.1 docs 2025-02-09 14:40:29 +00:00
mathuo
98cbe377c2
Merge pull request #855 from mathuo/853-middle-button-click---closes-tabs-with-hideclose
conditional middle-btn tab close
2025-02-09 14:38:46 +00:00
mathuo
d8da2f29c3
conditional middle-btn tab close 2025-02-08 15:38:31 +00:00
mathuo
8ed0fb5586
chore : v3.1.0 docs 2025-02-02 22:31:11 +00:00
mathuo
a56f01215b
chore : v3.1.0 docs 2025-02-02 22:30:58 +00:00
mathuo
e71d8b1d09
chore(release): publish v3.1.0 2025-02-02 22:30:35 +00:00
mathuo
71b99541b3
chore: v3.0.3 docs 2025-02-02 20:57:55 +00:00
mathuo
0e3afcf83f
Merge pull request #847 from mathuo/830-cannot-close-tab-with-middle-mouse-button
feat: close tab with middle btn
2025-01-31 22:16:59 +00:00
mathuo
1a85444b81
Merge pull request #848 from mathuo/835-crash-on-navigation-with-open-popup
835 crash on navigation with open popup
2025-01-31 22:16:40 +00:00
mathuo
19a22c49c3
bug: popup disposal runs after instance dispose 2025-01-30 20:40:18 +00:00
mathuo
06f02ba411
Merge pull request #845 from chris-mrl/fix-crash-open-popout-navigation
check tabToRemove is not undefined before access
2025-01-30 20:37:09 +00:00
mathuo
0e9c648fa9
feat: close tab with middle btn 2025-01-30 20:30:22 +00:00
mathuo
bb93c9e4c8
Merge pull request #843 from mathuo/842-api-init-bug
bug: setup onDidAcitvePanelChange subscription quicker
2025-01-28 20:50:49 +00:00
mathuo
c6dcef537f
bug: setup onDidAcitvePanelChange subscription quicker 2025-01-28 20:30:38 +00:00
Chris Nolan
3e409c4265
check tabToRemove is not undefined before access 2025-01-28 08:15:19 +00:00
mathuo
ca09ae537d
Create SECURITY.md 2025-01-27 22:35:43 +00:00
mathuo
e9985df262
Merge pull request #831 from milomg/patch-1
Fix vertical sash in replit theme
2025-01-27 19:05:30 +00:00
Milo Mighdoll
dc6bfc67e3
Fix vertical sash in replit theme 2025-01-20 12:42:31 -05:00
mathuo
5ca5ffac8d
tmp 2025-01-19 20:00:26 +00:00
mathuo
723272d6de
Merge branch 'master' of https://github.com/mathuo/dockview into 610-feature-request-dropdown-menu-to-handle-overflow-tabs-4 2025-01-14 21:24:37 +00:00
mathuo
6421a72653
chore: use dockview-core package metrics in README
Some checks failed
CI / build (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-14 18:01:59 +00:00
Jacek Nowacki
0c1b8ce4d7
Fix provenance npm link 2025-01-12 12:12:26 +01:00
Alex Errant
f40532f272
drop extraneous <BrowserHeader /> 2025-01-11 16:17:34 -06:00
mathuo
6b75b5093c
chore: fix docs 2025-01-11 21:04:44 +00:00
mathuo
c0cbe89835
chore(release): publish v3.0.2 2025-01-11 15:12:57 +00:00
mathuo
b46a473cdf
chore: v3.0.2 release notes 2025-01-11 15:00:00 +00:00
mathuo
239c99ffe5
chore: fix docs 2025-01-11 14:58:06 +00:00
mathuo
6678ae24e0
Merge pull request #825 from mathuo/818-container-with-theme-class-is-created-twice-1
bug: duplicate container HTML Element
2025-01-11 14:57:56 +00:00
mathuo
680eaca96c
Merge pull request #824 from mathuo/797-popout-group-flow-error-1
bug: fix floating->popout->floating group transition
2025-01-11 14:36:14 +00:00
mathuo
3d23a2d352
Merge branch 'master' of https://github.com/mathuo/dockview into 818-container-with-theme-class-is-created-twice-1 2025-01-11 14:34:28 +00:00
mathuo
c7c1ae9238
Merge pull request #816 from zaxer2/Making-React-Dockview-Demo-work-nice-with-Strict-Mode
Fixing duplicate state in React-Dockview-Demo caused by strict mode
2025-01-11 14:33:28 +00:00
mathuo
c122bfd310
fixup demo wrt React.StrictMode 2025-01-10 22:09:24 +00:00
mathuo
1a3c6ea7db
bug: duplicate container HTML Element 2025-01-10 22:08:37 +00:00
mathuo
c6865c691c
bug: fix floating->popout->floating group transition 2025-01-09 20:42:19 +00:00
mathuo
872ec7cba9
chore(release): publish v3.0.1 2025-01-09 20:17:19 +00:00
mathuo
2a3647db28
chore: v3.0.1 docs 2025-01-09 20:16:59 +00:00
mathuo
2f9af48146
Merge pull request #819 from mathuo/818-container-with-theme-class-is-created-twice
bug: duplicate root container
2025-01-09 20:07:39 +00:00
mathuo
04e4ea1a70
tabs overflow menu 2025-01-06 21:11:49 +00:00
mathuo
2d8d38b466
bug: duplicate root container 2025-01-06 20:14:14 +00:00
zaxer2
10fd5778dc
Copying Sandbox change to corresponding Template
see prev. commit
2025-01-03 13:24:28 -05:00
zaxer2
cd91f4a4d3
Updated react dockview-demo CodeSandbox to reset relevant state on subsequent onReady calls
Strict Mode causes the Dockview component to render twice, causing onReady to be called twice, which puts two copies of DefaultLayout in state without resetting between calls.

A most-ideal solution would probably be to expose an onUnload hook or something like that, to be called after one onReady but before the next. Then, the state resets could be called in that hook. but this fix will at least ensure the Dockview-Demo codesandbox behaves nicely for the time being.
2025-01-03 13:22:07 -05:00
mathuo
148a05a201
chore(release): publish v3.0.0 2024-12-29 14:17:33 +00:00
mathuo
8359596be0
chore: v3.0.0 docs 2024-12-29 14:17:13 +00:00
mathuo
cb45889aca
chore: sonar fixes 2024-12-29 13:55:54 +00:00
mathuo
5e116c80b4
Merge pull request #812 from mathuo/809-standardize-framework-components-1
chore: align api with existing components
2024-12-28 14:37:24 +00:00
mathuo
20eb30ed1c
chore: docs config 2024-12-28 14:30:30 +00:00
mathuo
5173922c69
chore: docs 2024-12-28 14:29:48 +00:00
mathuo
0ca56eaa8a
chore: align api with existing components 2024-12-28 14:21:23 +00:00
mathuo
c14b66e5e5
Merge pull request #811 from mathuo/784-class-dv-single-tab-not-updated-on-drag-drop-of-tabs
bug: fix classname enablement
2024-12-27 22:38:23 +00:00
mathuo
cc5037d208
bug: fix classname enablement 2024-12-27 19:52:23 +00:00
mathuo
a53665adc5
chore: autogen docs 2024-12-23 20:04:14 +00:00
mathuo
d799b5bc89
Merge pull request #810 from mathuo/809-standardize-framework-components
feat: migrate to generic construction
2024-12-23 19:58:33 +00:00
mathuo
fade057412
feat: migrate to generic construction 2024-12-23 19:51:51 +00:00
mathuo
f6e7e4e390
Merge pull request #806 from mathuo/805-rename-class-dockview-react-part-to-dv-react-part
feat: rename class
2024-12-23 15:56:06 +00:00
mathuo
d33babd522
Merge pull request #808 from mathuo/807-dockview-vue-vue-peer-dependency
chore: vue3 peerDependency
2024-12-23 15:54:06 +00:00
mathuo
7df9c510bd
chore: clean up project dependencies 2024-12-23 15:53:20 +00:00
mathuo
3479828237
chore: vue3 peerDependency 2024-12-23 15:52:53 +00:00
mathuo
b3ce3e0366
feat: rename class 2024-12-22 19:24:20 +00:00
mathuo
32fa1511d8
chore(release): publish v2.1.4 2024-12-22 19:20:36 +00:00
mathuo
e1304ce694
chore: docs 2024-12-22 19:20:24 +00:00
mathuo
bdf81fd5b5
Merge branch 'master' of https://github.com/mathuo/dockview 2024-12-22 19:20:06 +00:00
mathuo
cd7984cb2b
Merge pull request #804 from mathuo/803-popout-overlay-container-incorrect-for-floating-group-return
bug: fix overlay container for popout to floating
2024-12-22 19:20:01 +00:00
mathuo
491872ea75
chore: v2.1.4 docs 2024-12-22 19:19:38 +00:00
mathuo
296b49180e
bug: fix overlay container for popout to floating 2024-12-22 19:14:14 +00:00
mathuo
487d02bd90
Merge pull request #802 from mathuo/801-disposable-fix
chore: adjust dockview disposable
2024-12-22 18:54:07 +00:00
mathuo
4d01d4e0af
chore: adjust dockview disposable 2024-12-22 18:47:29 +00:00
mathuo
8650de90aa
chore(release): publish v2.1.3 2024-12-22 10:17:37 +00:00
mathuo
e45d9e48e8
Merge pull request #800 from mathuo/799-popout-group-new-group-rendering-bug
bug: popout group to new group rendering bug fix
2024-12-22 10:17:18 +00:00
mathuo
da750e89d7
chore: v2.1.3 docs 2024-12-22 10:13:24 +00:00
mathuo
81a904ccbc
bug: popout group to new group rendering bug fix 2024-12-22 10:10:26 +00:00
mathuo
6016362c5c
chore: docs 2024-12-22 09:41:57 +00:00
mathuo
7fe2e43d43
chore(release): publish v2.1.2 2024-12-21 19:25:31 +00:00
mathuo
56d4dc7c74
chore: v2.1.2 docs 2024-12-21 19:25:06 +00:00
mathuo
6fbe2b4233
Merge pull request #798 from mathuo/797-popout-group-flow-error
bug: fixup popout group flows
2024-12-21 19:24:50 +00:00
mathuo
f749372eb2
chore: memory leak fixes and tests 2024-12-21 14:22:53 +00:00
mathuo
c216d70354
chore: comments 2024-12-21 14:00:57 +00:00
mathuo
0d32d285d2
chore: remove testing code 2024-12-21 13:59:24 +00:00
mathuo
5d868e63ce
feat: additional event sequencing checks 2024-12-21 13:58:11 +00:00
mathuo
7515b9b05d
bug: fixup popout group flows 2024-12-21 13:33:09 +00:00
mathuo
703418e27f
chore: typo 2024-12-20 20:07:58 +00:00
mathuo
53cdf42d84
chore(release): publish v2.1.1 2024-12-20 20:07:17 +00:00
mathuo
140a61f401
chore: v2.1.1 docs 2024-12-20 20:06:49 +00:00
mathuo
ff0bf75632
Merge pull request #796 from mathuo/795-popout-groups-bug
bug: only dispose of ref group if empty
2024-12-20 20:05:02 +00:00
mathuo
dd918294d1
bug: only dispose of ref group if empty 2024-12-19 22:05:59 +00:00
mathuo
684b0593e7
chore: update auto-gen docs 2024-12-19 21:33:55 +00:00
mathuo
13c4a65e18
chore(release): publish v2.1.0 2024-12-17 22:17:55 +00:00
mathuo
755135c5ad
chore: fix demo 2024-12-17 22:10:49 +00:00
mathuo
15bf4bbd1f
chore: v2.1.0 docs 2024-12-17 21:39:01 +00:00
mathuo
429177436e
Merge pull request #789 from mathuo/788-sonar-fixes
chore: adjust sonar parameters
2024-12-10 20:20:29 +00:00
mathuo
cf6891368e
chore: adjust sonar parameters 2024-12-10 19:54:23 +00:00
mathuo
e2302372b1
chore: fix sonar issues 2024-12-10 19:30:32 +00:00
mathuo
f13f009155
chore: adjust sonar parameters 2024-12-10 19:21:16 +00:00
mathuo
c5025424c7
Merge pull request #783 from mathuo/777-keeping-header-actions-even-when-no-tabs-are-open
feat: ensure group always exists
2024-12-10 19:20:31 +00:00
mathuo
a66fdae8a3
Merge pull request #762 from mathuo/494-unable-to-persist-fullscreen-maximized-mode
feat: serialization of maximized views
2024-12-10 19:16:08 +00:00
mathuo
c777ff463d
Merge pull request #787 from mathuo/786-dockview-react-peer-dependencies
feat: specify react peerDependency
2024-12-10 19:15:03 +00:00
mathuo
04327b4613
Merge branch 'master' of https://github.com/mathuo/dockview into 777-keeping-header-actions-even-when-no-tabs-are-open 2024-12-10 19:12:59 +00:00
mathuo
a02241d1a7
feat: ensure group always exists 2024-12-10 19:11:46 +00:00
Daniel Goodwin
615886e666
Do not close group if it's the only group available 2024-12-10 19:11:17 +00:00
mathuo
1b921864fc
chore: fix sonar issue 2024-12-10 19:11:17 +00:00
mathuo
1ce96c8ab8
test: add tests 2024-12-10 19:11:16 +00:00
mathuo
d651c2213e
bug: fix setVisible on floating groups 2024-12-10 19:11:16 +00:00
Jafar Akhondali
1d61a47e7c
Block malicious looking requests to prevent path traversal attacks. 2024-12-10 19:11:16 +00:00
mathuo
aeb0e38249
feat: tab container empty space not focusable 2024-12-10 19:11:16 +00:00
mathuo
df3269d56e
chore: v2.0.0 docs 2024-12-10 19:11:16 +00:00
mathuo
0233e8fa42
chore(release): publish v2.0.0 2024-12-10 19:11:15 +00:00
mathuo
2bcd62412d
feat: specify react peerDependency 2024-12-10 19:01:18 +00:00
mathuo
32e759ef75
Merge pull request #785 from mathuo/782-dockview-fails-or-crashes-when-attempting-to-make-a-floating-group-from-a-popout-group
bug: wrong event ordering
2024-12-09 20:03:36 +00:00
mathuo
ede5a58ec9
Merge pull request #769 from mathuo/705-when-loading-stored-layout-having-popout-grooups-dockview-doesnt-respect-the-popouturl-passed-instead-it-defaults-it-to-popouthtml
feat: persist custom popout group urls
2024-12-09 19:41:44 +00:00
mathuo
56bdec846c
bug: wrong event ordering 2024-12-09 19:40:49 +00:00
mathuo
25489bf48e
Merge pull request #770 from mathuo/689-docs-demos-for-vanilla-js-support
chore: fix files
2024-11-15 21:19:03 +00:00
mathuo
9564319e43
chore: fix files 2024-11-15 21:18:09 +00:00
mathuo
461c30dc5b
feat: persist custom popout group urls 2024-11-15 21:03:38 +00:00
mathuo
be628a5683
Merge pull request #743 from mathuo/689-docs-demos-for-vanilla-js-support
chore: docs
2024-11-15 20:48:58 +00:00
mathuo
19f027a0db
chore: docs 2024-11-15 20:36:51 +00:00
mathuo
e46a586044
chore: remove old docs 2024-11-15 20:36:26 +00:00
mathuo
2f4150013b
feat: serialization of maximized views 2024-11-10 20:19:20 +00:00
mathuo
24cc974a68
Merge pull request #755 from mathuo/665-floating-group-setvisible-will-crash
bug: fix setVisible on floating groups
2024-11-10 14:29:57 +00:00
mathuo
a00875aae0
chore: fix sonar issue 2024-11-10 11:12:01 +00:00
mathuo
02d0ad8634
test: add tests 2024-11-10 10:58:21 +00:00
mathuo
93b29e77c4
Merge pull request #759 from JafarAkhondali/master
Fixing a Path Traversal Vulnerability
2024-11-10 10:42:03 +00:00
mathuo
df0d12128b
Merge pull request #761 from mathuo/760-remove-tabindex-from-empty-tab-space
feat: tab container empty space not focusable
2024-11-07 18:58:54 +00:00
mathuo
986132c602
feat: tab container empty space not focusable 2024-11-07 18:58:25 +00:00
Jafar Akhondali
fe39c475a2 Block malicious looking requests to prevent path traversal attacks. 2024-11-07 17:23:01 +01:00
mathuo
c9e90168a4
bug: fix setVisible on floating groups 2024-11-04 20:28:27 +00:00
mathuo
1a9ee8c34e
chore: v2.0.0 docs 2024-11-03 19:23:16 +00:00
mathuo
8a05aede4c
chore(release): publish v2.0.0 2024-11-03 19:21:57 +00:00
mathuo
83b01a9a66
Merge branch 'master' of https://github.com/mathuo/dockview into 689-docs-demos-for-vanilla-js-support 2024-11-03 19:19:03 +00:00
mathuo
7ebbd437a1
Merge pull request #752 from mathuo/739-sonar-code-cleanup
chore: sonar
2024-11-03 19:15:18 +00:00
mathuo
d8ae04e274
chore: sonar 2024-11-03 19:14:49 +00:00
mathuo
3f5c9743ad
Merge pull request #751 from mathuo/750-add-panel-in-a-specific-index-within-a-group-without-making-it-active
feat: addPanel positional index
2024-11-03 19:05:35 +00:00
mathuo
3d1fa25677
Merge pull request #744 from mathuo/717-setconstraints-no-longer-works
bug: fix constraints
2024-11-03 19:05:19 +00:00
mathuo
108bc49401
Merge pull request #748 from mathuo/747-fromjson-error-while-the-json-include-popoutgroups
bug: duplicate .close() call
2024-11-03 19:05:00 +00:00
mathuo
291e76afdb
feat: addPanel positional index 2024-11-03 15:12:23 +00:00
mathuo
2dc4f81b20
bug: fix constraints 2024-11-03 14:56:58 +00:00
mathuo
ca6f46ac8e
Merge pull request #749 from mathuo/716-css-variable-dv-background-color-is-always-black-which-breaks-theming-of-auxilliary-components
feat: remove --dv-background-color
2024-11-03 14:19:38 +00:00
mathuo
dad9eba474
feat: remove --dv-background-color 2024-11-02 21:11:21 +00:00
mathuo
adaeb16b98
bug: duplicate .close() call 2024-11-02 20:18:11 +00:00
mathuo
ee7cf637bb
Merge pull request #746 from mathuo/739-sonar-code-cleanup
chore: fix theme
2024-10-31 19:20:12 +00:00
mathuo
e70755e4fc
chore: update auto-gen api 2024-10-29 20:25:09 +00:00
mathuo
112dc2e2a9
Merge branch 'master' of https://github.com/mathuo/dockview into 739-sonar-code-cleanup 2024-10-29 20:24:16 +00:00
mathuo
5cf3cfb616
chore: docs 2024-10-29 20:22:40 +00:00
mathuo
f6f248c63a
chore: fix theme 2024-10-29 20:06:03 +00:00
mathuo
4bf3fc4820
Merge pull request #740 from mathuo/739-sonar-code-cleanup
chore: code cleanup
2024-10-27 20:09:46 +00:00
mathuo
40c1a26ead
chore: fix 2024-10-27 20:00:54 +00:00
mathuo
3f3ed52007
Merge pull request #742 from mathuo/741-remove-depreciated-method
chore: remove depricated method
2024-10-27 19:54:18 +00:00
mathuo
b812cf9117
chore: code cleanup 2024-10-27 19:53:29 +00:00
mathuo
210ff38cfa
chore: remove depricated method 2024-10-27 19:47:30 +00:00
mathuo
3ffb5034c6
Merge pull request #738 from mathuo/726-__dockview_internal_drag_event__-appearing-in-editor-text-after-dragdrop-of-panel
feat: use empty string to fill dataTransfer field
2024-10-27 10:37:26 +00:00
mathuo
2657c79620
feat: use empty string to fill dataTransfer field 2024-10-27 10:20:54 +00:00
mathuo
cd4cd0f0ce
Merge pull request #673 from mathuo/672-vu3-injectprovides-not-propagated-to-panels
bug: maintain inject/provide context
2024-10-27 09:48:55 +00:00
mathuo
408db93917
Merge pull request #737 from mathuo/736-bug-set-initial-size-and-use-proper-window-on-event
736 bug set initial size and use proper window on event
2024-10-27 09:40:35 +00:00
mathuo
f557e9080a
test: add tests 2024-10-26 22:05:07 +01:00
mathuo
e0744531ca
Merge pull request #688 from mathuo/687-css-class-prefixing
chore: rename classes to start with dv-
2024-10-26 21:57:54 +01:00
mathuo
3c680b251f
Merge pull request #727 from iammola/popout-group-sizing-event
🐛 fix: set initial size and use proper window on event
2024-10-26 21:52:43 +01:00
mathuo
c086cabd39
Merge branch 'master' of https://github.com/mathuo/dockview into 687-css-class-prefixing 2024-10-12 15:22:08 +01:00
mathuo
a202a49182
Merge pull request #731 from mathuo/710-reloading-of-popout-window-is-displaying-blank-page
bug: close popout window if unloaded
2024-10-12 15:20:51 +01:00
mathuo
efd77413da
chore: docs typo 2024-10-12 15:20:03 +01:00
mathuo
0097da7fde
chore(release): publish v1.17.2 2024-10-12 15:18:12 +01:00
mathuo
8c7369ccf0
chore: 1.17.2 release notes 2024-10-12 15:17:49 +01:00
mathuo
8015860bde
Merge pull request #730 from mathuo/720-allow-customization-of-the-default_overlay_z_index-value
720 allow customization of the default overlay z index value
2024-10-09 22:23:04 +01:00
mathuo
14382aa0fd
Merge pull request #715 from NotWearingPants/patch-2
docs: add the splash-screen to the dockview package
2024-10-09 22:16:57 +01:00
mathuo
7ddb63383f
bug: close popout window if unloaded 2024-10-09 22:16:27 +01:00
mathuo
ac56a1f250
feat: fixup z-index overlay override 2024-10-09 22:14:06 +01:00
mathuo
12bb6f6d20
Merge pull request #724 from amirmamaghani/master
Added support for custom overlay z-index
2024-10-09 21:53:27 +01:00
mathuo
bc455265cd
Merge pull request #714 from NotWearingPants/patch-1
docs: fix typo in core/panels/tabs
2024-10-04 20:09:27 +01:00
Ademola Adedeji
6881daa593 🐛 fix: set initial size and use proper window on event 2024-10-03 20:17:17 -03:00
Amir
3c90e9d196 Added support for custom overlay z-index 2024-10-01 08:48:32 +02:00
NotWearingPants
c55257ac56
docs: add the splash-screen to the dockview package 2024-09-18 02:10:39 +03:00
NotWearingPants
1b7f29e800
docs: fix typo in core/panels/tabs 2024-09-18 01:41:51 +03:00
mathuo
693a3cd6ef
chore: docs 2024-09-05 20:26:17 +01:00
mathuo
d1d8c8083e
chore(release): publish v1.17.1 2024-09-05 20:18:53 +01:00
mathuo
fd29ea570f
Merge branch 'master' of https://github.com/mathuo/dockview 2024-09-05 20:18:41 +01:00
mathuo
1acdd969a0
chore: v1.17.1 docs 2024-09-05 20:18:08 +01:00
mathuo
aeda5f4ca1
Merge pull request #709 from mathuo/696-touch-support-for-dragging-dockview-panels
bug: fix mouseup->pointerup
2024-09-05 20:16:36 +01:00
mathuo
5df4c5cce1
bug: fix mouseup->pointerup 2024-09-05 20:16:01 +01:00
mathuo
62fee4e7ec
chore(release): publish v1.17.0 2024-09-05 19:49:21 +01:00
mathuo
b24470fa42
chore: v1.17.0 release notes 2024-09-05 19:48:56 +01:00
mathuo
0c602afab5
Merge pull request #703 from mathuo/702-documentation
chore: docs
2024-09-05 19:33:13 +01:00
mathuo
ab5a11f13b
chore: docs 2024-09-05 19:20:37 +01:00
mathuo
fc7b9fb2e2
Merge pull request #698 from mathuo/696-touch-support-for-dragging-dockview-panels
feat: ensure use of pointerevents for touch support
2024-08-28 20:32:07 +01:00
mathuo
d519c239e3
Merge pull request #690 from mathuo/640-panel-and-group-default-sizes-and-bounding-dimensions
640 panel and group default sizes and bounding dimensions
2024-08-28 20:31:42 +01:00
mathuo
2b36844110
Merge pull request #693 from mathuo/640-panel-and-group-default-sizes-and-bounding-dimensions-1
feat: retain size when moving groups
2024-08-28 20:31:25 +01:00
mathuo
e2e91834ff
feat: retain size when moving groups 2024-08-27 21:16:14 +01:00
mathuo
72f457ab9d
feat: panel initial sizing 2024-08-27 20:59:38 +01:00
mathuo
1033b80783
feat: ensure use of pointerevents for touch support 2024-08-27 20:54:27 +01:00
mathuo
7adabec088
Merge pull request #694 from OneAndOnlyFinbar/master
Fix typo in replit theme
2024-08-24 21:40:33 +01:00
OneAndOnlyFinbar
4841d95573
Fix typo in replit theme
Remove duplicated pound sign.
2024-08-24 15:02:18 -04:00
mathuo
2dc0dafa5a
feat: addGroup initial sizes 2024-08-20 22:00:21 +01:00
mathuo
9d4f4cb534
feat: panel constraints 2024-08-20 21:52:11 +01:00
mathuo
05084030db
Merge pull request #686 from mathuo/651-offical-support-for-vanilla-typescript-1
chore: updateOptions logic
2024-08-16 19:16:42 +01:00
mathuo
4d71775804
chore: fix test 2024-08-16 18:54:33 +01:00
mathuo
77bccb3f69
chore: remove method 2024-08-15 21:39:45 +01:00
mathuo
affb8590dc
chore: rename classes to start with dv- 2024-08-15 20:52:53 +01:00
mathuo
6c39c448bb
chore: updateOptions logic 2024-08-15 20:43:00 +01:00
mathuo
a6a2d048c7
chore(release): publish v1.16.1 2024-08-13 19:45:23 +01:00
mathuo
464e255d56
chore: 1.16.1 docs 2024-08-13 19:44:51 +01:00
mathuo
0e8139217b
Merge pull request #685 from mathuo/684-multiple-classes-on-dockviewreact-element-doesnt-work-in-1160
bug: multiple classnames
2024-08-13 19:43:29 +01:00
mathuo
1876511c70
bug: multiple classnames 2024-08-13 19:37:24 +01:00
mathuo
56182aa60f
chore(release): publish v1.16.0 2024-08-11 21:48:35 +01:00
mathuo
bf35b265db
chore: 1.16.0 docs 2024-08-11 21:47:42 +01:00
mathuo
520aa39724
Merge pull request #683 from mathuo/656-renderer-always-causes-floating-group-z-index-bug
feat: correct z-index level for floating always rendered panel
2024-08-11 20:41:15 +01:00
mathuo
59acd94bc6
feat: correct z-index level for floating always rendered panel 2024-08-11 20:34:49 +01:00
mathuo
619b0b3276
Merge pull request #679 from mathuo/656-renderer-always-causes-floating-group-z-index-bug
656 renderer always causes floating group z index bug
2024-08-11 11:04:33 +01:00
mathuo
d686b2c2c7
chore: refactor and fix tests 2024-08-11 10:45:47 +01:00
mathuo
479d4bb322
Merge pull request #682 from mathuo/681-resize-handlers-position-is-wrong-for-more-than-2-panels-in-one-branch
681 resize handlers position is wrong for more than 2 panels in one branch
2024-08-11 10:33:17 +01:00
mathuo
49b1c5a174
bug: gap sizing fixes 2024-08-11 10:26:04 +01:00
mathuo
35b3ee2b0f
Merge pull request #667 from RayJason/fix/layout-view-size-calc
fix: splitview layout view size with hidden view
2024-08-10 12:37:25 +01:00
mathuo
a443bb5e41
Merge branch 'master' of https://github.com/mathuo/dockview into 656-renderer-always-causes-floating-group-z-index-bug 2024-08-06 21:43:40 +01:00
mathuo
a05d2af417
chore: fix test 2024-08-06 21:37:07 +01:00
mathuo
1efa5c0ef6
bug: always rendered floating groups z-index 2024-08-06 21:33:20 +01:00
mathuo
6bf0263797
Merge branch 'master' of https://github.com/mathuo/dockview into 656-renderer-always-causes-floating-group-z-index-bug 2024-08-05 21:35:44 +01:00
mathuo
ed3fd57d6a
Merge pull request #678 from mathuo/677-floating-panel-documentation-example-not-working
chore: fix documentation
2024-08-05 21:28:53 +01:00
mathuo
5a80b3a58d
Merge pull request #662 from mathuo/660-dragging-last-panel-from-popout-group-breaks-layout
feat: moving single panel out of popout group
2024-08-05 21:23:37 +01:00
mathuo
f022f3cb26
Merge pull request #652 from mathuo/651-offical-support-for-vanilla-typescript
feat: offical support for vanilla typescript
2024-08-05 21:23:10 +01:00
mathuo
af5ce5bbfd
chore: fix documentation 2024-08-05 21:18:57 +01:00
mathuo
ea5b94ad90
bug: maintain inject/provide context 2024-08-01 20:47:07 +01:00
mathuo
e5e9603221
chore(release): publish v1.15.3 2024-08-01 20:07:13 +01:00
mathuo
cc18f5055c
chore: v1.15.3 docs 2024-08-01 20:06:34 +01:00
mathuo
fafc18b419
Merge pull request #670 from mathuo/668-load-layout-will-not-keep-the-size-of-floating-group
bug: floating-groups deserialization
2024-08-01 20:06:19 +01:00
mathuo
bb312d871c
Merge pull request #666 from RayJason/fix/style-typo
fix: default tab ellipsis typo
2024-08-01 20:01:35 +01:00
mathuo
49561d14cc
bug: floating-groups deserialization 2024-08-01 20:00:11 +01:00
RayJason
1cd4251083 fix: mistake of sashes offset when custom gap 2024-07-30 23:10:22 +08:00
RayJason
ee74785d7e fix: calc views style 2024-07-30 21:11:36 +08:00
RayJason
eda3ea1210 fix: splitview layout view size with hidden view 2024-07-30 15:44:38 +08:00
RayJason
f689e38e59 fix: default tab ellipsis typo 2024-07-30 15:41:01 +08:00
mathuo
f765dd52fc
bug: render mode z-index fixes 2024-07-27 20:34:06 +01:00
mathuo
04d54e1dc5
feat: moving single panel out of popout group 2024-07-27 20:24:48 +01:00
mathuo
e93a008fe4
chore: docs 2024-07-23 20:42:09 +01:00
mathuo
5b9dbdf57e
chore(release): publish v1.15.2 2024-07-17 22:34:33 +01:00
mathuo
0e77bdf4d7
chore: v1.15.2 release notes 2024-07-17 22:34:13 +01:00
mathuo
0211148b66
Merge pull request #655 from mathuo/654-resize
bug: fix layout force flag
2024-07-17 22:33:50 +01:00
mathuo
973ecff0be
bug: fix layout force flag 2024-07-17 22:28:31 +01:00
mathuo
bdf286103f
bug: continue to check size change when force= 2024-07-17 19:53:55 +01:00
mathuo
e86155adf4
feat: offical support for vanilla typescript 2024-07-17 19:41:57 +01:00
mathuo
374bd2adff
chore(release): publish v1.15.1 2024-07-16 20:25:01 +01:00
mathuo
8618ed3aad
chore: v1.15.1 release notes 2024-07-16 20:23:19 +01:00
mathuo
aa39b55563
Merge pull request #648 from mathuo/647-115-removed-types-that-are-still-valid
bug: fixup types
2024-07-16 20:22:38 +01:00
mathuo
24e8c99dea
Merge pull request #645 from timadevelop/fix/popout-single-dockviewpanel
bug: fix passing through options for opening single panel in a popot window
2024-07-16 20:20:30 +01:00
mathuo
eeab0251d2
Merge pull request #649 from mathuo/613-gap-between-panels-leads-to-some-panels-size-overflow-2
bug: fix dockview  option
2024-07-16 18:59:15 +01:00
mathuo
2111e2293c
bug: fix dockview option 2024-07-15 21:24:06 +01:00
mathuo
bd28468bf5
bug: fixup types 2024-07-15 20:44:51 +01:00
Vlad Timofeev
05b1c27320 bug: fix passing through options for opening single panel in a popout window 2024-07-15 15:50:30 +03:00
mathuo
b326291f94
chore(release): publish v1.15.0 2024-07-10 21:41:09 +01:00
mathuo
8a4380ec14
Merge pull request #638 from mathuo/626-docs-1
chore: v1.15.0 docs
2024-07-10 21:09:39 +01:00
mathuo
eb3cac8b1b
chore: improve docs 2024-07-09 21:16:59 +01:00
mathuo
0db4f59990
chore: v1.15.0 docs 2024-07-06 22:09:39 +01:00
mathuo
aed7b97bac
Merge pull request #618 from mathuo/613-gap-between-panels-leads-to-some-panels-size-overflow-1
bug: panel gap styling
2024-07-06 22:07:21 +01:00
mathuo
9e4b3bbdd6
Merge pull request #616 from mathuo/582-expose-event-for-moving-tab-within-same-panel
feat: move panel events
2024-07-06 22:07:03 +01:00
mathuo
cd59248f34
Merge branch 'master' of https://github.com/mathuo/dockview into 582-expose-event-for-moving-tab-within-same-panel 2024-07-06 21:59:08 +01:00
mathuo
876b107284
feat: move panel events 2024-07-06 21:58:38 +01:00
mathuo
ea9dc13992
bug: panel gap styling 2024-07-06 21:22:34 +01:00
mathuo
f20b5285da
Merge pull request #628 from mathuo/544-floatinggroups-support-for-css-absolute-box-attributes-bottom-and-right
544 floatinggroups support for css absolute box attributes bottom and right
2024-07-03 22:18:34 +01:00
mathuo
9b46c1e7d9
chore: addFlotaingGroup method changes 2024-07-01 22:18:27 +01:00
mathuo
d7e1fd4bb2
Merge pull request #633 from mathuo/630-after-all-the-groups-in-the-branch-are-hidden-an-error-is-reported-when-calling-fromjson
feat: setVisible enhancements
2024-07-01 22:15:48 +01:00
mathuo
ae88703a8b
feat: setVisible enhancements 2024-07-01 20:29:03 +01:00
mathuo
cf8f18dbbd
test: fix tests 2024-06-19 22:17:59 +01:00
mathuo
b96ccf00ce
Merge branch 'master' of https://github.com/mathuo/dockview into 544-floatinggroups-support-for-css-absolute-box-attributes-bottom-and-right 2024-06-14 17:16:21 +01:00
mathuo
5285baebdb
Merge pull request #621 from crubier/master
Allow right and bottom anchored floating groups
2024-06-14 17:14:58 +01:00
mathuo
3652505a08
Merge pull request #627 from mathuo/626-docs
chore: docs
2024-06-08 23:51:13 +03:00
mathuo
c5f94906d4
chore: docs 2024-06-08 21:39:43 +01:00
mathuo
e3fb689d27
chore(release): publish v1.14.2 2024-06-08 20:01:33 +01:00
mathuo
93e5ecec4c
chore: 1.14.2 release notes 2024-06-08 20:00:47 +01:00
mathuo
82dbdb29c3
Merge pull request #625 from mathuo/624-dockview-vue-error-cannot-read-properties-of-null-reading-appcontext
bug: vue getInstance
2024-06-08 19:15:19 +03:00
mathuo
476b0a1d42
Merge pull request #623 from mathuo/622-settitle-issues
bug: fix setTitle issues
2024-06-08 19:15:08 +03:00
mathuo
e7622f6c2b
bug: vue getInstance 2024-06-08 16:55:15 +01:00
mathuo
cd6604d28c
bug: fix setTitle issues 2024-06-04 22:56:27 +03:00
Vincent Lecrubier
cffd742b7b Docs 2024-06-03 11:41:07 +01:00
Vincent Lecrubier
bbdb99dbb5 Support bottom and right anchor for Overlay 2024-06-03 11:25:38 +01:00
mathuo
ce381f8ce9
Merge pull request #620 from mathuo/562-dockview-framework-wrappers-vuejs-angular-javascript-etc-3
chore: vue docs
2024-05-30 21:06:37 +01:00
mathuo
1bd2bff8f8
chore: vue docs 2024-05-30 20:59:38 +01:00
mathuo
943c2dc425
Merge pull request #617 from mathuo/562-dockview-framework-wrappers-vuejs-angular-javascript-etc-3
chore: vue3 docs
2024-05-30 20:44:23 +01:00
mathuo
bc278f2beb
chore: vue3 docs 2024-05-30 20:37:57 +01:00
mathuo
b81afd45e8
chore(release): publish v1.14.1 2024-05-28 21:47:50 +01:00
mathuo
77b8023b56
Merge branch 'master' of https://github.com/mathuo/dockview 2024-05-28 21:46:51 +01:00
mathuo
963607defe
chore: 1.14.1 release notes 2024-05-28 21:46:46 +01:00
mathuo
4a0b328622
chore: update README.md files 2024-05-28 21:46:32 +01:00
mathuo
5762faac9d
Merge pull request #614 from mathuo/613-gap-between-panels-leads-to-some-panels-size-overflow
bug: --group-gap-size overflow fix
2024-05-28 21:42:20 +01:00
mathuo
7f750330b1
bug: --group-gap-size overflow fix 2024-05-24 19:50:25 +01:00
mathuo
55d9ca31d4
Merge pull request #612 from mathuo/611-sonar-suggestions
chore: sonar suggested fixes
2024-05-24 18:33:43 +01:00
mathuo
fde6764377
chore: sonar suggested fixes 2024-05-23 22:38:38 +01:00
mathuo
9c6dae30ca
chore(release): publish v1.14.0 2024-05-23 22:01:14 +01:00
mathuo
acadeedaf6
chore: v1.14.0 release notes 2024-05-23 22:00:04 +01:00
mathuo
fc84942b9b
Merge pull request #606 from mathuo/562-dockview-framework-wrappers-vuejs-angular-javascript-etc-2
562 dockview framework wrappers vuejs angular javascript etc 2
2024-05-14 21:03:21 +01:00
mathuo
e3eb851ab7
chore: configure dockview-react package 2024-05-14 20:42:33 +01:00
mathuo
84d02b9925
Merge pull request #605 from mathuo/603-sonarcloud-cleanups
chore: cleanup code
2024-05-14 20:39:57 +01:00
mathuo
3fcbf6515e
chore: configure dockview-vue package 2024-05-14 20:37:45 +01:00
mathuo
9306d9fcdc
chore: configure dockview-react package 2024-05-14 20:36:35 +01:00
mathuo
d29052e606
chore: cleanup code 2024-05-14 20:33:14 +01:00
mathuo
9ee2b821ff
Merge pull request #604 from mathuo/603-sonarcloud-cleanups
chore: sonarcloud requests
2024-05-13 21:33:27 +01:00
mathuo
c4ab6ac562
chore: sonarcloud requests 2024-05-13 20:50:05 +01:00
mathuo
35630e4f1c
Merge pull request #594 from mathuo/562-dockview-framework-wrappers-vuejs-angular-javascript-etc-1
feat: vue3
2024-05-13 19:42:45 +01:00
mathuo
07b548c6b6
Merge branch 'master' of https://github.com/mathuo/dockview into 562-dockview-framework-wrappers-vuejs-angular-javascript-etc-1 2024-05-13 19:34:34 +01:00
mathuo
7eff8a8642
Merge pull request #602 from mathuo/601-internal-clean-up-update-function
chore: cleanup internals
2024-05-13 19:34:02 +01:00
mathuo
abaad37edd
chore: cleanup internals 2024-05-13 18:11:24 +01:00
mathuo
b68d8df9ac
chore: clean code 2024-05-11 10:42:44 +01:00
mathuo
07d67b2246
chore: remove unused imports 2024-05-10 22:59:13 +01:00
mathuo
0ae16cf444
feat: vue3 fixes 2024-05-10 22:44:24 +01:00
mathuo
26cd1cc1cc
chore: configure dockview-vue package 2024-05-09 21:04:45 +01:00
mathuo
56457fe269
Merge branch 'master' of https://github.com/mathuo/dockview into 562-dockview-framework-wrappers-vuejs-angular-javascript-etc-1 2024-05-09 20:48:58 +01:00
mathuo
5d6055c4d2
feat: vue3 2024-05-01 20:35:07 +01:00
351 changed files with 19818 additions and 11592 deletions

View File

@ -1,6 +1,8 @@
{
"packages": [
"packages/dockview-core",
"packages/dockview-vue",
"packages/dockview-react",
"packages/dockview"
],
"sandboxes": [
@ -41,4 +43,4 @@
"/packages/docs/sandboxes/javascript/vanilla-dockview"
],
"node": "18"
}
}

View File

@ -26,6 +26,10 @@ 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

@ -27,7 +27,7 @@ jobs:
- run: npm run build
- run: npm run test:cov
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
uses: sonarsource/sonarqube-scan-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -35,6 +35,12 @@ jobs:
- 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
@ -64,3 +70,9 @@ jobs:
- 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

View File

@ -1,18 +1,18 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews with ReactJS support written in TypeScript</p>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
</div>
---
[![npm version](https://badge.fury.io/js/dockview.svg)](https://www.npmjs.com/package/dockview)
[![npm](https://img.shields.io/npm/dm/dockview)](https://www.npmjs.com/package/dockview)
[![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)
[![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)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview-core)](https://bundlephobia.com/result?p=dockview-core)
##
@ -35,24 +35,4 @@ Please see the website: https://dockview.dev
- 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>
```
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#user-content-provenance).

8
SECURITY.md Normal file
View File

@ -0,0 +1,8 @@
# 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

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

View File

@ -16,7 +16,7 @@
"packages/*"
],
"scripts": {
"build": "lerna run build --scope '{dockview-core,dockview}'",
"build": "lerna run build --scope '{dockview-core,dockview,dockview-vue,dockview-react}'",
"clean": "lerna run clean",
"docs": "typedoc",
"generate-docs": "node scripts/docs.mjs",
@ -58,7 +58,7 @@
"jest-environment-jsdom": "^29.7.0",
"jest-sonar-reporter": "^2.0.0",
"jsdom": "^23.0.1",
"lerna": "^8.0.1",
"lerna": "^8.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^5.0.5",
@ -77,8 +77,5 @@
},
"engines": {
"node": ">=18.0"
},
"dependencies": {
"ag-grid-vue3": "^31.1.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview-angular",
"version": "1.13.1",
"version": "4.2.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -54,6 +54,6 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
},
"dependencies": {
"dockview-core": "^1.13.1"
"dockview-core": "^4.2.5"
}
}

View File

@ -1,7 +1,7 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews written in TypeScript</p>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
</div>
@ -36,23 +36,3 @@ Please see the website: https://dockview.dev
- 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-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,6 +1,6 @@
{
"name": "dockview-core",
"version": "1.13.1",
"version": "4.2.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",

View File

@ -1,23 +1,28 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import {
GroupPanelPartInitParameters,
TabPartInitParameters,
IContentRenderer,
ITabRenderer,
} from '../../dockview/types';
import { PanelUpdateEvent } from '../../panel/types';
import { TabLocation } from '../../dockview/framework';
export class DockviewPanelModelMock implements IDockviewPanelModel {
constructor(
readonly contentComponent: string,
readonly content: IContentRenderer,
readonly tabComponent?: string,
readonly tab?: ITabRenderer
readonly tabComponent: string,
readonly tab: ITabRenderer
) {
//
}
init(params: GroupPanelPartInitParameters): void {
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return this.tab;
}
init(params: TabPartInitParameters): void {
//
}

View File

@ -0,0 +1,45 @@
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

@ -44,3 +44,30 @@ export function createOffsetDragOverEvent(params: {
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

@ -9,7 +9,8 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const panelMock = jest.fn<DockviewPanel, []>(() => {
@ -49,7 +50,8 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(
@ -81,7 +83,8 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(

View File

@ -70,8 +70,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragEnd(element);
expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe('auto');
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
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('auto');
expect(webview.style.pointerEvents).toBe('auto');
expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('');
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).toBeCalledTimes(0);
expect(spy).toHaveBeenCalledTimes(0);
handler.dispose();
});

View File

@ -16,10 +16,10 @@ describe('droptarget', () => {
beforeEach(() => {
element = document.createElement('div');
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {
@ -53,7 +53,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
'.dv-drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
@ -61,7 +61,7 @@ describe('droptarget', () => {
const event = new Event('dragover');
(event as any)['__dockview_droptarget_event_is_used__'] = true;
fireEvent(element, event);
expect(element.querySelector('.drop-target-dropzone')).toBeNull();
expect(element.querySelector('.dv-drop-target-dropzone')).toBeNull();
});
test('directionToPosition', () => {
@ -102,7 +102,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
'.dv-drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
@ -124,7 +124,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
'.dv-drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -155,12 +155,12 @@ describe('droptarget', () => {
fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
const target = element.querySelector(
'.drop-target-dropzone'
'.dv-drop-target-dropzone'
) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -187,13 +187,13 @@ describe('droptarget', () => {
}
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('left');
check(
element
.getElementsByClassName('drop-target-selection')
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
@ -209,13 +209,13 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('top');
check(
element
.getElementsByClassName('drop-target-selection')
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
@ -231,13 +231,13 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('bottom');
check(
element
.getElementsByClassName('drop-target-selection')
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '50%',
@ -253,13 +253,13 @@ describe('droptarget', () => {
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('right');
check(
element
.getElementsByClassName('drop-target-selection')
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement,
{
top: '0px',
@ -276,14 +276,14 @@ describe('droptarget', () => {
expect(
(
element
.getElementsByClassName('drop-target-selection')
.getElementsByClassName('dv-drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target);
expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.drop-target');
viewQuery = element.querySelectorAll('.dv-drop-target');
expect(viewQuery.length).toBe(0);
});

View File

@ -1,156 +0,0 @@
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,5 +1,4 @@
import { fireEvent } from '@testing-library/dom';
import { Emitter, Event } from '../../../../events';
import { ContentContainer } from '../../../../dockview/components/panel/content';
import {
GroupPanelPartInitParameters,
@ -10,9 +9,9 @@ import { PanelUpdateEvent } from '../../../../panel/types';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { OverlayRenderContainer } from '../../../../overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer';
class TestContentRenderer
extends CompositeDisposable
@ -58,7 +57,8 @@ describe('contentContainer', () => {
const disposable = new CompositeDisposable();
const overlayRenderContainer = new OverlayRenderContainer(
document.createElement('div')
document.createElement('div'),
fromPartial<DockviewComponent>({})
);
const cut = new ContentContainer(

View File

@ -8,6 +8,7 @@ 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', () => {
@ -20,7 +21,7 @@ describe('tab', () => {
new groupMock()
);
expect(cut.element.className).toBe('tab inactive-tab');
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
});
test('that active tab has active-tab class', () => {
@ -34,10 +35,10 @@ describe('tab', () => {
);
cut.setActive(true);
expect(cut.element.className).toBe('tab active-tab');
expect(cut.element.className).toBe('dv-tab dv-active-tab');
cut.setActive(false);
expect(cut.element.className).toBe('tab inactive-tab');
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
});
test('that an external event does not render a drop target and calls through to the group model', () => {
@ -46,15 +47,10 @@ describe('tab', () => {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -72,38 +68,33 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalled();
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that if you drag over yourself no drop target is shown', () => {
test('that if you drag over yourself a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -121,10 +112,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -136,11 +127,11 @@ describe('tab', () => {
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that if you drag over another tab a drop target is shown', () => {
@ -175,10 +166,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -193,7 +184,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
@ -229,10 +220,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -253,7 +244,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
@ -289,10 +280,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -313,7 +304,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
});

View File

@ -0,0 +1,63 @@
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

@ -0,0 +1,66 @@
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

@ -10,13 +10,15 @@ 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: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -40,17 +42,17 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -60,7 +62,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
@ -69,18 +71,18 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const dropTargetContainer = document.createElement('div');
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -95,17 +97,17 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -126,8 +128,12 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
});
test('that dropping over the empty space should render a drop target', () => {
@ -135,7 +141,8 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -164,17 +171,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -189,7 +196,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
@ -198,7 +205,8 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -227,17 +235,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -252,7 +260,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
@ -261,7 +269,8 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -289,17 +298,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
.getElementsByClassName('dv-void-container')
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -320,7 +329,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
@ -329,7 +338,8 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -341,7 +351,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
@ -354,7 +364,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -369,7 +379,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left2);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -381,7 +391,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(undefined);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
'.dv-tabs-and-actions-container > .dv-left-actions-container'
);
expect(query.length).toBe(1);
@ -393,7 +403,8 @@ describe('tabsContainer', () => {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: { parentElement: document.createElement('div') },
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -405,7 +416,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
@ -418,7 +429,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -433,7 +444,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right2);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
@ -445,7 +456,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(undefined);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
'.dv-tabs-and-actions-container > .dv-right-actions-container'
);
expect(query.length).toBe(1);
@ -454,11 +465,13 @@ describe('tabsContainer', () => {
test('that a tab will become floating when clicked if not floating and shift is selected', () => {
const accessor = fromPartial<DockviewComponent>({
options: { parentElement: document.createElement('div') },
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -471,7 +484,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
const container = cut.element.querySelector('.dv-void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -486,22 +499,20 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.addFloatingGroup).toHaveBeenCalledWith(
groupPanel,
{
x: 100,
y: 60,
},
{ inDragMode: true }
);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
expect(accessor.addFloatingGroup).toHaveBeenCalledWith(groupPanel, {
x: 100,
y: 60,
inDragMode: true,
});
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
@ -511,11 +522,13 @@ describe('tabsContainer', () => {
test('that a tab that is already floating cannot be floated again', () => {
const accessor = fromPartial<DockviewComponent>({
options: { parentElement: document.createElement('div') },
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -528,7 +541,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
const container = cut.element.querySelector('.dv-void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -543,14 +556,15 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(0);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
@ -560,12 +574,13 @@ describe('tabsContainer', () => {
test('that selecting a tab with shift down will move that tab into a new floating group', () => {
const accessor = fromPartial<DockviewComponent>({
options: { parentElement: document.createElement('div') },
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, []>(() => {
@ -595,10 +610,10 @@ describe('tabsContainer', () => {
const panel = createPanel('test_id');
cut.openPanel(panel);
const el = cut.element.querySelector('.tab')!;
const el = cut.element.querySelector('.dv-tab')!;
expect(el).toBeTruthy();
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(el, event);
@ -616,12 +631,13 @@ describe('tabsContainer', () => {
test('pre header actions', () => {
const accessor = fromPartial<DockviewComponent>({
options: { parentElement: document.createElement('div') },
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, []>(() => {
@ -654,14 +670,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.pre-actions-container');
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('.pre-actions-container');
result = cut.element.querySelector('.dv-pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
@ -669,26 +685,27 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div');
cut.setPrefixActionsElement(updatedActions);
result = cut.element.querySelector('.pre-actions-container');
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('.pre-actions-container');
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: { parentElement: document.createElement('div') },
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, []>(() => {
@ -721,14 +738,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.left-actions-container');
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('.left-actions-container');
result = cut.element.querySelector('.dv-left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
@ -736,26 +753,27 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div');
cut.setLeftActionsElement(updatedActions);
result = cut.element.querySelector('.left-actions-container');
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('.left-actions-container');
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: { parentElement: document.createElement('div') },
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, []>(() => {
@ -788,14 +806,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.right-actions-container');
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('.right-actions-container');
result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
@ -803,15 +821,47 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div');
cut.setRightActionsElement(updatedActions);
result = cut.element.querySelector('.right-actions-container');
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('.right-actions-container');
result = cut.element.querySelector('.dv-right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('class dv-single-tab is present when only one tab exists`', () => {
const cut = new TabsContainer(
fromPartial<DockviewComponent>({
options: {},
onDidOptionsChange: jest.fn(),
}),
fromPartial<DockviewGroupPanel>({})
);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
const panel1 = new TestPanel(
'panel_1',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
const panel2 = new TestPanel(
'panel_2',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
cut.closePanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
cut.closePanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
});
});

View File

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

View File

@ -1,27 +0,0 @@
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

@ -0,0 +1,190 @@
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

@ -9,40 +9,22 @@ import {
} from '../../dockview/types';
import { PanelUpdateEvent, Parameters } from '../../panel/types';
import {
DockviewGroupLocation,
DockviewGroupPanelModel,
GroupOptions,
} from '../../dockview/dockviewGroupPanelModel';
import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { CompositeDisposable } from '../../lifecycle';
import {
ActiveGroupEvent,
DockviewPanelApi,
GroupChangedEvent,
RendererChangedEvent,
} from '../../api/dockviewPanelApi';
import { DockviewPanelApi } from '../../api/dockviewPanelApi';
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 {
DockviewPanelRenderer,
OverlayRenderContainer,
} from '../../overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from '../../api/dockviewGroupPanelApi';
import { SizeEvent } from '../../api/gridviewPanelApi';
import {
PanelDimensionChangeEvent,
FocusEvent,
VisibilityEvent,
ActiveEvent,
WillFocusEvent,
} from '../../api/panelApi';
import { Position } from '../../dnd/droptarget';
import { Emitter, Event } from '../../events';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
import { Emitter } from '../../events';
import { fromPartial } from '@total-typescript/shoehorn';
import { TabLocation } from '../../dockview/framework';
enum GroupChangeKind2 {
ADD_PANEL,
@ -55,12 +37,16 @@ class TestModel implements IDockviewPanelModel {
readonly contentComponent: string;
readonly tab: ITabRenderer;
constructor(id: string) {
constructor(readonly 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 {
//
}
@ -116,10 +102,6 @@ class Watermark implements IWatermarkRenderer {
return {};
}
updateParentGroup() {
//
}
dispose() {
//
}
@ -276,7 +258,7 @@ describe('dockviewGroupPanelModel', () => {
});
dockview = fromPartial<DockviewComponent>({
options: { parentElement: document.createElement('div') },
options: {},
createWatermarkComponent: () => new Watermark(),
doSetGroupActive: jest.fn(),
id: 'dockview-1',
@ -285,8 +267,10 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div')
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: () => ({ dispose: jest.fn() }),
});
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
@ -495,12 +479,12 @@ describe('dockviewGroupPanelModel', () => {
test('default', () => {
let viewQuery = groupview.element.querySelectorAll(
'.groupview > .tabs-and-actions-container'
'.dv-groupview > .dv-tabs-and-actions-container'
);
expect(viewQuery).toBeTruthy();
viewQuery = groupview.element.querySelectorAll(
'.groupview > .content-container'
'.dv-groupview > .dv-content-container'
);
expect(viewQuery).toBeTruthy();
});
@ -516,19 +500,18 @@ describe('dockviewGroupPanelModel', () => {
groupview.model.closeAllPanels();
expect(removePanelMock).toBeCalledWith(panel1);
expect(removePanelMock).toBeCalledWith(panel2);
expect(removePanelMock).toBeCalledWith(panel3);
expect(removePanelMock).toHaveBeenCalledWith(panel1, undefined);
expect(removePanelMock).toHaveBeenCalledWith(panel2, undefined);
expect(removePanelMock).toHaveBeenCalledWith(panel3, undefined);
});
test('closeAllPanels with no panels', () => {
groupview.model.closeAllPanels();
expect(removeGroupMock).toBeCalledWith(groupview);
expect(removeGroupMock).toHaveBeenCalledWith(groupview);
});
test('that group is set on panel during onDidAddPanel event', () => {
const cut = new DockviewComponent({
parentElement: document.createElement('div'),
const cut = new DockviewComponent(document.createElement('div'), {
createComponent(options) {
switch (options.name) {
case 'component':
@ -548,17 +531,19 @@ describe('dockviewGroupPanelModel', () => {
});
test('toJSON() default', () => {
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
});
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 cut = new DockviewGroupPanelModel(
document.createElement('div'),
@ -576,17 +561,19 @@ describe('dockviewGroupPanelModel', () => {
});
test('toJSON() locked and hideHeader', () => {
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
});
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 cut = new DockviewGroupPanelModel(
document.createElement('div'),
@ -609,17 +596,19 @@ describe('dockviewGroupPanelModel', () => {
});
test("that openPanel with skipSetActive doesn't set panel to active", () => {
const dockviewComponent = new DockviewComponent({
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'component':
return new TestContentPart(options.id);
default:
throw new Error(`unsupported`);
}
},
});
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 groupviewContainer = document.createElement('div');
const cut = new DockviewGroupPanelModel(
@ -630,7 +619,7 @@ describe('dockviewGroupPanelModel', () => {
null as any
);
const contentContainer = groupviewContainer
.getElementsByClassName('content-container')
.getElementsByClassName('dv-content-container')
.item(0)!.childNodes;
const panel1 = new TestPanel('id_1', panelApi);
@ -659,12 +648,11 @@ describe('dockviewGroupPanelModel', () => {
test('that should not show drop target is external event', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {
parentElement: document.createElement('div'),
},
options: {},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -702,13 +690,13 @@ describe('dockviewGroupPanelModel', () => {
});
const element = container
.getElementsByClassName('content-container')
.item(0)!;
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
@ -716,19 +704,18 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(1);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that the .locked behaviour is as', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {
parentElement: document.createElement('div'),
},
options: {},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -764,13 +751,13 @@ describe('dockviewGroupPanelModel', () => {
});
const element = container
.getElementsByClassName('content-container')
.item(0)!;
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
function run(value: number) {
fireEvent.dragEnter(element);
@ -784,7 +771,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = false;
run(10);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
fireEvent.dragEnd(element);
@ -792,7 +779,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = 'no-drop-target';
run(10);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
fireEvent.dragEnd(element);
@ -800,7 +787,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = true;
run(10);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
fireEvent.dragEnd(element);
@ -808,35 +795,29 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = true;
run(25);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
fireEvent.dragEnd(element);
});
test('that should not show drop target if dropping on self', () => {
test('that should show drop target if dropping on self', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {
parentElement: document.createElement('div'),
},
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div')
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -863,13 +844,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel1', panelApi));
const element = container
.getElementsByClassName('content-container')
.item(0)!;
.getElementsByClassName('dv-content-container')
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -882,23 +863,23 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(0);
expect(
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that should not allow drop when dropping on self for same component id', () => {
test('that should allow drop when dropping on self for same component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {
parentElement: document.createElement('div'),
},
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div')
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -937,13 +918,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel2', panelApi));
const element = container
.getElementsByClassName('content-container')
.item(0)!;
.getElementsByClassName('dv-content-container')
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -956,23 +937,23 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(0);
expect(
element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
});
test('that should not allow drop when not dropping for different component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {
parentElement: document.createElement('div'),
},
options: {},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div')
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -1011,13 +992,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel2', panelApi));
const element = container
.getElementsByClassName('content-container')
.item(0)!;
.getElementsByClassName('dv-content-container')
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')],
@ -1030,7 +1011,7 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(1);
expect(
element.getElementsByClassName('drop-target-dropzone').length
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
@ -1115,7 +1096,7 @@ describe('dockviewGroupPanelModel', () => {
container.getElementsByClassName('watermark-test-container').length
).toBe(0);
expect(
container.getElementsByClassName('tabs-and-actions-container')
container.getElementsByClassName('dv-tabs-and-actions-container')
.length
).toBe(1);

View File

@ -7,24 +7,8 @@ import { fromPartial } from '@total-typescript/shoehorn';
describe('dockviewPanel', () => {
test('update title', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
onDidActiveChange: jest.fn(),
} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
@ -32,7 +16,11 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -67,23 +55,8 @@ describe('dockviewPanel', () => {
});
test('that .setTitle updates the title', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
onDidActiveChange: jest.fn(),
} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
@ -91,7 +64,10 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -117,22 +93,8 @@ describe('dockviewPanel', () => {
});
test('dispose cleanup', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest
@ -146,7 +108,11 @@ describe('dockviewPanel', () => {
.mockReturnValue({ dispose: jest.fn() }),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -169,22 +135,8 @@ describe('dockviewPanel', () => {
});
test('get params', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
@ -192,7 +144,11 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -215,22 +171,8 @@ describe('dockviewPanel', () => {
});
test('setSize propagates to underlying group', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
@ -239,7 +181,11 @@ describe('dockviewPanel', () => {
setSize: jest.fn(),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -256,27 +202,16 @@ describe('dockviewPanel', () => {
cut.api.setSize({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledTimes(1);
expect(group.api.setSize).toHaveBeenCalledWith({
height: 123,
width: 456,
});
expect(group.api.setSize).toHaveBeenCalledTimes(1);
});
test('updateParameter', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const api = fromPartial<DockviewApi>({});
const accessor = fromPartial<DockviewComponent>({});
const group = fromPartial<DockviewGroupPanel>({
api: {
onDidVisibilityChange: jest.fn(),
@ -284,7 +219,11 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(),
},
});
const model = <IDockviewPanelModel>new panelModelMock();
const model = fromPartial<IDockviewPanelModel>({
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel(
'fake-id',
@ -305,6 +244,9 @@ 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({
@ -315,6 +257,9 @@ 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: {
@ -335,5 +280,8 @@ describe('dockviewPanel', () => {
g: '',
h: null,
});
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '', e: null, g: '', h: null },
});
});
});

View File

@ -30,7 +30,6 @@ describe('dockviewGroupPanel', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'contentComponent':
@ -84,7 +83,6 @@ describe('dockviewGroupPanel', () => {
test('that the default tab is created', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'contentComponent':
@ -117,7 +115,6 @@ describe('dockviewGroupPanel', () => {
test('that the provided default tab is chosen when no implementation is provided', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
parentElement: document.createElement('div'),
defaultTabComponent: 'tabComponent',
createComponent(options) {
switch (options.name) {
@ -150,7 +147,6 @@ describe('dockviewGroupPanel', () => {
test('that is library default tab instance is created when no alternative exists', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'contentComponent':
@ -174,7 +170,6 @@ describe('dockviewGroupPanel', () => {
test('that the default content is created', () => {
accessorMock = fromPartial<DockviewComponent>({
options: {
parentElement: document.createElement('div'),
createComponent(options) {
switch (options.name) {
case 'contentComponent':

View File

@ -1,4 +1,5 @@
import {
disableIframePointEvents,
isInDocument,
quasiDefaultPrevented,
quasiPreventDefault,
@ -45,4 +46,38 @@ describe('dom', () => {
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

@ -3,7 +3,6 @@ import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
describe('events', () => {
@ -143,73 +142,6 @@ describe('events', () => {
expect(value).toBe(3);
});
it('addDisposableWindowListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableWindowListener(
element as any,
'mousedown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'mousedown',
handler,
true
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'mousedown',
handler,
true
);
});
it('addDisposableWindowListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableWindowListener(
element as any,
'mousedown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'mousedown',
handler,
undefined
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'mousedown',
handler,
undefined
);
});
it('addDisposableListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
@ -220,14 +152,14 @@ describe('events', () => {
const disposable = addDisposableListener(
element as any,
'mousedown',
'pointerdown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'mousedown',
'pointerdown',
handler,
true
);
@ -238,7 +170,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'mousedown',
'pointerdown',
handler,
true
);
@ -254,13 +186,13 @@ describe('events', () => {
const disposable = addDisposableListener(
element as any,
'mousedown',
'pointerdown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'mousedown',
'pointerdown',
handler,
undefined
);
@ -271,7 +203,74 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'mousedown',
'pointerdown',
handler,
undefined
);
});
it('addDisposableListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler,
true
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
true
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
true
);
});
it('addDisposableListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = jest.fn();
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler
);
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown',
handler,
undefined
);
expect(element.removeEventListener).toBeCalledTimes(0);
disposable.dispose();
expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith(
'pointerdown',
handler,
undefined
);

View File

@ -17,13 +17,9 @@ class TestPanel implements IGridPanelView {
_onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event;
get isActive(): boolean {
return true;
}
get params(): Parameters {
return {};
}
isVisible: boolean = true;
isActive: boolean = true;
params: Parameters = {};
constructor(
public readonly id: string,
@ -70,8 +66,10 @@ class TestPanel implements IGridPanelView {
}
class ClassUnderTest extends BaseGrid<TestPanel> {
constructor(options: BaseGridOptions) {
super(options);
readonly gridview = this.gridview;
constructor(parentElement: HTMLElement, options: BaseGridOptions) {
super(parentElement, options);
}
doRemoveGroup(
@ -107,9 +105,47 @@ 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({
parentElement: document.createElement('div'),
const cut = new ClassUnderTest(document.createElement('div'), {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});

View File

@ -5,6 +5,7 @@ import {
IGridView,
IViewSize,
SerializedGridview,
getGridLocation,
orthogonal,
} from '../../gridview/gridview';
import { Orientation, Sizing } from '../../splitview/splitview';
@ -18,7 +19,7 @@ class MockGridview implements IGridView {
IViewSize | undefined
>().event;
element: HTMLElement = document.createElement('div');
isVisible: boolean = true;
width: number = 0;
height: number = 0;
@ -1105,4 +1106,102 @@ describe('gridview', () => {
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,12 +32,40 @@ describe('gridview', () => {
container = document.createElement('div');
});
test('added views are visible by default', () => {
const gridview = new GridviewComponent({
parentElement: container,
test('update className', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
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, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -53,11 +81,17 @@ describe('gridview', () => {
});
test('remove panel', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -84,11 +118,17 @@ describe('gridview', () => {
});
test('active panel', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -145,13 +185,21 @@ describe('gridview', () => {
});
test('deserialize and serialize a layout', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
@ -196,6 +244,9 @@ 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')!;
@ -273,11 +324,17 @@ describe('gridview', () => {
});
test('toJSON shouldnt fire any layout events', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(1000, 1000);
@ -310,11 +367,17 @@ describe('gridview', () => {
});
test('gridview events', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -434,11 +497,17 @@ describe('gridview', () => {
test('dispose of gridviewComponent', () => {
expect(container.childNodes.length).toBe(0);
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -460,15 +529,21 @@ describe('gridview', () => {
gridview.dispose();
expect(container.childNodes.length).toBe(0);
expect(container.children.length).toBe(0);
});
test('#1/VERTICAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -523,11 +598,17 @@ describe('gridview', () => {
});
test('#2/HORIZONTAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -582,11 +663,17 @@ describe('gridview', () => {
});
test('#3/HORIZONTAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -659,11 +746,17 @@ describe('gridview', () => {
});
test('#4/HORIZONTAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -754,11 +847,17 @@ describe('gridview', () => {
});
test('#5/VERTICAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -849,11 +948,17 @@ describe('gridview', () => {
});
test('#5/VERTICAL/proportional/false', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -944,11 +1049,17 @@ describe('gridview', () => {
});
test('#6/VERTICAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -1069,11 +1180,17 @@ describe('gridview', () => {
});
test('#7/VERTICAL layout first', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -1194,11 +1311,17 @@ describe('gridview', () => {
});
test('#8/VERTICAL layout after', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -1321,11 +1444,17 @@ describe('gridview', () => {
});
test('#9/HORIZONTAL', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -1446,11 +1575,17 @@ describe('gridview', () => {
});
test('#9/HORIZONTAL/proportional/false', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(800, 400);
@ -1571,11 +1706,17 @@ describe('gridview', () => {
});
test('#10/HORIZONTAL scale x:1.5 y:2', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.fromJSON({
@ -1699,11 +1840,17 @@ describe('gridview', () => {
});
test('panel is disposed of when component is disposed', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(1000, 1000);
@ -1730,11 +1877,17 @@ describe('gridview', () => {
});
test('panel is disposed of when removed', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(1000, 1000);
@ -1760,11 +1913,17 @@ describe('gridview', () => {
});
test('panel is disposed of when fromJSON is called', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(1000, 1000);
@ -1799,11 +1958,17 @@ describe('gridview', () => {
test('fromJSON events should still fire', () => {
jest.useFakeTimers();
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
let addGroup: GridviewPanel[] = [];
@ -1922,11 +2087,17 @@ describe('gridview', () => {
test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(1600, 800);
@ -2049,11 +2220,17 @@ 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({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(6000, 5000);
@ -2325,11 +2502,17 @@ 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({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
gridview.layout(5000, 6000);
@ -2599,14 +2782,20 @@ describe('gridview', () => {
});
test('that loading a corrupt layout throws an error and leaves a clean gridview behind', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
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('.view-container');
let el = gridview.element.querySelector('.dv-view-container');
expect(el).toBeTruthy();
expect(el!.childNodes.length).toBe(0);
@ -2667,34 +2856,44 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
}).toThrow(
"Cannot create 'panel_1', no component 'somethingBad' provided"
);
}).toThrow("unsupported panel 'somethingBad'");
expect(gridview.groups.length).toBe(0);
el = gridview.element.querySelector('.view-container');
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({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
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({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
@ -2702,11 +2901,17 @@ describe('gridview', () => {
});
test('that setVisible toggles visiblity', () => {
const gridview = new GridviewComponent({
parentElement: container,
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
components: { default: TestGridview },
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);

View File

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

View File

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

View File

@ -0,0 +1,418 @@
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,14 +1,18 @@
import { Droptarget } from '../dnd/droptarget';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { Emitter } from '../events';
import { IRenderable, OverlayRenderContainer } from '../overlayRenderContainer';
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 { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('overlayRenderContainer', () => {
let referenceContainer: IRenderable;
let parentContainer: HTMLElement;
let cut: OverlayRenderContainer;
beforeEach(() => {
parentContainer = document.createElement('div');
@ -17,22 +21,28 @@ describe('overlayRenderContainer', () => {
element: document.createElement('div'),
dropTarget: fromPartial<Droptarget>({}),
};
cut = new OverlayRenderContainer(parentContainer);
});
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: {
@ -58,17 +68,25 @@ describe('overlayRenderContainer', () => {
});
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: {
@ -186,4 +204,62 @@ describe('overlayRenderContainer', () => {
referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(3);
});
test('related z-index from `aria-level` set on floating panels', async () => {
const group = fromPartial<DockviewGroupPanel>({});
const element = document.createElement('div');
element.setAttribute('aria-level', '2');
const spy = jest.spyOn(element, 'getAttribute');
const accessor = fromPartial<DockviewComponent>({
floatingGroups: [
{
group,
overlay: {
element,
},
},
],
});
const cut = new OverlayRenderContainer(parentContainer, accessor);
const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({
api: {
id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true,
group,
location: { type: 'floating' },
},
view: {
content: {
element: panelContentEl,
},
},
group: {
api: {
location: { type: 'floating' },
},
},
});
cut.attach({ panel, referenceContainer });
await exhaustMicrotaskQueue();
expect(spy).toHaveBeenCalledWith('aria-level');
expect(panelContentEl.parentElement!.style.zIndex).toBe(
'calc(var(--dv-overlay-z-index, 999) + 5)'
);
});
});

View File

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

View File

@ -4,19 +4,28 @@ import { PanelUpdateEvent } from '../../panel/types';
import { PaneviewComponent } from '../../paneview/paneviewComponent';
import {
PaneviewPanel,
IPaneBodyPart,
IPaneHeaderPart,
IPanePart,
PanePanelComponentInitParameter,
} from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel {
constructor(id: string, component: string) {
super(id, component, 'header', Orientation.VERTICAL, false, true);
super({
id,
component,
headerComponent: 'header',
orientation: Orientation.VERTICAL,
isExpanded: false,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
}
getHeaderComponent() {
return new (class Header implements IPaneHeaderPart {
return new (class Header implements IPanePart {
private _element: HTMLElement = document.createElement('div');
get element() {
@ -38,7 +47,7 @@ class TestPanel extends PaneviewPanel {
}
getBodyComponent() {
return new (class Header implements IPaneBodyPart {
return new (class Header implements IPanePart {
private _element: HTMLElement = document.createElement('div');
get element() {
@ -60,7 +69,7 @@ class TestPanel extends PaneviewPanel {
}
}
describe('componentPaneview', () => {
describe('paneviewComponent', () => {
let container: HTMLElement;
beforeEach(() => {
@ -68,13 +77,39 @@ describe('componentPaneview', () => {
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({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -82,12 +117,12 @@ describe('componentPaneview', () => {
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel2',
});
@ -144,13 +179,19 @@ describe('componentPaneview', () => {
});
test('serialization', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.fromJSON({
size: 6,
views: [
@ -158,7 +199,7 @@ describe('componentPaneview', () => {
size: 1,
data: {
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
},
expanded: true,
@ -167,7 +208,7 @@ describe('componentPaneview', () => {
size: 2,
data: {
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
},
expanded: false,
@ -176,13 +217,15 @@ describe('componentPaneview', () => {
size: 3,
data: {
id: 'panel3',
component: 'testPanel',
component: 'default',
title: 'Panel 3',
},
},
],
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.layout(400, 800);
const panel1 = paneview.getPanel('panel1');
@ -222,53 +265,57 @@ describe('componentPaneview', () => {
size: 756,
data: {
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
data: {
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
},
expanded: false,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
data: {
id: 'panel3',
component: 'testPanel',
component: 'default',
title: 'Panel 3',
},
expanded: false,
minimumSize: 100,
headerSize: 22,
},
],
});
});
test('toJSON shouldnt fire any layout events', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
});
@ -282,41 +329,15 @@ describe('componentPaneview', () => {
disposable.dispose();
});
test('dispose of paneviewComponent', () => {
expect(container.childNodes.length).toBe(0);
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',
});
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,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -324,12 +345,12 @@ describe('componentPaneview', () => {
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
});
@ -346,10 +367,14 @@ describe('componentPaneview', () => {
});
test('panel is disposed of when removed', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -357,12 +382,12 @@ describe('componentPaneview', () => {
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
});
@ -379,10 +404,14 @@ describe('componentPaneview', () => {
});
test('panel is disposed of when fromJSON is called', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -390,12 +419,12 @@ describe('componentPaneview', () => {
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
});
@ -412,10 +441,14 @@ describe('componentPaneview', () => {
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -428,16 +461,17 @@ describe('componentPaneview', () => {
size: 1,
data: {
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
},
minimumSize: 100,
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
},
expanded: true,
@ -446,7 +480,7 @@ describe('componentPaneview', () => {
size: 3,
data: {
id: 'panel3',
component: 'testPanel',
component: 'default',
title: 'Panel 3',
},
expanded: true,
@ -462,41 +496,46 @@ describe('componentPaneview', () => {
size: 122,
data: {
id: 'panel1',
component: 'testPanel',
component: 'default',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 122,
size: 22,
data: {
id: 'panel2',
component: 'testPanel',
component: 'default',
title: 'Panel 2',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 356,
size: 456,
data: {
id: 'panel3',
component: 'testPanel',
component: 'default',
title: 'Panel 3',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
],
});
});
test('that disableAutoResizing is false by default', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -504,10 +543,14 @@ describe('componentPaneview', () => {
});
test('that disableAutoResizing can be enabled', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
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,
});
@ -516,10 +559,14 @@ describe('componentPaneview', () => {
});
test('that setVisible toggles visiblity', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
default: TestPanel,
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,
});
@ -548,4 +595,25 @@ describe('componentPaneview', () => {
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(
'.split-view-container horizontal'
'.dv-split-view-container dv-horizontal'
);
expect(viewQuery).toBeTruthy();
@ -111,7 +111,7 @@ describe('splitview', () => {
expect(splitview.orientation).toBe(Orientation.VERTICAL);
const viewQuery = container.querySelectorAll(
'.split-view-container vertical'
'.dv-split-view-container dv-vertical'
);
expect(viewQuery).toBeTruthy();
@ -128,48 +128,48 @@ describe('splitview', () => {
splitview.addView(new Testview(50, 50));
let viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view'
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(3);
let sashQuery = container.querySelectorAll(
'.split-view-container > .sash-container > .sash'
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(2);
splitview.removeView(2);
viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view'
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(2);
sashQuery = container.querySelectorAll(
'.split-view-container > .sash-container > .sash'
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(1);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view'
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(1);
sashQuery = container.querySelectorAll(
'.split-view-container > .sash-container > .sash'
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(0);
splitview.removeView(0);
viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view'
'.dv-split-view-container > .dv-view-container > .dv-view'
);
expect(viewQuery.length).toBe(0);
sashQuery = container.querySelectorAll(
'.split-view-container > .sash-container > .sash'
'.dv-split-view-container > .dv-sash-container > .dv-sash'
);
expect(sashQuery.length).toBe(0);
@ -188,14 +188,14 @@ describe('splitview', () => {
splitview.addView(view2);
let viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view.visible'
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
);
expect(viewQuery.length).toBe(2);
splitview.setViewVisible(1, false);
viewQuery = container.querySelectorAll(
'.split-view-container > .view-container > .view.visible'
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
);
expect(viewQuery.length).toBe(1);
@ -619,7 +619,7 @@ describe('splitview', () => {
);
const sashElement = container
.getElementsByClassName('sash')
.getElementsByClassName('dv-sash')
.item(0) as HTMLElement;
// validate the expected state before drag
@ -772,4 +772,130 @@ describe('splitview', () => {
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,25 +26,52 @@ 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({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
const panel1 = splitview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
});
const panel2 = splitview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
});
splitview.movePanel(0, 1);
@ -66,18 +93,22 @@ describe('componentSplitview', () => {
});
test('remove panel', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'testPanel' });
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
@ -102,11 +133,15 @@ describe('componentSplitview', () => {
});
test('horizontal dimensions', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
@ -116,11 +151,15 @@ describe('componentSplitview', () => {
});
test('vertical dimensions', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
@ -130,18 +169,22 @@ describe('componentSplitview', () => {
});
test('api resize', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 600);
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'testPanel' });
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
splitview.addPanel({ id: 'panel3', component: 'default' });
const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!;
@ -183,16 +226,20 @@ describe('componentSplitview', () => {
});
test('api', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel1', component: 'default' });
const panel1 = splitview.getPanel('panel1');
@ -203,7 +250,7 @@ describe('componentSplitview', () => {
// expect(panel1?.api.isFocused).toBeFalsy();
expect(panel1!.api.isVisible).toBeTruthy();
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel2 = splitview.getPanel('panel2');
@ -225,18 +272,22 @@ describe('componentSplitview', () => {
test('vertical panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -277,18 +328,22 @@ describe('componentSplitview', () => {
test('horizontal panels', () => {
const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel1', component: 'default' });
splitview.addPanel({ id: 'panel2', component: 'default' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -327,51 +382,63 @@ describe('componentSplitview', () => {
});
test('serialization', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 6);
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'testPanel' },
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'testPanel' },
data: { id: 'panel2', component: 'default' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
{ size: 3, data: { id: 'panel3', component: 'default' } },
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
expect(splitview.length).toBe(3);
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [
{
size: 1,
data: { id: 'panel1', component: 'testPanel' },
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'testPanel' },
data: { id: 'panel2', component: 'default' },
snap: true,
},
{
size: 3,
data: { id: 'panel3', component: 'testPanel' },
data: { id: 'panel3', component: 'default' },
snap: false,
},
],
@ -382,11 +449,15 @@ describe('componentSplitview', () => {
});
test('toJSON shouldnt fire any layout events', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -394,11 +465,11 @@ describe('componentSplitview', () => {
splitview.addPanel({
id: 'panel1',
component: 'testPanel',
component: 'default',
});
splitview.addPanel({
id: 'panel2',
component: 'testPanel',
component: 'default',
});
const disposable = splitview.onDidLayoutChange(() => {
@ -411,41 +482,16 @@ describe('componentSplitview', () => {
disposable.dispose();
});
test('dispose of splitviewComponent', () => {
expect(container.childNodes.length).toBe(0);
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
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,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -473,11 +519,15 @@ describe('componentSplitview', () => {
});
test('panel is disposed of when removed', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -505,11 +555,15 @@ describe('componentSplitview', () => {
});
test('panel is disposed of when fromJSON is called', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -541,11 +595,15 @@ describe('componentSplitview', () => {
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.layout(400, 600);
@ -554,15 +612,15 @@ describe('componentSplitview', () => {
views: [
{
size: 1,
data: { id: 'panel1', component: 'testPanel' },
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'testPanel' },
data: { id: 'panel2', component: 'default' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
{ size: 3, data: { id: 'panel3', component: 'default' } },
],
size: 6,
orientation: Orientation.VERTICAL,
@ -573,17 +631,17 @@ describe('componentSplitview', () => {
views: [
{
size: 100,
data: { id: 'panel1', component: 'testPanel' },
data: { id: 'panel1', component: 'default' },
snap: false,
},
{
size: 200,
data: { id: 'panel2', component: 'testPanel' },
data: { id: 'panel2', component: 'default' },
snap: true,
},
{
size: 300,
data: { id: 'panel3', component: 'testPanel' },
data: { id: 'panel3', component: 'default' },
snap: false,
},
],
@ -594,11 +652,15 @@ describe('componentSplitview', () => {
});
test('that disableAutoResizing is false by default', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -606,11 +668,15 @@ describe('componentSplitview', () => {
});
test('that disableAutoResizing can be enabled', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
});
@ -619,11 +685,15 @@ describe('componentSplitview', () => {
});
test('that setVisible toggles visiblity', () => {
const splitview = new SplitviewComponent({
parentElement: container,
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
@ -649,4 +719,25 @@ describe('componentSplitview', () => {
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,10 +1,16 @@
import {
DockviewMaximizedGroupChanged,
FloatingGroupOptions,
IDockviewComponent,
MovePanelEvent,
PopoutGroupChangePositionEvent,
PopoutGroupChangeSizeEvent,
SerializedDockview,
} from '../dockview/dockviewComponent';
import {
AddGroupOptions,
AddPanelOptions,
DockviewComponentOptions,
DockviewDndOverlayEvent,
MovementOptions,
} from '../dockview/options';
@ -27,7 +33,6 @@ import {
AddSplitviewComponentOptions,
ISplitviewComponent,
SerializedSplitview,
SplitviewComponentUpdateOptions,
} from '../splitview/splitviewComponent';
import { IView, Orientation, Sizing } from '../splitview/splitview';
import { ISplitviewPanel } from '../splitview/splitviewPanel';
@ -35,9 +40,9 @@ import {
DockviewGroupPanel,
IDockviewGroupPanel,
} from '../dockview/dockviewGroupPanel';
import { Emitter, Event } from '../events';
import { Event } from '../events';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel';
import { PaneviewDidDropEvent } from '../paneview/draggablePaneviewPanel';
import {
GroupDragEvent,
TabDragEvent,
@ -48,6 +53,12 @@ import {
DockviewWillDropEvent,
WillShowOverlayLocationEvent,
} from '../dockview/dockviewGroupPanelModel';
import {
PaneviewComponentOptions,
PaneviewDndOverlayEvent,
} from '../paneview/options';
import { SplitviewComponentOptions } from '../splitview/options';
import { GridviewComponentOptions } from '../gridview/options';
export interface CommonApi<T = any> {
readonly height: number;
@ -59,6 +70,7 @@ export interface CommonApi<T = any> {
fromJSON(data: T): void;
toJSON(): T;
clear(): void;
dispose(): void;
}
export class SplitviewApi implements CommonApi<SerializedSplitview> {
@ -141,13 +153,6 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
constructor(private readonly component: ISplitviewComponent) {}
/**
* Update configuratable options.
*/
updateOptions(options: SplitviewComponentUpdateOptions): void {
this.component.updateOptions(options);
}
/**
* Removes an existing panel and optionally provide a `Sizing` method
* for the subsequent resize.
@ -211,6 +216,20 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
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> {
@ -280,19 +299,12 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
/**
* 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<PaneviewDropEvent> {
const emitter = new Emitter<PaneviewDropEvent>();
get onDidDrop(): Event<PaneviewDidDropEvent> {
return this.component.onDidDrop;
}
const disposable = this.component.onDidDrop((e) => {
emitter.fire({ ...e, api: this });
});
emitter.dispose = () => {
disposable.dispose();
emitter.dispose();
};
return emitter.event;
get onUnhandledDragOverEvent(): Event<PaneviewDndOverlayEvent> {
return this.component.onUnhandledDragOverEvent;
}
constructor(private readonly component: IPaneviewComponent) {}
@ -361,6 +373,20 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
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> {
@ -528,6 +554,17 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
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> {
@ -636,6 +673,10 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onDidRemovePanel;
}
get onDidMovePanel(): Event<MovePanelEvent> {
return this.component.onDidMovePanel;
}
/**
* Invoked after a layout is deserialzied using the `fromJSON` method.
*/
@ -700,6 +741,14 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onUnhandledDragOverEvent;
}
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
return this.component.onDidPopoutGroupSizeChange;
}
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
return this.component.onDidPopoutGroupPositionChange;
}
/**
* All panel objects.
*/
@ -800,9 +849,9 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
*/
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
options?: FloatingGroupOptions
): void {
return this.component.addFloatingGroup(item, coord);
return this.component.addFloatingGroup(item, options);
}
/**
@ -852,7 +901,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
this.component.exitMaximizedGroup();
}
get onDidMaximizedGroupChange(): Event<void> {
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> {
return this.component.onDidMaximizedGroupChange;
}
@ -867,7 +916,18 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
}
): Promise<void> {
): Promise<boolean> {
return this.component.addPopoutGroup(item, options);
}
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
}

View File

@ -6,9 +6,17 @@ import {
DockviewGroupLocation,
} from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events';
import { MutableDisposable } from '../lifecycle';
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>;
@ -17,7 +25,7 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi {
* If you require the Window object
*/
getWindow(): Window;
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void;
moveTo(options: DockviewGroupMoveParams): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
@ -28,12 +36,10 @@ export interface DockviewGroupPanelFloatingChangeEvent {
readonly location: DockviewGroupLocation;
}
// TODO find a better way to initialize and avoid needing null checks
const NOT_INITIALIZED_MESSAGE = 'DockviewGroupPanelApiImpl not initialized';
const NOT_INITIALIZED_MESSAGE =
'dockview: DockviewGroupPanelApiImpl not initialized';
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private readonly _mutableDisposable = new MutableDisposable();
private _group: DockviewGroupPanel | undefined;
readonly _onDidLocationChange =
@ -41,8 +47,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
private readonly _onDidActivePanelChange =
new Emitter<DockviewGroupChangeEvent>();
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
get location(): DockviewGroupLocation {
@ -57,8 +62,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
this.addDisposables(
this._onDidLocationChange,
this._onDidActivePanelChange,
this._mutableDisposable
this._onDidActivePanelChange
);
}
@ -75,7 +79,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
: window;
}
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void {
moveTo(options: DockviewGroupMoveParams): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
@ -94,6 +98,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
position: options.group
? options.position ?? 'center'
: 'center',
index: options.index,
},
});
}
@ -131,19 +136,5 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
initialize(group: DockviewGroupPanel): void {
this._group = group;
/**
* TODO: Annoying initialization order caveat
*
* Due to the order on initialization we know that the model isn't defined until later in the same stack-frame of setup.
* By queuing a microtask we can ensure the setup is completed within the same stack-frame, but after everything else has
* finished ensuring the `model` is defined.
*/
queueMicrotask(() => {
this._mutableDisposable.value =
this._group!.model.onDidActivePanelChange((event) => {
this._onDidActivePanelChange.fire(event);
});
});
}
}

View File

@ -4,9 +4,11 @@ import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget';
import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import {
DockviewGroupMoveParams,
DockviewGroupPanelFloatingChangeEvent,
} from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
export interface TitleEvent {
@ -25,6 +27,8 @@ export interface GroupChangedEvent {
// empty
}
export type DockviewPanelMoveParams = DockviewGroupMoveParams;
export interface DockviewPanelApi
extends Omit<
GridviewPanelApi,
@ -50,11 +54,7 @@ export interface DockviewPanelApi
close(): void;
setTitle(title: string): void;
setRenderer(renderer: DockviewPanelRenderer): void;
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void;
moveTo(options: DockviewPanelMoveParams): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
@ -69,7 +69,7 @@ export class DockviewPanelApiImpl
implements DockviewPanelApi
{
private _group: DockviewGroupPanel;
private _tabComponent: string | undefined;
private readonly _tabComponent: string | undefined;
readonly _onDidTitleChange = new Emitter<TitleEvent>();
readonly onDidTitleChange = this._onDidTitleChange.event;
@ -131,7 +131,7 @@ export class DockviewPanelApiImpl
}
constructor(
private panel: DockviewPanel,
private readonly panel: DockviewPanel,
group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
component: string,
@ -160,16 +160,14 @@ export class DockviewPanelApiImpl
return this.group.api.getWindow();
}
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void {
moveTo(options: DockviewPanelMoveParams): void {
this.accessor.moveGroupOrPanel({
from: { groupId: this._group.id, panelId: this.panel.id },
to: {
group: options.group,
position: options.position ?? 'center',
group: options.group ?? this._group,
position: options.group
? options.position ?? 'center'
: 'center',
index: options.index,
},
});
@ -204,13 +202,14 @@ export class DockviewPanelApiImpl
this.groupEventsDisposable.value = new CompositeDisposable(
this.group.api.onDidVisibilityChange((event) => {
if (!event.isVisible && this.isVisible) {
this._onDidVisibilityChange.fire(event);
} else if (
event.isVisible &&
!this.isVisible &&
this.group.model.isPanelActive(this.panel)
) {
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);
}
}),

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { getElementsByTagName } from '../dom';
import { disableIframePointEvents } from '../dom';
import { addDisposableListener, Emitter } from '../events';
import {
CompositeDisposable,
@ -40,23 +40,14 @@ export abstract class DragHandler extends CompositeDisposable {
return;
}
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
const iframes = disableIframePointEvents();
this.pointerEventsDisposable.value = {
dispose: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
iframes.release();
},
};
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
@ -76,18 +67,17 @@ export abstract class DragHandler extends CompositeDisposable {
* 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
P * 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__'
);
event.dataTransfer.setData('text/plain', '');
}
}
}),
addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose();
this.dataDisposable.dispose();
setTimeout(() => {
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
})
);
}

View File

@ -1,4 +1,6 @@
class TransferObject {}
class TransferObject {
// intentionally empty class
}
export class PanelTransfer extends TransferObject {
constructor(

View File

@ -13,8 +13,8 @@ export class DragAndDropObserver extends CompositeDisposable {
private target: EventTarget | null = null;
constructor(
private element: HTMLElement,
private callbacks: IDragAndDropObserverCallbacks
private readonly element: HTMLElement,
private readonly callbacks: IDragAndDropObserverCallbacks
) {
super();

View File

@ -0,0 +1,23 @@
.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

@ -0,0 +1,102 @@
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,7 +1,8 @@
.drop-target {
.dv-drop-target {
position: relative;
--dv-transition-duration: 70ms;
> .drop-target-dropzone {
> .dv-drop-target-dropzone {
position: absolute;
left: 0px;
top: 0px;
@ -10,15 +11,18 @@
z-index: 1000;
pointer-events: none;
> .drop-target-selection {
> .dv-drop-target-selection {
position: relative;
box-sizing: border-box;
height: 100%;
width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s ease-out;
transition: top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out,
opacity var(--dv-transition-duration) ease-out;
will-change: transform;
pointer-events: none;

View File

@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = {
const SMALL_WIDTH_BOUNDARY = 100;
const SMALL_HEIGHT_BOUNDARY = 100;
export interface DropTargetTargetModel {
getElements(
event?: DragEvent,
outline?: HTMLElement
): {
root: HTMLElement;
overlay: HTMLElement;
changed: boolean;
};
exists(): boolean;
clear(): void;
}
export interface DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
}
export class Droptarget extends CompositeDisposable {
@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable {
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;
}
@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable {
) {
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: () => undefined,
onDragEnter: () => {
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
const width = this.element.clientWidth;
const height = this.element.clientHeight;
const target =
this.options.getOverlayOutline?.() ?? this.element;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable {
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const x = (e.clientX ?? 0) - rect.left;
const y = (e.clientY ?? 0) - rect.top;
const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet,
@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable {
}
if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
@ -194,16 +239,26 @@ export class Droptarget extends CompositeDisposable {
this.markAsUsed(e);
if (!this.targetElement) {
if (overrideTraget) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';
this.targetElement.className = 'dv-drop-target-dropzone';
this.overlayElement = document.createElement('div');
this.overlayElement.className = 'drop-target-selection';
this.overlayElement.className = 'dv-drop-target-selection';
this._state = 'center';
this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('drop-target');
this.element.append(this.targetElement);
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);
@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable {
this._state = quadrant;
},
onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget();
},
onDragEnd: () => {
onDragEnd: (e) => {
const target = this.options.getOverrideTarget?.();
if (target && Droptarget.ACTUAL_TARGET === this) {
if (this._state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({
position: this._state,
nativeEvent: e,
});
}
}
this.removeDropTarget();
target?.clear();
},
onDrop: (e) => {
e.preventDefault();
@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable {
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
@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): void {
if (!this.overlayElement) {
const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
return;
}
@ -300,6 +381,103 @@ export class Droptarget extends CompositeDisposable {
}
}
if (target) {
const outlineEl =
this.options.getOverlayOutline?.() ?? this.element;
const elBox = outlineEl.getBoundingClientRect();
const ta = target.getElements(undefined, outlineEl);
const el = ta.root;
const overlay = ta.overlay;
const bigbox = el.getBoundingClientRect();
const rootTop = elBox.top - bigbox.top;
const rootLeft = elBox.left - bigbox.left;
const box = {
top: rootTop,
left: rootLeft,
width: width,
height: height,
};
if (rightClass) {
box.left = rootLeft + width * (1 - size);
box.width = width * size;
} else if (leftClass) {
box.width = width * size;
} else if (topClass) {
box.height = height * size;
} else if (bottomClass) {
box.top = rootTop + height * (1 - size);
box.height = height * size;
}
if (isSmallX && isLeft) {
box.width = 4;
}
if (isSmallX && isRight) {
box.left = rootLeft + width - 4;
box.width = 4;
}
const topPx = `${Math.round(box.top)}px`;
const leftPx = `${Math.round(box.left)}px`;
const widthPx = `${Math.round(box.width)}px`;
const heightPx = `${Math.round(box.height)}px`;
if (
overlay.style.top === topPx &&
overlay.style.left === leftPx &&
overlay.style.width === widthPx &&
overlay.style.height === heightPx
) {
return;
}
overlay.style.top = topPx;
overlay.style.left = leftPx;
overlay.style.width = widthPx;
overlay.style.height = heightPx;
overlay.style.visibility = 'visible';
overlay.className = `dv-drop-target-anchor${
this.options.className ? ` ${this.options.className}` : ''
}`;
toggleClass(overlay, 'dv-drop-target-left', isLeft);
toggleClass(overlay, 'dv-drop-target-right', isRight);
toggleClass(overlay, 'dv-drop-target-top', isTop);
toggleClass(overlay, 'dv-drop-target-bottom', isBottom);
toggleClass(
overlay,
'dv-drop-target-center',
quadrant === 'center'
);
if (ta.changed) {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
true
);
setTimeout(() => {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
false
);
}, 10);
}
return;
}
if (!this.overlayElement) {
return;
}
const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
/**
@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable {
private removeDropTarget(): void {
if (this.targetElement) {
this._state = undefined;
this.element.removeChild(this.targetElement);
this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined;
this.overlayElement = undefined;
this.element.classList.remove('drop-target');
}
}
}

View File

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

View File

@ -21,7 +21,7 @@ export class GroupDragHandler extends DragHandler {
this.addDisposables(
addDisposableListener(
element,
'mousedown',
'pointerdown',
(e) => {
if (e.shiftKey) {
/**
@ -72,9 +72,11 @@ 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);
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 });
}
return {

View File

@ -28,9 +28,9 @@ export class ContentContainer
extends CompositeDisposable
implements IContentContainer
{
private _element: HTMLElement;
private readonly _element: HTMLElement;
private panel: IDockviewPanel | undefined;
private disposable = new MutableDisposable();
private readonly disposable = new MutableDisposable();
private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
@ -50,12 +50,20 @@ export class ContentContainer
) {
super();
this._element = document.createElement('div');
this._element.className = 'content-container';
this._element.className = 'dv-content-container';
this._element.tabIndex = -1;
this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, {
getOverlayOutline: () => {
return accessor.options.theme?.dndPanelOverlay === 'group'
? this.element.parentElement
: null;
},
className: 'dv-drop-target-content',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (
@ -76,26 +84,12 @@ export class ContentContainer
}
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.group.id) {
if (position === 'center') {
// don't allow to drop on self for center position
return false;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return false;
}
}
const groupHasOnePanelAndIsActiveDragElement =
this.group.panels.length === 1 &&
data.groupId === this.group.id;
return !groupHasOnePanelAndIsActiveDragElement;
return true;
}
return this.group.canDisplayOverlay(event, position, 'content');
},
getOverrideTarget: target ? () => target.model : undefined,
});
this.addDisposables(this.dropTarget);
@ -154,6 +148,10 @@ export class ContentContainer
referenceContainer: this,
});
break;
default:
throw new Error(
`dockview: invalid renderer type '${panel.api.renderer}'`
);
}
if (doRender) {

View File

@ -0,0 +1,87 @@
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,7 +6,7 @@
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
}
.tab {
.dv-tab {
flex-shrink: 0;
&:focus-within,
@ -33,7 +33,7 @@
}
}
&.active-tab {
&.dv-active-tab {
.dv-default-tab {
.dv-default-tab-action {
visibility: visible;
@ -41,7 +41,7 @@
}
}
&.inactive-tab {
&.dv-inactive-tab {
.dv-default-tab {
.dv-default-tab-action {
visibility: hidden;
@ -58,15 +58,13 @@
position: relative;
height: 100%;
display: flex;
min-width: 80px;
align-items: center;
padding: 0px 8px;
white-space: nowrap;
text-overflow: elipsis;
text-overflow: ellipsis;
.dv-default-tab-content {
padding: 0px 8px;
flex-grow: 1;
margin-right: 4px;
}
.dv-default-tab-action {

View File

@ -1,16 +1,13 @@
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 _element: HTMLElement;
private _content: HTMLElement;
private action: HTMLElement;
//
private params: GroupPanelPartInitParameters = {} as any;
private readonly _element: HTMLElement;
private readonly _content: HTMLElement;
private readonly action: HTMLElement;
private _title: string | undefined;
get element(): HTMLElement {
return this._element;
@ -21,7 +18,7 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this._element = document.createElement('div');
this._element.className = 'dv-default-tab';
//
this._content = document.createElement('div');
this._content.className = 'dv-default-tab-content';
@ -29,53 +26,39 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this.action.className = 'dv-default-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.addDisposables(
addDisposableListener(this.action, 'mousedown', (ev) => {
params.api.onDidTitleChange((event) => {
this._title = event.title;
this.render();
}),
addDisposableListener(this.action, 'pointerdown', (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.params.title) {
this._content.textContent = this.params.title;
if (this._content.textContent !== this._title) {
this._content.textContent = this._title ?? '';
}
}
}

View File

@ -12,11 +12,11 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import {
DroptargetEvent,
Droptarget,
Position,
WillShowOverlayEvent,
} from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler {
private readonly panelTransfer =
@ -50,8 +50,8 @@ export class Tab extends CompositeDisposable {
private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined;
private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onPointDown = new Emitter<MouseEvent>();
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
@ -73,11 +73,11 @@ export class Tab extends CompositeDisposable {
super();
this._element = document.createElement('div');
this._element.className = 'tab';
this._element.className = 'dv-tab';
this._element.tabIndex = 0;
this._element.draggable = true;
toggleClass(this.element, 'inactive-tab', true);
toggleClass(this.element, 'dv-inactive-tab', true);
const dragHandler = new TabDragHandler(
this._element,
@ -87,7 +87,8 @@ export class Tab extends CompositeDisposable {
);
this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
acceptedTargetZones: ['left', 'right'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
@ -96,15 +97,7 @@ export class Tab extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panel.id !== data.panelId;
return true;
}
return this.group.model.canDisplayOverlay(
@ -113,24 +106,38 @@ export class Tab extends CompositeDisposable {
'tab'
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables(
this._onChanged,
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, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
}
this._onChanged.fire(event);
addDisposableListener(this._element, 'pointerdown', (event) => {
this._onPointDown.fire(event);
}),
this.dropTarget.onDrop((event) => {
this._onDropped.fire(event);
@ -140,8 +147,8 @@ export class Tab extends CompositeDisposable {
}
public setActive(isActive: boolean): void {
toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'inactive-tab', !isActive);
toggleClass(this.element, 'dv-active-tab', isActive);
toggleClass(this.element, 'dv-inactive-tab', !isActive);
}
public setContent(part: ITabRenderer): void {

View File

@ -0,0 +1,19 @@
.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

@ -0,0 +1,25 @@
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

@ -0,0 +1,79 @@
.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

@ -0,0 +1,301 @@
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 @@
.tabs-and-actions-container {
.dv-tabs-and-actions-container {
display: flex;
background-color: var(--dv-tabs-and-actions-container-background-color);
flex-shrink: 0;
@ -6,70 +6,32 @@
height: var(--dv-tabs-and-actions-container-height);
font-size: var(--dv-tabs-and-actions-container-font-size);
&.hidden {
display: none;
}
&.dv-single-tab.dv-full-width-single-tab {
.tabs-container {
flex-grow: 1;
.tab {
.dv-scrollable {
flex-grow: 1;
}
}
}
.void-container {
flex-grow: 0;
}
.dv-tabs-container {
flex-grow: 1;
.dv-tab {
flex-grow: 1;
padding: 0px;
}
}
.dv-void-container {
flex-grow: 0;
}
}
.void-container {
.dv-void-container {
display: flex;
flex-grow: 1;
cursor: grab;
}
.tabs-container {
.dv-right-actions-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,21 +1,23 @@
import {
IDisposable,
CompositeDisposable,
IValueDisposable,
Disposable,
MutableDisposable,
} from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { Tab } from '../tab/tab';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { findRelativeZIndexParent, toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayEvent } from '../../../dnd/droptarget';
import {
DockviewGroupDropLocation,
WillShowOverlayLocationEvent,
} from '../../dockviewGroupPanelModel';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import {
createDropdownElementHandle,
DropdownElement,
} from './tabOverflowControl';
export interface TabDropIndexEvent {
readonly event: DragEvent;
@ -60,25 +62,28 @@ export class TabsContainer
implements ITabsContainer
{
private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement;
private readonly tabs: Tabs;
private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<Tab>[] = [];
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;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
get onTabDragStart(): Event<TabDragEvent> {
return this.tabs.onTabDragStart;
}
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> =
@ -90,11 +95,11 @@ export class TabsContainer
this._onWillShowOverlay.event;
get panels(): string[] {
return this.tabs.map((_) => _.value.panel.id);
return this.tabs.panels;
}
get size(): number {
return this.tabs.length;
return this.tabs.size;
}
get hidden(): boolean {
@ -106,6 +111,118 @@ 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 = '';
@ -158,289 +275,129 @@ export class TabsContainer
}
}
get element(): HTMLElement {
return this._element;
isActive(tab: Tab): boolean {
return this.tabs.isActive(tab);
}
public isActive(tab: Tab): 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.panel.id === id);
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = '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 = 'right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'pre-actions-container';
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
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
);
}
}),
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
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.length,
});
}),
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,
'mousedown',
(event) => {
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 }
);
}
}
),
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
}
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
})
);
}
public setActive(_isGroupActive: boolean) {
setActive(_isGroupActive: boolean) {
// noop
}
private addTab(
tab: IValueDisposable<Tab>,
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;
}
delete(id: string): void {
this.tabs.delete(id);
this.updateClassnames();
}
public delete(id: string): void {
const index = this.tabs.findIndex((tab) => tab.value.panel.id === id);
const tabToRemove = this.tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
setActivePanel(panel: IDockviewPanel): void {
this.tabs.setActivePanel(panel);
}
public setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
});
openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void {
this.tabs.openPanel(panel, index);
this.updateClassnames();
}
public 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);
if (!panel.view?.tab) {
throw new Error('invalid header component');
}
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onChanged((event) => {
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;
}
const isLeftClick = event.button === 0;
if (!isLeftClick || event.defaultPrevented) {
return;
}
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
}),
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);
}
public closePanel(panel: IDockviewPanel): void {
closePanel(panel: IDockviewPanel): void {
this.delete(panel.id);
}
public dispose(): void {
super.dispose();
private updateClassnames(): void {
toggleClass(this._element, 'dv-single-tab', this.size === 1);
}
for (const { value, disposable } of this.tabs) {
disposable.dispose();
value.dispose();
private toggleDropdown(options: { tabs: string[]; reset: boolean }): void {
const tabs = options.reset ? [] : options.tabs;
this._overflowTabs = tabs;
if (this._overflowTabs.length > 0 && this.dropdownPart) {
this.dropdownPart.update({ tabs: tabs.length });
return;
}
this.tabs = [];
if (this._overflowTabs.length === 0) {
this._dropdownDisposable.dispose();
return;
}
const root = document.createElement('div');
root.className = 'dv-tabs-overflow-dropdown-root';
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;
}),
addDisposableListener(
root,
'pointerdown',
(event) => {
event.preventDefault();
},
{ capture: 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);
}
const relativeParent = findRelativeZIndexParent(root);
this.accessor.popupService.openPopover(el, {
x: event.clientX,
y: event.clientY,
zIndex: relativeParent?.style.zIndex
? `calc(${relativeParent.style.zIndex} * 2)`
: undefined,
});
})
);
}
}

View File

@ -1,4 +1,3 @@
import { last } from '../../../array';
import { getPanelData } from '../../../dnd/dataTransfer';
import {
Droptarget,
@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement;
@ -35,14 +35,13 @@ export class VoidContainer extends CompositeDisposable {
this._element = document.createElement('div');
this._element.className = 'void-container';
this._element.tabIndex = 0;
this._element.className = 'dv-void-container';
this._element.draggable = true;
this.addDisposables(
this._onDrop,
this._onDragStart,
addDisposableListener(this._element, 'click', () => {
addDisposableListener(this._element, 'pointerdown', () => {
this.accessor.doSetGroupActive(this.group);
})
);
@ -55,16 +54,7 @@ export class VoidContainer extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
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 true;
}
return group.model.canDisplayOverlay(
@ -73,6 +63,7 @@ export class VoidContainer extends CompositeDisposable {
'header_space'
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;

View File

@ -1,42 +1,4 @@
.watermark {
.dv-watermark {
display: flex;
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;
.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);
}
}
}
height: 100%;
}

View File

@ -2,21 +2,13 @@ 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 _element: HTMLElement;
private _group: DockviewGroupPanel | undefined;
private _api: DockviewApi | undefined;
private readonly _element: HTMLElement;
get element(): HTMLElement {
return this._element;
@ -25,70 +17,10 @@ export class Watermark
constructor() {
super();
this._element = document.createElement('div');
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
this._element.className = 'dv-watermark';
}
init(_params: WatermarkRendererInitParameters): void {
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);
// noop
}
}

View File

@ -57,6 +57,10 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
view,
{
renderer: panelData.renderer,
minimumWidth: panelData.minimumWidth,
minimumHeight: panelData.minimumHeight,
maximumWidth: panelData.maximumWidth,
maximumHeight: panelData.maximumHeight,
}
);

View File

@ -14,64 +14,42 @@
.dv-overlay-render-container {
position: relative;
}
.split-view-container {
&.horizontal {
> .view-container > .view {
&:not(:last-child) {
border-right: var(--dv-group-gap-size) solid transparent;
}
&:not(:first-child) {
border-left: var(--dv-group-gap-size) solid transparent;
}
}
}
&.vertical {
> .view-container > .view {
&:not(:last-child) {
border-bottom: var(--dv-group-gap-size) solid transparent;
}
&:not(:first-child) {
border-top: var(--dv-group-gap-size) solid transparent;
}
}
}
}
}
.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-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);
}
}
}
}
&.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);
&.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);
}
}
}
}
@ -81,7 +59,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
**/
.tab {
.dv-tab {
&.dv-tab-dragging {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,11 @@
import { Overlay } from '../dnd/overlay';
import { Overlay } from '../overlay/overlay';
import { CompositeDisposable } from '../lifecycle';
import { AnchoredBox } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
export interface IDockviewFloatingGroupPanel {
readonly group: IDockviewGroupPanel;
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void;
position(bounds: Partial<AnchoredBox>): void;
}
export class DockviewFloatingGroupPanel
@ -20,18 +14,10 @@ export class DockviewFloatingGroupPanel
{
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super();
this.addDisposables(overlay);
}
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void {
position(bounds: Partial<AnchoredBox>): void {
this.overlay.setBounds(bounds);
}
}

View File

@ -1,4 +1,4 @@
.groupview {
.dv-groupview {
display: flex;
flex-direction: column;
height: 100%;
@ -9,13 +9,7 @@
outline: none;
}
&.empty {
> .tabs-and-actions-container {
display: none;
}
}
> .content-container {
> .dv-content-container {
flex-grow: 1;
min-height: 0;
outline: none;

View File

@ -34,6 +34,38 @@ export class DockviewGroupPanel
{
private readonly _model: DockviewGroupPanelModel;
override get minimumWidth(): number {
const activePanelMinimumWidth = this.activePanel?.minimumWidth;
if (typeof activePanelMinimumWidth === 'number') {
return activePanelMinimumWidth;
}
return super.__minimumWidth();
}
override get minimumHeight(): number {
const activePanelMinimumHeight = this.activePanel?.minimumHeight;
if (typeof activePanelMinimumHeight === 'number') {
return activePanelMinimumHeight;
}
return super.__minimumHeight();
}
override get maximumWidth(): number {
const activePanelMaximumWidth = this.activePanel?.maximumWidth;
if (typeof activePanelMaximumWidth === 'number') {
return activePanelMaximumWidth;
}
return super.__maximumWidth();
}
override get maximumHeight(): number {
const activePanelMaximumHeight = this.activePanel?.maximumHeight;
if (typeof activePanelMaximumHeight === 'number') {
return activePanelMaximumHeight;
}
return super.__maximumHeight();
}
get panels(): IDockviewPanel[] {
return this._model.panels;
}
@ -71,8 +103,14 @@ export class DockviewGroupPanel
id,
'groupview_default',
{
minimumHeight: MINIMUM_DOCKVIEW_GROUP_PANEL_HEIGHT,
minimumWidth: MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH,
minimumHeight:
options.constraints?.minimumHeight ??
MINIMUM_DOCKVIEW_GROUP_PANEL_HEIGHT,
minimumWidth:
options.constraints?.maximumHeight ??
MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH,
maximumHeight: options.constraints?.maximumHeight,
maximumWidth: options.constraints?.maximumWidth,
},
new DockviewGroupPanelApiImpl(id, accessor)
);
@ -86,6 +124,12 @@ export class DockviewGroupPanel
options,
this
);
this.addDisposables(
this.model.onDidActivePanelChange((event) => {
this.api._onDidActivePanelChange.fire(event);
})
);
}
override focus(): void {

View File

@ -36,8 +36,10 @@ import {
DockviewUnhandledDragOverEvent,
IHeaderActionsRenderer,
} from './options';
import { OverlayRenderContainer } from '../overlayRenderContainer';
import { OverlayRenderContainer } from '../overlay/overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi';
import { Contraints } from '../gridview/gridviewPanel';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
interface GroupMoveEvent {
groupId: string;
@ -50,6 +52,9 @@ interface CoreGroupOptions {
locked?: DockviewGroupPanelLocked;
hideHeader?: boolean;
skipSetActive?: boolean;
constraints?: Partial<Contraints>;
initialWidth?: number;
initialHeight?: number;
}
export interface GroupOptions extends CoreGroupOptions {
@ -192,7 +197,7 @@ export interface IDockviewGroupPanelModel extends IPanel {
export type DockviewGroupLocation =
| { type: 'grid' }
| { type: 'floating' }
| { type: 'popout'; getWindow: () => Window };
| { type: 'popout'; getWindow: () => Window; popoutUrl?: string };
export class WillShowOverlayLocationEvent implements IDockviewEvent {
get kind(): DockviewGroupDropLocation {
@ -260,6 +265,9 @@ export class DockviewGroupPanelModel
private _location: DockviewGroupLocation = { type: 'grid' };
private mostRecentlyUsed: IDockviewPanel[] = [];
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
private _overwriteDropTargetContainer: DropTargetAnchorContainer | null =
null;
private readonly _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange: Event<IViewSize | undefined> =
@ -268,7 +276,7 @@ export class DockviewGroupPanelModel
private _width = 0;
private _height = 0;
private _panels: IDockviewPanel[] = [];
private readonly _panels: IDockviewPanel[] = [];
private readonly _panelDisposables = new Map<string, IDisposable>();
private readonly _onMove = new Emitter<GroupMoveEvent>();
@ -322,7 +330,7 @@ export class DockviewGroupPanelModel
private readonly _api: DockviewApi;
get element(): HTMLElement {
throw new Error('not supported');
throw new Error('dockview: not supported');
}
get activePanel(): IDockviewPanel | undefined {
@ -338,7 +346,7 @@ export class DockviewGroupPanelModel
toggleClass(
this.container,
'locked-groupview',
'dv-locked-groupview',
value === 'no-drop-target' || value
);
}
@ -425,14 +433,14 @@ export class DockviewGroupPanelModel
constructor(
private readonly container: HTMLElement,
private accessor: DockviewComponent,
private readonly accessor: DockviewComponent,
public id: string,
private readonly options: GroupOptions,
private readonly groupPanel: DockviewGroupPanel
) {
super();
toggleClass(this.container, 'groupview', true);
toggleClass(this.container, 'dv-groupview', true);
this._api = new DockviewApi(this.accessor);
@ -501,7 +509,9 @@ export class DockviewGroupPanelModel
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidActivePanelChange,
this._onUnhandledDragOverEvent
this._onUnhandledDragOverEvent,
this._onDidPanelTitleChange,
this._onDidPanelParametersChange
);
}
@ -509,8 +519,6 @@ export class DockviewGroupPanelModel
this.contentContainer.element.focus();
}
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
set renderContainer(value: OverlayRenderContainer | null) {
this.panels.forEach((panel) => {
this.renderContainer.detatch(panel);
@ -530,6 +538,17 @@ export class DockviewGroupPanelModel
);
}
set dropTargetContainer(value: DropTargetAnchorContainer | null) {
this._overwriteDropTargetContainer = value;
}
get dropTargetContainer(): DropTargetAnchorContainer | null {
return (
this._overwriteDropTargetContainer ??
this.accessor.rootDropTargetContainer
);
}
initialize(): void {
if (this.options.panels) {
this.options.panels.forEach((panel) => {
@ -784,7 +803,15 @@ export class DockviewGroupPanelModel
}
private doClose(panel: IDockviewPanel): void {
this.accessor.removePanel(panel);
const isLast =
this.panels.length === 1 && this.accessor.groups.length === 1;
this.accessor.removePanel(
panel,
isLast && this.accessor.options.noPanelsOverlay === 'emptyGroup'
? { removeEmptyGroup: false }
: undefined
);
}
public isPanelActive(panel: IDockviewPanel): boolean {
@ -802,8 +829,8 @@ export class DockviewGroupPanelModel
this._isGroupActive = isGroupActive;
toggleClass(this.container, 'active-group', isGroupActive);
toggleClass(this.container, 'inactive-group', !isGroupActive);
toggleClass(this.container, 'dv-active-group', isGroupActive);
toggleClass(this.container, 'dv-inactive-group', !isGroupActive);
this.tabsContainer.setActive(this.isActive);
@ -952,8 +979,6 @@ export class DockviewGroupPanelModel
}
private updateContainer(): void {
toggleClass(this.container, 'empty', this.isEmpty);
this.panels.forEach((panel) => panel.runEvents());
if (this.isEmpty && !this.watermark) {
@ -964,22 +989,18 @@ export class DockviewGroupPanelModel
});
this.watermark = watermark;
addDisposableListener(this.watermark.element, 'click', () => {
addDisposableListener(this.watermark.element, 'pointerdown', () => {
if (!this.isActive) {
this.accessor.doSetGroupActive(this.groupPanel);
}
});
this.tabsContainer.hide();
this.contentContainer.element.appendChild(this.watermark.element);
this.watermark.updateParentGroup(this.groupPanel, true);
}
if (!this.isEmpty && this.watermark) {
this.watermark.element.remove();
this.watermark.dispose?.();
this.watermark = undefined;
this.tabsContainer.show();
}
}
@ -993,7 +1014,7 @@ export class DockviewGroupPanelModel
target,
position,
getPanelData,
this.accessor.getPanel(this.id)!
this.accessor.getPanel(this.id)
);
this._onUnhandledDragOverEvent.fire(firedEvent);
@ -1042,6 +1063,29 @@ export class DockviewGroupPanelModel
const data = getPanelData();
if (data && data.viewId === this.accessor.id) {
if (type === 'content') {
if (data.groupId === this.id) {
// don't allow to drop on self for center position
if (position === 'center') {
return;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return;
}
}
}
if (type === 'header') {
if (data.groupId === this.id) {
if (data.panelId === null) {
return;
}
}
}
if (data.panelId === null) {
// this is a group move dnd event
const { groupId } = data;

View File

@ -9,8 +9,9 @@ import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
import { IDockviewPanelModel } from './dockviewPanelModel';
import { DockviewComponent } from './dockviewComponent';
import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { WillFocusEvent } from '../api/panelApi';
import { Contraints } from '../gridview/gridviewPanel';
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view: IDockviewPanelModel;
@ -18,6 +19,10 @@ export interface IDockviewPanel extends IDisposable, IPanel {
readonly api: DockviewPanelApi;
readonly title: string | undefined;
readonly params: Parameters | undefined;
readonly minimumWidth?: number;
readonly minimumHeight?: number;
readonly maximumWidth?: number;
readonly maximumHeight?: number;
updateParentGroup(
group: DockviewGroupPanel,
options?: { skipSetActive?: boolean }
@ -40,6 +45,11 @@ export class DockviewPanel
private _title: string | undefined;
private _renderer: DockviewPanelRenderer | undefined;
private readonly _minimumWidth: number | undefined;
private readonly _minimumHeight: number | undefined;
private readonly _maximumWidth: number | undefined;
private readonly _maximumHeight: number | undefined;
get params(): Parameters | undefined {
return this._params;
}
@ -56,6 +66,22 @@ export class DockviewPanel
return this._renderer ?? this.accessor.renderer;
}
get minimumWidth(): number | undefined {
return this._minimumWidth;
}
get minimumHeight(): number | undefined {
return this._minimumHeight;
}
get maximumWidth(): number | undefined {
return this._maximumWidth;
}
get maximumHeight(): number | undefined {
return this._maximumHeight;
}
constructor(
public readonly id: string,
component: string,
@ -64,11 +90,15 @@ export class DockviewPanel
private readonly containerApi: DockviewApi,
group: DockviewGroupPanel,
readonly view: IDockviewPanelModel,
options: { renderer?: DockviewPanelRenderer }
options: { renderer?: DockviewPanelRenderer } & Partial<Contraints>
) {
super();
this._renderer = options.renderer;
this._group = group;
this._minimumWidth = options.minimumWidth;
this._minimumHeight = options.minimumHeight;
this._maximumWidth = options.maximumWidth;
this._maximumHeight = options.maximumHeight;
this.api = new DockviewPanelApiImpl(
this,
@ -87,7 +117,7 @@ export class DockviewPanel
// you are actually just resizing the panels parent which is the group
this.group.api.setSize(event);
}),
this.api.onDidRendererChange((event) => {
this.api.onDidRendererChange(() => {
this.group.model.rerender(this);
})
);
@ -129,6 +159,10 @@ export class DockviewPanel
: undefined,
title: this.title,
renderer: this._renderer,
minimumHeight: this._minimumHeight,
maximumHeight: this._maximumHeight,
minimumWidth: this._minimumWidth,
maximumWidth: this._maximumWidth,
};
}
@ -137,13 +171,6 @@ export class DockviewPanel
if (didTitleChange) {
this._title = title;
this.view.update({
params: {
params: this._params,
title: this.title,
},
});
this.api._onDidTitleChange.fire({ title });
}
}
@ -178,10 +205,7 @@ export class DockviewPanel
// update the view with the updated props
this.view.update({
params: {
params: this._params,
title: this.title,
},
params: this._params,
});
}

View File

@ -4,26 +4,29 @@ import {
IContentRenderer,
ITabRenderer,
} from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { IDisposable } from '../lifecycle';
import { IDockviewComponent } from './dockviewComponent';
import { PanelUpdateEvent } from '../panel/types';
import { TabLocation } from './framework';
export interface IDockviewPanelModel extends IDisposable {
readonly contentComponent: string;
readonly tabComponent?: string;
readonly content: IContentRenderer;
readonly tab?: ITabRenderer;
readonly tab: ITabRenderer;
update(event: PanelUpdateEvent): void;
layout(width: number, height: number): void;
init(params: GroupPanelPartInitParameters): void;
updateParentGroup(group: DockviewGroupPanel, isPanelVisible: boolean): void;
createTabRenderer(tabLocation: TabLocation): ITabRenderer;
}
export class DockviewPanelModel implements IDockviewPanelModel {
private readonly _content: IContentRenderer;
private readonly _tab: ITabRenderer;
private _params: GroupPanelPartInitParameters | undefined;
private _updateEvent: PanelUpdateEvent | undefined;
get content(): IContentRenderer {
return this._content;
}
@ -42,16 +45,23 @@ export class DockviewPanelModel implements IDockviewPanelModel {
this._tab = this.createTabComponent(this.id, tabComponent);
}
init(params: GroupPanelPartInitParameters): void {
this.content.init(params);
this.tab.init(params);
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
const cmp = this.createTabComponent(this.id, this.tabComponent);
if (this._params) {
cmp.init({ ...this._params, tabLocation });
}
if (this._updateEvent) {
cmp.update?.(this._updateEvent);
}
return cmp;
}
updateParentGroup(
_group: DockviewGroupPanel,
_isPanelVisible: boolean
): void {
// noop
init(params: GroupPanelPartInitParameters): void {
this._params = params;
this.content.init(params);
this.tab.init({ ...params, tabLocation: 'header' });
}
layout(width: number, height: number): void {
@ -59,6 +69,8 @@ export class DockviewPanelModel implements IDockviewPanelModel {
}
update(event: PanelUpdateEvent): void {
this._updateEvent = event;
this.content.update?.(event);
this.tab.update?.(event);
}

View File

@ -11,9 +11,11 @@ export interface IGroupPanelBaseProps<T extends { [index: string]: any } = any>
containerApi: DockviewApi;
}
export type TabLocation = 'header' | 'headerOverflow';
export type IDockviewPanelHeaderProps<
T extends { [index: string]: any } = any
> = IGroupPanelBaseProps<T>;
> = IGroupPanelBaseProps<T> & { tabLocation: TabLocation };
export type IDockviewPanelProps<T extends { [index: string]: any } = any> =
IGroupPanelBaseProps<T>;

View File

@ -12,8 +12,12 @@ import {
GroupOptions,
} from './dockviewGroupPanelModel';
import { IDockviewPanel } from './dockviewPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { IGroupHeaderProps } from './framework';
import { FloatingGroupOptions } from './dockviewComponent';
import { Contraints } from '../gridview/gridviewPanel';
import { AcceptableEvent, IAcceptableEvent } from '../events';
import { DockviewTheme } from './theme';
export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement;
@ -32,6 +36,10 @@ export interface ViewFactoryData {
}
export interface DockviewOptions {
/**
* Disable the auto-resizing which is controlled through a `ResizeObserver`.
* Call `.layout(width, height)` to manually resize the container.
*/
disableAutoResizing?: boolean;
hideBorders?: boolean;
singleTabMode?: 'fullwidth' | 'default';
@ -45,43 +53,55 @@ export interface DockviewOptions {
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer;
debug?: boolean;
// #start dnd
dndEdges?: false | DroptargetOverlayModel;
/**
* @deprecated use `dndEdges` instead. To be removed in a future version.
* */
rootOverlayModel?: DroptargetOverlayModel;
locked?: boolean;
disableDnd?: boolean;
// #end dnd
locked?: boolean;
className?: string;
/**
* Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`.
*/
noPanelsOverlay?: 'emptyGroup' | 'watermark';
theme?: DockviewTheme;
disableTabsOverflowList?: boolean;
/**
* Select `native` to use built-in scrollbar behaviours and `custom` to use an internal implementation
* that allows for improved scrollbar overlay UX.
*
* This is only applied to the tab header section. Defaults to `custom`.
*/
scrollbars?: 'native' | 'custom';
}
export interface DockviewDndOverlayEvent {
export interface DockviewDndOverlayEvent extends IAcceptableEvent {
nativeEvent: DragEvent;
target: DockviewGroupDropLocation;
position: Position;
group?: DockviewGroupPanel;
getData: () => PanelTransfer | undefined;
//
isAccepted: boolean;
accept(): void;
}
export class DockviewUnhandledDragOverEvent implements DockviewDndOverlayEvent {
private _isAccepted = false;
get isAccepted(): boolean {
return this._isAccepted;
}
export class DockviewUnhandledDragOverEvent
extends AcceptableEvent
implements DockviewDndOverlayEvent
{
constructor(
readonly nativeEvent: DragEvent,
readonly target: DockviewGroupDropLocation,
readonly position: Position,
readonly getData: () => PanelTransfer | undefined,
readonly group?: DockviewGroupPanel
) {}
accept(): void {
this._isAccepted = true;
) {
super();
}
}
export const PROPERTY_KEYS: (keyof DockviewOptions)[] = (() => {
export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => {
/**
* by readong the keys from an empty value object TypeScript will error
* when we add or remove new properties to `DockviewOptions`
@ -98,13 +118,29 @@ export const PROPERTY_KEYS: (keyof DockviewOptions)[] = (() => {
rootOverlayModel: undefined,
locked: undefined,
disableDnd: undefined,
className: undefined,
noPanelsOverlay: undefined,
dndEdges: undefined,
theme: undefined,
disableTabsOverflowList: undefined,
scrollbars: undefined,
};
return Object.keys(properties) as (keyof DockviewOptions)[];
})();
export interface CreateComponentOptions {
/**
* The unqiue identifer of the component
*/
id: string;
/**
* The component name, this should determine what is rendered.
*/
name: string;
}
export interface DockviewFrameworkOptions {
parentElement: HTMLElement;
defaultTabComponent?: string;
createRightHeaderActionComponent?: (
group: DockviewGroupPanel
@ -115,14 +151,10 @@ export interface DockviewFrameworkOptions {
createPrefixHeaderActionComponent?: (
group: DockviewGroupPanel
) => IHeaderActionsRenderer;
createTabComponent?: (options: {
id: string;
name: string;
}) => ITabRenderer | undefined;
createComponent: (options: {
id: string;
name: string;
}) => IContentRenderer;
createTabComponent?: (
options: CreateComponentOptions
) => ITabRenderer | undefined;
createComponent: (options: CreateComponentOptions) => IContentRenderer;
createWatermarkComponent?: () => IWatermarkRenderer;
}
@ -140,11 +172,19 @@ export interface PanelOptions<P extends object = Parameters> {
type RelativePanel = {
direction?: Direction;
referencePanel: string | IDockviewPanel;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
};
type RelativeGroup = {
direction?: Direction;
referenceGroup: string | DockviewGroupPanel;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
};
type AbsolutePosition = {
@ -175,19 +215,12 @@ export function isPanelOptionsWithGroup(
}
type AddPanelFloatingGroupUnion = {
floating:
| {
height?: number;
width?: number;
x?: number;
y?: number;
}
| true;
floating: Partial<FloatingGroupOptions> | true;
position: never;
};
type AddPanelPositionUnion = {
floating: false | never;
floating: false;
position: AddPanelPositionOptions;
};
@ -225,7 +258,10 @@ export type AddPanelOptions<P extends object = Parameters> = {
* Defaults to `false` which forces newly added panels to become active.
*/
inactive?: boolean;
} & Partial<AddPanelOptionsUnion>;
initialWidth?: number;
initialHeight?: number;
} & Partial<AddPanelOptionsUnion> &
Partial<Contraints>;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;

View File

@ -0,0 +1,54 @@
import { CompositeDisposable } from '../lifecycle';
import { DockviewComponent } from './dockviewComponent';
export class StrictEventsSequencing extends CompositeDisposable {
constructor(private readonly accessor: DockviewComponent) {
super();
this.init();
}
private init(): void {
const panels = new Set<string>();
const groups = new Set<string>();
this.addDisposables(
this.accessor.onDidAddPanel((panel) => {
if (panels.has(panel.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidAddPanel] called for panel ${panel.api.id} but panel already exists`
);
} else {
panels.add(panel.api.id);
}
}),
this.accessor.onDidRemovePanel((panel) => {
if (!panels.has(panel.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidRemovePanel] called for panel ${panel.api.id} but panel does not exists`
);
} else {
panels.delete(panel.api.id);
}
}),
this.accessor.onDidAddGroup((group) => {
if (groups.has(group.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidAddGroup] called for group ${group.api.id} but group already exists`
);
} else {
groups.add(group.api.id);
}
}),
this.accessor.onDidRemoveGroup((group) => {
if (!groups.has(group.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidRemoveGroup] called for group ${group.api.id} but group does not exists`
);
} else {
groups.delete(group.api.id);
}
})
);
}
}

View File

@ -0,0 +1,70 @@
export interface DockviewTheme {
/**
* The name of the theme
*/
name: string;
/**
* The class name to apply to the theme containing the CSS variables settings.
*/
className: string;
/**
* The gap between the groups
*/
gap?: number;
/**
* The mouting position of the overlay shown when dragging a panel. `absolute`
* will mount the overlay to root of the dockview component whereas `relative` will mount the overlay to the group container.
*/
dndOverlayMounting?: 'absolute' | 'relative';
/**
* When dragging a panel, the overlay can either encompass the panel contents or the entire group including the tab header space.
*/
dndPanelOverlay?: 'content' | 'group';
}
export const themeDark: DockviewTheme = {
name: 'dark',
className: 'dockview-theme-dark',
};
export const themeLight: DockviewTheme = {
name: 'light',
className: 'dockview-theme-light',
};
export const themeVisualStudio: DockviewTheme = {
name: 'visualStudio',
className: 'dockview-theme-vs',
};
export const themeAbyss: DockviewTheme = {
name: 'abyss',
className: 'dockview-theme-abyss',
};
export const themeDracula: DockviewTheme = {
name: 'dracula',
className: 'dockview-theme-dracula',
};
export const themeReplit: DockviewTheme = {
name: 'replit',
className: 'dockview-theme-replit',
gap: 10,
};
export const themeAbyssSpaced: DockviewTheme = {
name: 'abyssSpaced',
className: 'dockview-theme-abyss-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};
export const themeLightSpaced: DockviewTheme = {
name: 'lightSpaced',
className: 'dockview-theme-light-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};

View File

@ -1,11 +1,10 @@
import { IDockviewComponent } from './dockviewComponent';
import { DockviewPanelApi } from '../api/dockviewPanelApi';
import { PanelInitParameters, IPanel } from '../panel/types';
import { DockviewApi } from '../api/component.api';
import { Event } from '../events';
import { Optional } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { TabLocation } from './framework';
export interface HeaderPartInitParameters {
title: string;
@ -23,46 +22,39 @@ export interface WatermarkRendererInitParameters {
group?: IDockviewGroupPanel;
}
type RendererMethodOptionalList =
| 'dispose'
| 'update'
| 'layout'
| 'toJSON'
| 'focus';
export interface IWatermarkRenderer
extends Optional<
Omit<IPanel, 'id' | 'init'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
extends Optional<Omit<IPanel, 'id' | 'init'>, RendererMethodOptionalList> {
readonly element: HTMLElement;
init: (params: WatermarkRendererInitParameters) => void;
updateParentGroup(group: DockviewGroupPanel, visible: boolean): void;
}
export interface TabPartInitParameters extends GroupPanelPartInitParameters {
tabLocation: TabLocation;
}
export interface ITabRenderer
extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> {
readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void;
init(parameters: TabPartInitParameters): void;
}
export interface IContentRenderer
extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> {
readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void;
}
// watermark component
export interface WatermarkPartInitParameters {
accessor: IDockviewComponent;
}
// constructors
export interface WatermarkConstructor {
new (): IWatermarkRenderer;
}
export interface IGroupPanelInitParameters
extends PanelInitParameters,
HeaderPartInitParameters {
@ -76,4 +68,8 @@ export interface GroupviewPanelState {
title?: string;
renderer?: DockviewPanelRenderer;
params?: { [key: string]: any };
minimumWidth?: number;
minimumHeight?: number;
maximumWidth?: number;
maximumHeight?: number;
}

View File

@ -0,0 +1,90 @@
// import { SerializedGridObject } from '../gridview/gridview';
// import { Orientation } from '../splitview/splitview';
// import { SerializedDockview } from './dockviewComponent';
// import { GroupPanelViewState } from './dockviewGroupPanelModel';
// function typeValidate3(data: GroupPanelViewState, path: string): void {
// if (typeof data.id !== 'string') {
// throw new Error(`${path}.id must be a string`);
// }
// if (
// typeof data.activeView !== 'string' ||
// typeof data.activeView !== 'undefined'
// ) {
// throw new Error(`${path}.activeView must be a string of undefined`);
// }
// }
// function typeValidate2(
// data: SerializedGridObject<GroupPanelViewState>,
// path: string
// ): void {
// if (typeof data.size !== 'number' && typeof data.size !== 'undefined') {
// throw new Error(`${path}.size must be a number or undefined`);
// }
// if (
// typeof data.visible !== 'boolean' &&
// typeof data.visible !== 'undefined'
// ) {
// throw new Error(`${path}.visible must be a boolean or undefined`);
// }
// if (data.type === 'leaf') {
// if (
// typeof data.data !== 'object' ||
// data.data === null ||
// Array.isArray(data.data)
// ) {
// throw new Error('object must be a non-null object');
// }
// typeValidate3(data.data, `${path}.data`);
// } else if (data.type === 'branch') {
// if (!Array.isArray(data.data)) {
// throw new Error(`${path}.data must be an array`);
// }
// } else {
// throw new Error(`${path}.type must be onew of {'branch', 'leaf'}`);
// }
// }
// function typeValidate(data: SerializedDockview): void {
// if (typeof data !== 'object' || data === null) {
// throw new Error('object must be a non-null object');
// }
// const { grid, panels, activeGroup, floatingGroups } = data;
// if (typeof grid !== 'object' || grid === null) {
// throw new Error("'.grid' must be a non-null object");
// }
// if (typeof grid.height !== 'number') {
// throw new Error("'.grid.height' must be a number");
// }
// if (typeof grid.width !== 'number') {
// throw new Error("'.grid.width' must be a number");
// }
// if (typeof grid.root !== 'object' || grid.root === null) {
// throw new Error("'.grid.root' must be a non-null object");
// }
// if (grid.root.type !== 'branch') {
// throw new Error(".grid.root.type must be of type 'branch'");
// }
// if (
// grid.orientation !== Orientation.HORIZONTAL &&
// grid.orientation !== Orientation.VERTICAL
// ) {
// throw new Error(
// `'.grid.width' must be one of {${Orientation.HORIZONTAL}, ${Orientation.VERTICAL}}`
// );
// }
// typeValidate2(grid.root, '.grid.root');
// }

View File

@ -2,10 +2,39 @@ import {
Event as DockviewEvent,
Emitter,
addDisposableListener,
addDisposableWindowListener,
} from './events';
import { IDisposable, CompositeDisposable } from './lifecycle';
export interface OverflowEvent {
hasScrollX: boolean;
hasScrollY: boolean;
}
export class OverflowObserver extends CompositeDisposable {
private readonly _onDidChange = new Emitter<OverflowEvent>();
readonly onDidChange = this._onDidChange.event;
private _value: OverflowEvent | null = null;
constructor(el: HTMLElement) {
super();
this.addDisposables(
this._onDidChange,
watchElementResize(el, (entry) => {
const hasScrollX =
entry.target.scrollWidth > entry.target.clientWidth;
const hasScrollY =
entry.target.scrollHeight > entry.target.clientHeight;
this._value = { hasScrollX, hasScrollY };
this._onDidChange.fire(this._value);
})
);
}
}
export function watchElementResize(
element: HTMLElement,
cb: (entry: ResizeObserverEntry) => void
@ -82,8 +111,11 @@ export function isAncestor(
return false;
}
export function getElementsByTagName(tag: string): HTMLElement[] {
return Array.prototype.slice.call(document.getElementsByTagName(tag), 0);
export function getElementsByTagName(
tag: string,
document: ParentNode
): HTMLElement[] {
return Array.prototype.slice.call(document.querySelectorAll(tag), 0);
}
export interface IFocusTracker extends IDisposable {
@ -92,7 +124,7 @@ export interface IFocusTracker extends IDisposable {
refreshState?(): void;
}
export function trackFocus(element: HTMLElement | Window): IFocusTracker {
export function trackFocus(element: HTMLElement): IFocusTracker {
return new FocusTracker(element);
}
@ -106,9 +138,9 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _onDidBlur = new Emitter<void>();
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
private _refreshStateHandler: () => void;
private readonly _refreshStateHandler: () => void;
constructor(element: HTMLElement | Window) {
constructor(element: HTMLElement) {
super();
this.addDisposables(this._onDidFocus, this._onDidBlur);
@ -151,21 +183,12 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
}
};
if (element instanceof HTMLElement) {
this.addDisposables(
addDisposableListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableListener(element, 'blur', onBlur, true)
);
} else {
this.addDisposables(
addDisposableWindowListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableWindowListener(element, 'blur', onBlur, true)
);
}
this.addDisposables(
addDisposableListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableListener(element, 'blur', onBlur, true)
);
}
refreshState(): void {
@ -253,3 +276,225 @@ export function isInDocument(element: Element): boolean {
return false;
}
export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id);
}
/**
* Should be more efficient than element.querySelectorAll("*") since there
* is no need to store every element in-memory using this approach
*/
function allTagsNamesInclusiveOfShadowDoms(tagNames: string[]) {
const iframes: HTMLElement[] = [];
function findIframesInNode(node: Element) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (tagNames.includes(node.tagName)) {
iframes.push(node as HTMLElement);
}
if (node.shadowRoot) {
findIframesInNode(<any>node.shadowRoot);
}
for (const child of node.children) {
findIframesInNode(child);
}
}
}
findIframesInNode(document.documentElement);
return iframes;
}
export function disableIframePointEvents(rootNode: ParentNode = document) {
const iframes = allTagsNamesInclusiveOfShadowDoms(['IFRAME', 'WEBVIEW']);
const original = new WeakMap<HTMLElement, string>(); // don't hold onto HTMLElement references longer than required
for (const iframe of iframes) {
original.set(iframe, iframe.style.pointerEvents);
iframe.style.pointerEvents = 'none';
}
return {
release: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = original.get(iframe) ?? 'auto';
}
iframes.splice(0, iframes.length); // don't hold onto HTMLElement references longer than required
},
};
}
export function getDockviewTheme(element: HTMLElement): string | undefined {
function toClassList(element: HTMLElement) {
const list: string[] = [];
for (let i = 0; i < element.classList.length; i++) {
list.push(element.classList.item(i)!);
}
return list;
}
let theme: string | undefined = undefined;
let parent: HTMLElement | null = element;
while (parent !== null) {
theme = toClassList(parent).find((cls) =>
cls.startsWith('dockview-theme-')
);
if (typeof theme === 'string') {
break;
}
parent = parent.parentElement;
}
return theme;
}
export class Classnames {
private _classNames: string[] = [];
constructor(private readonly element: HTMLElement) {}
setClassNames(classNames: string) {
for (const className of this._classNames) {
toggleClass(this.element, className, false);
}
this._classNames = classNames
.split(' ')
.filter((v) => v.trim().length > 0);
for (const className of this._classNames) {
toggleClass(this.element, className, true);
}
}
}
const DEBOUCE_DELAY = 100;
export function isChildEntirelyVisibleWithinParent(
child: HTMLElement,
parent: HTMLElement
): boolean {
//
const childPosition = getDomNodePagePosition(child);
const parentPosition = getDomNodePagePosition(parent);
if (childPosition.left < parentPosition.left) {
return false;
}
if (
childPosition.left + childPosition.width >
parentPosition.left + parentPosition.width
) {
return false;
}
return true;
}
export function onDidWindowMoveEnd(window: Window): Emitter<void> {
const emitter = new Emitter<void>();
let previousScreenX = window.screenX;
let previousScreenY = window.screenY;
let timeout: any;
const checkMovement = () => {
if (window.closed) {
return;
}
const currentScreenX = window.screenX;
const currentScreenY = window.screenY;
if (
currentScreenX !== previousScreenX ||
currentScreenY !== previousScreenY
) {
clearTimeout(timeout);
timeout = setTimeout(() => {
emitter.fire();
}, DEBOUCE_DELAY);
previousScreenX = currentScreenX;
previousScreenY = currentScreenY;
}
requestAnimationFrame(checkMovement);
};
checkMovement();
return emitter;
}
export function onDidWindowResizeEnd(element: Window, cb: () => void) {
let resizeTimeout: any;
const disposable = new CompositeDisposable(
addDisposableListener(element, 'resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
cb();
}, DEBOUCE_DELAY);
})
);
return disposable;
}
export function shiftAbsoluteElementIntoView(
element: HTMLElement,
root: HTMLElement,
options: { buffer: number } = { buffer: 10 }
) {
const buffer = options.buffer;
const rect = element.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
let translateX = 0;
let translateY = 0;
const left = rect.left - rootRect.left;
const top = rect.top - rootRect.top;
const bottom = rect.bottom - rootRect.bottom;
const right = rect.right - rootRect.right;
// Check horizontal overflow
if (left < buffer) {
translateX = buffer - left;
} else if (right > buffer) {
translateX = -buffer - right;
}
// Check vertical overflow
if (top < buffer) {
translateY = buffer - top;
} else if (bottom > buffer) {
translateY = -bottom - buffer;
}
// Apply the translation if needed
if (translateX !== 0 || translateY !== 0) {
element.style.transform = `translate(${translateX}px, ${translateY}px)`;
}
}
export function findRelativeZIndexParent(el: HTMLElement): HTMLElement | null {
let tmp: HTMLElement | null = el;
while (tmp && (tmp.style.zIndex === 'auto' || tmp.style.zIndex === '')) {
tmp = tmp.parentElement;
}
return tmp;
}

View File

@ -41,6 +41,23 @@ export class DockviewEvent implements IDockviewEvent {
}
}
export interface IAcceptableEvent {
readonly isAccepted: boolean;
accept(): void;
}
export class AcceptableEvent implements IAcceptableEvent {
private _isAccepted = false;
get isAccepted(): boolean {
return this._isAccepted;
}
accept(): void {
this._isAccepted = true;
}
}
class LeakageMonitor {
readonly events = new Map<Event<any>, Stacktrace>();
@ -69,7 +86,7 @@ class Stacktrace {
private constructor(readonly value: string) {}
print(): void {
console.warn(this.value);
console.warn('dockview: stacktrace', this.value);
}
}
@ -124,7 +141,7 @@ export class Emitter<T> implements IDisposable {
this._listeners.splice(index, 1);
} else if (Emitter.ENABLE_TRACKING) {
// console.warn(
// `Listener already disposed`,
// `dockview: listener already disposed`,
// Stacktrace.create().print()
// );
}
@ -158,7 +175,10 @@ export class Emitter<T> implements IDisposable {
queueMicrotask(() => {
// don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block
for (const listener of this._listeners) {
console.warn(listener.stacktrace?.print());
console.warn(
'dockview: stacktrace',
listener.stacktrace?.print()
);
}
});
}
@ -173,32 +193,38 @@ export class Emitter<T> implements IDisposable {
}
}
export function addDisposableWindowListener<K extends keyof WindowEventMap>(
export function addDisposableListener<K extends keyof WindowEventMap>(
element: Window,
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
},
};
}
): IDisposable;
export function addDisposableListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable;
export function addDisposableListener<
K extends keyof HTMLElementEventMap | keyof WindowEventMap
>(
element: HTMLElement | Window,
type: K,
listener: (
this: K extends keyof HTMLElementEventMap ? HTMLElement : Window,
ev: K extends keyof HTMLElementEventMap
? HTMLElementEventMap[K]
: K extends keyof WindowEventMap
? WindowEventMap[K]
: never
) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
element.addEventListener(type, <any>listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
element.removeEventListener(type, <any>listener, options);
},
};
}

View File

@ -1,15 +1,19 @@
import { Emitter, Event, AsapEvent } from '../events';
import { getGridLocation, Gridview, IGridView } from './gridview';
import { Position } from '../dnd/droptarget';
import { Disposable, IValueDisposable } from '../lifecycle';
import { Disposable, IDisposable, IValueDisposable } from '../lifecycle';
import { sequentialNumberGenerator } from '../math';
import { ISplitviewStyles, Orientation, Sizing } from '../splitview/splitview';
import { IPanel } from '../panel/types';
import { MovementOptions2 } from '../dockview/options';
import { Resizable } from '../resizable';
import { Classnames } from '../dom';
const nextLayoutId = sequentialNumberGenerator();
/**
* A direction in which a panel can be moved or placed relative to another panel.
*/
export type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction): Position {
@ -28,13 +32,19 @@ export function toTarget(direction: Direction): Position {
}
}
export interface MaximizedChanged<T extends IGridPanelView> {
panel: T;
isMaximized: boolean;
}
export interface BaseGridOptions {
readonly proportionalLayout: boolean;
readonly orientation: Orientation;
readonly styles?: ISplitviewStyles;
readonly parentElement: HTMLElement;
readonly disableAutoResizing?: boolean;
readonly locked?: boolean;
readonly margin?: number;
readonly className?: string;
}
export interface IGridPanelView extends IGridView, IPanel {
@ -42,7 +52,7 @@ export interface IGridPanelView extends IGridView, IPanel {
readonly isActive: boolean;
}
export interface IBaseGrid<T extends IGridPanelView> {
export interface IBaseGrid<T extends IGridPanelView> extends IDisposable {
readonly element: HTMLElement;
readonly id: string;
readonly width: number;
@ -54,6 +64,8 @@ export interface IBaseGrid<T extends IGridPanelView> {
readonly activeGroup: T | undefined;
readonly size: number;
readonly groups: T[];
readonly onDidMaximizedChange: Event<MaximizedChanged<T>>;
readonly onDidLayoutChange: Event<void>;
getPanel(id: string): T | undefined;
toJSON(): object;
fromJSON(data: any): void;
@ -65,8 +77,6 @@ export interface IBaseGrid<T extends IGridPanelView> {
isMaximizedGroup(panel: T): boolean;
exitMaximizedGroup(): void;
hasMaximizedGroup(): boolean;
readonly onDidMaximizedGroupChange: Event<void>;
readonly onDidLayoutChange: Event<void>;
}
export abstract class BaseGrid<T extends IGridPanelView>
@ -85,6 +95,10 @@ export abstract class BaseGrid<T extends IGridPanelView>
private readonly _onDidAdd = new Emitter<T>();
readonly onDidAdd: Event<T> = this._onDidAdd.event;
private readonly _onDidMaximizedChange = new Emitter<MaximizedChanged<T>>();
readonly onDidMaximizedChange: Event<MaximizedChanged<T>> =
this._onDidMaximizedChange.event;
private readonly _onDidActiveChange = new Emitter<T | undefined>();
readonly onDidActiveChange: Event<T | undefined> =
this._onDidActiveChange.event;
@ -93,6 +107,12 @@ export abstract class BaseGrid<T extends IGridPanelView>
readonly onDidLayoutChange: Event<void> =
this._bufferOnDidLayoutChange.onEvent;
private readonly _onDidViewVisibilityChangeMicroTaskQueue = new AsapEvent();
readonly onDidViewVisibilityChangeMicroTaskQueue =
this._onDidViewVisibilityChangeMicroTaskQueue.onEvent;
private readonly _classNames: Classnames;
get id(): string {
return this._id;
}
@ -138,17 +158,23 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.gridview.locked = value;
}
constructor(options: BaseGridOptions) {
constructor(container: HTMLElement, options: BaseGridOptions) {
super(document.createElement('div'), options.disableAutoResizing);
this.element.style.height = '100%';
this.element.style.width = '100%';
options.parentElement.appendChild(this.element);
this._classNames = new Classnames(this.element);
this._classNames.setClassNames(options.className ?? '');
// the container is owned by the third-party, do not modify/delete it
container.appendChild(this.element);
this.gridview = new Gridview(
!!options.proportionalLayout,
options.styles,
options.orientation
options.orientation,
options.locked,
options.margin
);
this.gridview.locked = !!options.locked;
@ -158,6 +184,18 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.layout(0, 0, true); // set some elements height/widths
this.addDisposables(
this.gridview.onDidMaximizedNodeChange((event) => {
this._onDidMaximizedChange.fire({
panel: event.view as T,
isMaximized: event.isMaximized,
});
}),
this.gridview.onDidViewVisibilityChange(() =>
this._onDidViewVisibilityChangeMicroTaskQueue.fire()
),
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.layout(this.width, this.height, true);
}),
Disposable.from(() => {
this.element.parentElement?.removeChild(this.element);
}),
@ -171,6 +209,8 @@ export abstract class BaseGrid<T extends IGridPanelView>
)(() => {
this._bufferOnDidLayoutChange.fire();
}),
this._onDidMaximizedChange,
this._onDidViewVisibilityChangeMicroTaskQueue,
this._bufferOnDidLayoutChange
);
}
@ -190,6 +230,30 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.isViewVisible(getGridLocation(panel.element));
}
updateOptions(options: Partial<BaseGridOptions>) {
if (typeof options.proportionalLayout === 'boolean') {
// this.gridview.proportionalLayout = options.proportionalLayout; // not supported
}
if (options.orientation) {
this.gridview.orientation = options.orientation;
}
if ('styles' in options) {
// this.gridview.styles = options.styles; // not supported
}
if ('disableResizing' in options) {
this.disableResizing = options.disableAutoResizing ?? false;
}
if ('locked' in options) {
this.locked = options.locked ?? false;
}
if ('margin' in options) {
this.gridview.margin = options.margin ?? 0;
}
if ('className' in options) {
this._classNames.setClassNames(options.className ?? '');
}
}
maximizeGroup(panel: T): void {
this.gridview.maximizeView(panel);
this.doSetGroupActive(panel);
@ -207,10 +271,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.hasMaximizedView();
}
get onDidMaximizedGroupChange(): Event<void> {
return this.gridview.onDidMaximizedNodeChange;
}
protected doAddGroup(
group: T,
location: number[] = [0],
@ -310,7 +370,7 @@ export abstract class BaseGrid<T extends IGridPanelView>
public layout(width: number, height: number, forceResize?: boolean): void {
const different =
forceResize ?? (width !== this.width || height !== this.height);
forceResize || width !== this.width || height !== this.height;
if (!different) {
return;

View File

@ -32,7 +32,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
{
private _height = 0;
private _width = 0;
private _element: HTMLElement;
private readonly _element: HTMLElement;
protected part?: IFrameworkPart;
protected _params?: PanelInitParameters;

View File

@ -19,7 +19,7 @@ import { CompositeDisposable, IDisposable, Disposable } from '../lifecycle';
export class BranchNode extends CompositeDisposable implements IView {
readonly element: HTMLElement;
private splitview: Splitview;
private readonly splitview: Splitview;
private _orthogonalSize: number;
private _size: number;
private _childrenDisposable: IDisposable = Disposable.NONE;
@ -33,9 +33,12 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<boolean>();
readonly onDidVisibilityChange: Event<boolean> =
this._onDidVisibilityChange.event;
private readonly _onDidVisibilityChange = new Emitter<{
visible: boolean;
}>();
readonly onDidVisibilityChange: Event<{
visible: boolean;
}> = this._onDidVisibilityChange.event;
get width(): number {
return this.orientation === Orientation.HORIZONTAL
@ -139,6 +142,20 @@ export class BranchNode extends CompositeDisposable implements IView {
this.splitview.disabled = value;
}
get margin(): number {
return this.splitview.margin;
}
set margin(value: number) {
this.splitview.margin = value;
this.children.forEach((child) => {
if (child instanceof BranchNode) {
child.margin = value;
}
});
}
constructor(
readonly orientation: Orientation,
readonly proportionalLayout: boolean,
@ -146,6 +163,7 @@ export class BranchNode extends CompositeDisposable implements IView {
size: number,
orthogonalSize: number,
disabled: boolean,
margin: number | undefined,
childDescriptors?: INodeDescriptor[]
) {
super();
@ -153,13 +171,14 @@ export class BranchNode extends CompositeDisposable implements IView {
this._size = size;
this.element = document.createElement('div');
this.element.className = 'branch-node';
this.element.className = 'dv-branch-node';
if (!childDescriptors) {
this.splitview = new Splitview(this.element, {
orientation: this.orientation,
proportionalLayout,
styles,
margin,
});
this.splitview.layout(this.size, this.orthogonalSize);
} else {
@ -184,6 +203,7 @@ export class BranchNode extends CompositeDisposable implements IView {
descriptor,
proportionalLayout,
styles,
margin,
});
}
@ -200,10 +220,8 @@ export class BranchNode extends CompositeDisposable implements IView {
this.setupChildrenEvents();
}
setVisible(visible: boolean): void {
for (const child of this.children) {
child.setVisible(visible);
}
setVisible(_visible: boolean): void {
// noop
}
isChildVisible(index: number): boolean {
@ -224,7 +242,9 @@ export class BranchNode extends CompositeDisposable implements IView {
}
const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible);
// }
const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview
@ -233,7 +253,7 @@ export class BranchNode extends CompositeDisposable implements IView {
(visible && wereAllChildrenHidden) ||
(!visible && areAllChildrenHidden)
) {
this._onDidVisibilityChange.fire(visible);
this._onDidVisibilityChange.fire({ visible });
}
}
@ -335,7 +355,7 @@ export class BranchNode extends CompositeDisposable implements IView {
}),
...this.children.map((c, i) => {
if (c instanceof BranchNode) {
return c.onDidVisibilityChange((visible) => {
return c.onDidVisibilityChange(({ visible }) => {
this.setChildVisible(i, visible);
});
}

View File

@ -1,5 +1,5 @@
.grid-view,
.branch-node {
.dv-grid-view,
.dv-branch-node {
height: 100%;
width: 100%;
}

View File

@ -42,7 +42,8 @@ function flipNode<T extends Node>(
node.styles,
size,
orthogonalSize,
node.disabled
node.disabled,
node.margin
);
let totalSize = 0;
@ -112,7 +113,7 @@ export function getGridLocation(element: HTMLElement): number[] {
throw new Error('Invalid grid element');
}
if (/\bgrid-view\b/.test(parentElement.className)) {
if (/\bdv-grid-view\b/.test(parentElement.className)) {
return [];
}
@ -171,6 +172,7 @@ export interface IGridView {
readonly maximumWidth: number;
readonly minimumHeight: number;
readonly maximumHeight: number;
readonly isVisible: boolean;
priority?: LayoutPriority;
layout(width: number, height: number): void;
toJSON(): object;
@ -263,11 +265,21 @@ export interface IViewDeserializer {
fromJSON: (data: ISerializedLeafNode) => IGridView;
}
export interface SerializedNodeDescriptor {
location: number[];
}
export interface SerializedGridview<T> {
root: SerializedGridObject<T>;
width: number;
height: number;
orientation: Orientation;
maximizedNode?: SerializedNodeDescriptor;
}
export interface MaximizedViewChanged {
view: IGridView;
isMaximized: boolean;
}
export class Gridview implements IDisposable {
@ -275,6 +287,7 @@ export class Gridview implements IDisposable {
private _root: BranchNode | undefined;
private _locked = false;
private _margin = 0;
private _maximizedNode:
| { leaf: LeafNode; hiddenOnMaximize: LeafNode[] }
| undefined = undefined;
@ -287,7 +300,11 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly _onDidMaximizedNodeChange = new Emitter<void>();
private readonly _onDidViewVisibilityChange = new Emitter<void>();
readonly onDidViewVisibilityChange = this._onDidViewVisibilityChange.event;
private readonly _onDidMaximizedNodeChange =
new Emitter<MaximizedViewChanged>();
readonly onDidMaximizedNodeChange = this._onDidMaximizedNodeChange.event;
public get length(): number {
@ -356,6 +373,15 @@ export class Gridview implements IDisposable {
}
}
get margin(): number {
return this._margin;
}
set margin(value: number) {
this._margin = value;
this.root.margin = value;
}
maximizedView(): IGridView | undefined {
return this._maximizedNode?.leaf.view;
}
@ -380,6 +406,8 @@ export class Gridview implements IDisposable {
this.exitMaximizedView();
}
serializeBranchNode(this.getView(), this.orientation);
const hiddenOnMaximize: LeafNode[] = [];
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
@ -401,7 +429,10 @@ export class Gridview implements IDisposable {
hideAllViewsBut(this.root, node);
this._maximizedNode = { leaf: node, hiddenOnMaximize };
this._onDidMaximizedNodeChange.fire();
this._onDidMaximizedNodeChange.fire({
view: node.view,
isMaximized: true,
});
}
exitMaximizedView(): void {
@ -426,33 +457,68 @@ export class Gridview implements IDisposable {
showViewsInReverseOrder(this.root);
const tmp = this._maximizedNode.leaf;
this._maximizedNode = undefined;
this._onDidMaximizedNodeChange.fire();
this._onDidMaximizedNodeChange.fire({
view: tmp.view,
isMaximized: false,
});
}
public serialize(): SerializedGridview<any> {
const maximizedView = this.maximizedView();
let maxmizedViewLocation: number[] | undefined;
if (maximizedView) {
/**
* The minimum information we can get away with in order to serialize a maxmized view is it's location within the grid
* which is represented as a branch of indices
*/
maxmizedViewLocation = getGridLocation(maximizedView.element);
}
if (this.hasMaximizedView()) {
/**
* do not persist maximized view state
* firstly exit any maximized views to ensure the correct dimensions are persisted
* the saved layout cannot be in its maxmized state otherwise all of the underlying
* view dimensions will be wrong
*
* To counteract this we temporaily remove the maximized view to compute the serialized output
* of the grid before adding back the maxmized view as to not alter the layout from the users
* perspective when `.toJSON()` is called
*/
this.exitMaximizedView();
}
const root = serializeBranchNode(this.getView(), this.orientation);
return {
const resullt: SerializedGridview<any> = {
root,
width: this.width,
height: this.height,
orientation: this.orientation,
};
if (maxmizedViewLocation) {
resullt.maximizedNode = {
location: maxmizedViewLocation,
};
}
if (maximizedView) {
// replace any maximzied view that was removed for serialization purposes
this.maximizeView(maximizedView);
}
return resullt;
}
public dispose(): void {
this.disposable.dispose();
this._onDidChange.dispose();
this._onDidMaximizedNodeChange.dispose();
this._onDidViewVisibilityChange.dispose();
this.root.dispose();
this._maximizedNode = undefined;
this.element.remove();
@ -466,7 +532,8 @@ export class Gridview implements IDisposable {
this.styles,
this.root.size,
this.root.orthogonalSize,
this._locked
this.locked,
this.margin
);
}
@ -484,6 +551,24 @@ export class Gridview implements IDisposable {
deserializer,
height
);
/**
* The deserialied layout must be positioned through this.layout(...)
* before any maximizedNode can be positioned
*/
this.layout(json.width, json.height);
if (json.maximizedNode) {
const location = json.maximizedNode.location;
const [_, node] = this.getNode(location);
if (!(node instanceof LeafNode)) {
return;
}
this.maximizeView(node.view);
}
}
private _deserialize(
@ -527,16 +612,17 @@ export class Gridview implements IDisposable {
this.styles,
node.size, // <- orthogonal size - flips at each depth
orthogonalSize, // <- size - flips at each depth,
this._locked,
this.locked,
this.margin,
children
);
} else {
result = new LeafNode(
deserializer.fromJSON(node),
orientation,
orthogonalSize,
node.size
);
const view = deserializer.fromJSON(node);
if (typeof node.visible === 'boolean') {
view.setVisible?.(node.visible);
}
result = new LeafNode(view, orientation, orthogonalSize, node.size);
}
return result;
@ -580,7 +666,8 @@ export class Gridview implements IDisposable {
this.styles,
this.root.orthogonalSize,
this.root.size,
this._locked
this.locked,
this.margin
);
if (oldRoot.children.length === 0) {
@ -686,17 +773,24 @@ export class Gridview implements IDisposable {
constructor(
readonly proportionalLayout: boolean,
readonly styles: ISplitviewStyles | undefined,
orientation: Orientation
orientation: Orientation,
locked?: boolean,
margin?: number
) {
this.element = document.createElement('div');
this.element.className = 'grid-view';
this.element.className = 'dv-grid-view';
this._locked = locked ?? false;
this._margin = margin ?? 0;
this.root = new BranchNode(
orientation,
proportionalLayout,
styles,
0,
0,
this._locked
this.locked,
this.margin
);
}
@ -723,6 +817,8 @@ export class Gridview implements IDisposable {
throw new Error('Invalid from location');
}
this._onDidViewVisibilityChange.fire();
parent.setChildVisible(index, visible);
}
@ -781,7 +877,8 @@ export class Gridview implements IDisposable {
this.styles,
parent.size,
parent.orthogonalSize,
this._locked
this.locked,
this.margin
);
grandParent.addChild(newParent, parent.size, parentIndex);

View File

@ -23,7 +23,6 @@ import {
} from './gridviewPanel';
import { BaseComponentOptions, Parameters } from '../panel/types';
import { Orientation, Sizing } from '../splitview/splitview';
import { createComponent } from '../panel/componentFactory';
import { Emitter, Event } from '../events';
import { Position } from '../dnd/droptarget';
@ -49,15 +48,10 @@ export interface IGridPanelComponentView extends IGridPanelView {
init: (params: GridviewInitParameters) => void;
}
export type GridviewComponentUpdateOptions = Pick<
GridviewComponentOptions,
'orientation' | 'components' | 'frameworkComponents'
>;
export interface IGridviewComponent extends IBaseGrid<GridviewPanel> {
readonly orientation: Orientation;
readonly onDidLayoutFromJSON: Event<void>;
updateOptions(options: Partial<GridviewComponentUpdateOptions>): void;
updateOptions(options: Partial<GridviewComponentOptions>): void;
addPanel<T extends object = Parameters>(
options: AddComponentOptions<T>
): IGridviewPanel;
@ -119,13 +113,15 @@ export class GridviewComponent
this._deserializer = value;
}
constructor(options: GridviewComponentOptions) {
super({
parentElement: options.parentElement,
proportionalLayout: options.proportionalLayout,
constructor(container: HTMLElement, options: GridviewComponentOptions) {
super(container, {
proportionalLayout: options.proportionalLayout ?? true,
orientation: options.orientation,
styles: options.styles,
styles: options.hideBorders
? { separatorBorder: 'transparent' }
: undefined,
disableAutoResizing: options.disableAutoResizing,
className: options.className,
});
this._options = options;
@ -144,16 +140,11 @@ export class GridviewComponent
this._onDidActiveGroupChange.fire(event);
})
);
if (!this.options.components) {
this.options.components = {};
}
if (!this.options.frameworkComponents) {
this.options.frameworkComponents = {};
}
}
updateOptions(options: Partial<GridviewComponentUpdateOptions>): void {
override updateOptions(options: Partial<GridviewComponentOptions>): void {
super.updateOptions(options);
const hasOrientationChanged =
typeof options.orientation === 'string' &&
this.gridview.orientation !== options.orientation;
@ -219,19 +210,11 @@ export class GridviewComponent
this.gridview.deserialize(grid, {
fromJSON: (node) => {
const { data } = node;
const view = createComponent(
data.id,
data.component,
this.options.components ?? {},
this.options.frameworkComponents ?? {},
this.options.frameworkComponentFactory
? {
createComponent:
this.options.frameworkComponentFactory
.createComponent,
}
: undefined
);
const view = this.options.createComponent({
id: data.id,
name: data.component,
});
queue.push(() =>
view.init({
@ -366,19 +349,10 @@ export class GridviewComponent
}
}
const view = createComponent(
options.id,
options.component,
this.options.components ?? {},
this.options.frameworkComponents ?? {},
this.options.frameworkComponentFactory
? {
createComponent:
this.options.frameworkComponentFactory
.createComponent,
}
: undefined
);
const view = this.options.createComponent({
id: options.id,
name: options.component,
});
view.init({
params: options.params ?? {},

View File

@ -1,8 +1,5 @@
import { PanelInitParameters } from '../panel/types';
import {
GridviewComponent,
IGridPanelComponentView,
} from './gridviewComponent';
import { IGridPanelComponentView } from './gridviewComponent';
import { FunctionOrValue } from '../types';
import {
BasePanelView,
@ -18,6 +15,13 @@ import { Emitter, Event } from '../events';
import { IViewSize } from './gridview';
import { BaseGrid, IGridPanelView } from './baseComponentGridview';
export interface Contraints {
minimumWidth?: number;
maximumWidth?: number;
minimumHeight?: number;
maximumHeight?: number;
}
export interface GridviewInitParameters extends PanelInitParameters {
minimumWidth?: number;
maximumWidth?: number;
@ -70,6 +74,38 @@ export abstract class GridviewPanel<
}
get minimumWidth(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__minimumWidth();
}
get minimumHeight(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__minimumHeight();
}
get maximumHeight(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__maximumHeight();
}
get maximumWidth(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__maximumWidth();
}
protected __minimumWidth(): number {
const width =
typeof this._minimumWidth === 'function'
? this._minimumWidth()
@ -83,35 +119,7 @@ export abstract class GridviewPanel<
return width;
}
get minimumHeight(): number {
const height =
typeof this._minimumHeight === 'function'
? this._minimumHeight()
: this._minimumHeight;
if (height !== this._evaluatedMinimumHeight) {
this._evaluatedMinimumHeight = height;
this.updateConstraints();
}
return height;
}
get maximumHeight(): number {
const height =
typeof this._maximumHeight === 'function'
? this._maximumHeight()
: this._maximumHeight;
if (height !== this._evaluatedMaximumHeight) {
this._evaluatedMaximumHeight = height;
this.updateConstraints();
}
return height;
}
get maximumWidth(): number {
protected __maximumWidth(): number {
const width =
typeof this._maximumWidth === 'function'
? this._maximumWidth()
@ -125,10 +133,42 @@ export abstract class GridviewPanel<
return width;
}
protected __minimumHeight(): number {
const height =
typeof this._minimumHeight === 'function'
? this._minimumHeight()
: this._minimumHeight;
if (height !== this._evaluatedMinimumHeight) {
this._evaluatedMinimumHeight = height;
this.updateConstraints();
}
return height;
}
protected __maximumHeight(): number {
const height =
typeof this._maximumHeight === 'function'
? this._maximumHeight()
: this._maximumHeight;
if (height !== this._evaluatedMaximumHeight) {
this._evaluatedMaximumHeight = height;
this.updateConstraints();
}
return height;
}
get isActive(): boolean {
return this.api.isActive;
}
get isVisible(): boolean {
return this.api.isVisible;
}
constructor(
id: string,
component: string,

View File

@ -17,7 +17,7 @@ export class LeafNode implements IView {
this._onDidChange.event;
private _size: number;
private _orthogonalSize: number;
private _disposable: IDisposable;
private readonly _disposable: IDisposable;
private get minimumWidth(): number {
return this.view.minimumWidth;

View File

@ -1,21 +1,34 @@
import { GridviewPanel } from './gridviewPanel';
import { ISplitviewStyles, Orientation } from '../splitview/splitview';
import {
ComponentConstructor,
FrameworkFactory,
} from '../panel/componentFactory';
import { Orientation } from '../splitview/splitview';
import { CreateComponentOptions } from '../dockview/options';
export interface GridviewComponentOptions {
export interface GridviewOptions {
disableAutoResizing?: boolean;
proportionalLayout: boolean;
proportionalLayout?: boolean;
orientation: Orientation;
components?: {
[componentName: string]: ComponentConstructor<GridviewPanel>;
};
frameworkComponents?: {
[componentName: string]: any;
};
frameworkComponentFactory?: FrameworkFactory<GridviewPanel>;
styles?: ISplitviewStyles;
parentElement: HTMLElement;
className?: string;
hideBorders?: boolean;
}
export interface GridviewFrameworkOptions {
createComponent: (options: CreateComponentOptions) => GridviewPanel;
}
export type GridviewComponentOptions = GridviewOptions &
GridviewFrameworkOptions;
export const PROPERTY_KEYS_GRIDVIEW: (keyof GridviewOptions)[] = (() => {
/**
* by readong the keys from an empty value object TypeScript will error
* when we add or remove new properties to `DockviewOptions`
*/
const properties: Record<keyof GridviewOptions, undefined> = {
disableAutoResizing: undefined,
proportionalLayout: undefined,
orientation: undefined,
hideBorders: undefined,
className: undefined,
};
return Object.keys(properties) as (keyof GridviewOptions)[];
})();

View File

@ -1,31 +1,48 @@
export * from './dnd/dataTransfer';
export {
getPaneData,
getPanelData,
PaneTransfer,
PanelTransfer,
} from './dnd/dataTransfer';
/**
* Events, Emitters and Disposables are very common concepts that most codebases will contain.
* We export them with a 'Dockview' prefix here to prevent accidental use by others.
* Events, Emitters and Disposables are very common concepts that many codebases will contain, however we need
* to export them for dockview framework packages to use.
* To be a good citizen these are exported with a `Dockview` prefix to prevent accidental use by others.
*/
export { Emitter as DockviewEmitter, Event as DockviewEvent } from './events';
export {
IDisposable as IDockviewDisposable,
IDisposable as DockviewIDisposable,
MutableDisposable as DockviewMutableDisposable,
CompositeDisposable as DockviewCompositeDisposable,
Disposable as DockviewDisposable,
} from './lifecycle';
export * from './panel/types';
export * from './panel/componentFactory';
export * from './splitview/splitview';
export {
SplitviewComponentOptions,
PanelViewInitParameters,
SplitviewOptions,
SplitviewFrameworkOptions,
PROPERTY_KEYS_SPLITVIEW,
} from './splitview/options';
export * from './paneview/paneview';
export * from './gridview/gridview';
export { GridviewComponentOptions } from './gridview/options';
export {
GridviewComponentOptions,
GridviewOptions,
GridviewFrameworkOptions,
PROPERTY_KEYS_GRIDVIEW,
} from './gridview/options';
export * from './gridview/baseComponentGridview';
export * from './paneview/draggablePaneviewPanel';
export {
DraggablePaneviewPanel,
PaneviewDidDropEvent as PaneviewDropEvent,
} from './paneview/draggablePaneviewPanel';
export * from './dockview/components/panel/content';
export * from './dockview/components/tab/tab';
@ -47,22 +64,33 @@ export {
} from './dockview/framework';
export * from './dockview/options';
export * from './dockview/theme';
export * from './dockview/dockviewPanel';
export * from './dockview/components/tab/defaultTab';
export * from './dockview/deserializer';
export { DefaultTab } from './dockview/components/tab/defaultTab';
export {
DefaultDockviewDeserialzier,
IPanelDeserializer,
} from './dockview/deserializer';
export * from './dockview/dockviewComponent';
export * from './gridview/gridviewComponent';
export * from './splitview/splitviewComponent';
export * from './paneview/paneviewComponent';
export { PaneviewComponentOptions } from './paneview/options';
export {
PaneviewComponentOptions,
PaneviewOptions,
PaneviewFrameworkOptions,
PROPERTY_KEYS_PANEVIEW,
PaneviewUnhandledDragOverEvent,
PaneviewDndOverlayEvent,
} from './paneview/options';
export * from './gridview/gridviewPanel';
export * from './splitview/splitviewPanel';
export { SplitviewPanel, ISplitviewPanel } from './splitview/splitviewPanel';
export * from './paneview/paneviewPanel';
export * from './dockview/types';
export { DockviewPanelRenderer } from './overlayRenderContainer';
export { DockviewPanelRenderer } from './overlay/overlayRenderContainer';
export {
Position,
@ -71,6 +99,7 @@ export {
MeasuredValue,
DroptargetOverlayModel,
} from './dnd/droptarget';
export {
FocusEvent,
PanelDimensionChangeEvent,
@ -87,6 +116,7 @@ export {
TitleEvent,
RendererChangedEvent,
DockviewPanelApi,
DockviewPanelMoveParams,
} from './api/dockviewPanelApi';
export {
PanelSizeEvent,
@ -97,6 +127,7 @@ export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi';
export {
DockviewGroupPanelApi,
DockviewGroupPanelFloatingChangeEvent,
DockviewGroupMoveParams,
} from './api/dockviewGroupPanelApi';
export {
CommonApi,
@ -105,3 +136,9 @@ export {
GridviewApi,
DockviewApi,
} from './api/component.api';
export {
createDockview,
createGridview,
createPaneview,
createSplitview,
} from './api/entryPoints';

View File

@ -1,6 +1,10 @@
export const clamp = (value: number, min: number, max: number): number => {
if (min > max) {
throw new Error(`${min} > ${max} is an invalid condition`);
/**
* caveat: an error should be thrown here if this was a proper `clamp` function but we need to handle
* cases where `min` > `max` and in those cases return `min`.
*/
return min;
}
return Math.min(max, Math.max(value, min));
};

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