goblin-magic 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/test/navigation.spec.js +117 -0
- package/widgets/cancelable-dialog/widget.js +116 -0
- package/widgets/dialog/widget.js +26 -25
- package/widgets/listener-stack/listener-stack.js +62 -0
- package/widgets/magic-dialog/widget.js +4 -20
- package/widgets/magic-navigation/service.js +111 -3
- package/widgets/magic-navigation/widget.js +31 -13
- package/widgets/menu/widget.js +22 -3
- package/widgets/menu-dialog/widget.js +52 -0
- package/widgets/movable/widget.js +1 -0
- package/widgets/tab-layout/styles.js +16 -0
- package/widgets/tab-layout/widget.js +86 -4
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goblin-magic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "goblin-magic",
|
|
5
5
|
"author": "Epsitec SA",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Yannick Vessaz <vessaz@epsitec.ch>",
|
|
8
|
-
"Mathieu Schroeter <schroeter@epsitec.ch>"
|
|
8
|
+
"Mathieu Schroeter <schroeter@epsitec.ch>",
|
|
9
|
+
"Samuel Loup <loup@epsitec.ch>"
|
|
9
10
|
],
|
|
10
11
|
"license": "MIT",
|
|
11
12
|
"config": {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {expect} = require('chai');
|
|
4
|
+
const {Elf} = require('xcraft-core-goblin/lib/test.js');
|
|
5
|
+
|
|
6
|
+
function restore(navLogic) {
|
|
7
|
+
navLogic.change('windows', {
|
|
8
|
+
'win@1': {
|
|
9
|
+
panelIds: ['panel@1', 'panel@2'],
|
|
10
|
+
dialogIds: ['dialog@1'],
|
|
11
|
+
activePanelId: 'panel@1',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
navLogic.change('panels', {
|
|
15
|
+
'panel@1': {
|
|
16
|
+
tabIds: ['tab@1', 'tab@2', 'tab@3'],
|
|
17
|
+
lastTabIds: [],
|
|
18
|
+
},
|
|
19
|
+
'panel@2': {
|
|
20
|
+
tabIds: ['tab@4', 'tab@5', 'tab@6'],
|
|
21
|
+
lastTabIds: [],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
navLogic.change('tabs', {
|
|
25
|
+
'tab@1': {highlighted: false},
|
|
26
|
+
'tab@2': {highlighted: false},
|
|
27
|
+
'tab@3': {highlighted: false},
|
|
28
|
+
'tab@4': {highlighted: false},
|
|
29
|
+
'tab@5': {highlighted: false},
|
|
30
|
+
'tab@6': {highlighted: false},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('goblin.magic.navigation', function () {
|
|
35
|
+
const {
|
|
36
|
+
MagicNavigationLogic,
|
|
37
|
+
} = require('../widgets/magic-navigation/service.js');
|
|
38
|
+
|
|
39
|
+
it('moveTab', function () {
|
|
40
|
+
const navLogic = Elf.trial(MagicNavigationLogic);
|
|
41
|
+
navLogic.create('magicNavigation@test');
|
|
42
|
+
|
|
43
|
+
// --- one panel -----------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/* 1,2,3 → 2,3,1 */
|
|
46
|
+
restore(navLogic);
|
|
47
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@1', 'tab@3', 'right');
|
|
48
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
49
|
+
.to.be.eql(['tab@2', 'tab@3', 'tab@1']);
|
|
50
|
+
|
|
51
|
+
/* 1,2,3 → 2,1,3 */
|
|
52
|
+
restore(navLogic);
|
|
53
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@1', 'tab@3', 'left');
|
|
54
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
55
|
+
.to.be.eql(['tab@2', 'tab@1', 'tab@3']);
|
|
56
|
+
|
|
57
|
+
/* 1,2,3 → 2,1,3 */
|
|
58
|
+
restore(navLogic);
|
|
59
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@1', 'tab@2', 'right');
|
|
60
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
61
|
+
.to.be.eql(['tab@2', 'tab@1', 'tab@3']);
|
|
62
|
+
|
|
63
|
+
/* 1,2,3 → 1,2,3 */
|
|
64
|
+
restore(navLogic);
|
|
65
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@1', 'tab@2', 'left');
|
|
66
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
67
|
+
.to.be.eql(['tab@1', 'tab@2', 'tab@3']);
|
|
68
|
+
|
|
69
|
+
/* 1,2,3 → 3,1,2 */
|
|
70
|
+
restore(navLogic);
|
|
71
|
+
navLogic.moveTab('panel@1', 'tab@3', 'panel@1', 'tab@1', 'left');
|
|
72
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
73
|
+
.to.be.eql(['tab@3', 'tab@1', 'tab@2']);
|
|
74
|
+
|
|
75
|
+
/* 1,2,3 → 1,3,2 */
|
|
76
|
+
restore(navLogic);
|
|
77
|
+
navLogic.moveTab('panel@1', 'tab@3', 'panel@1', 'tab@1', 'right');
|
|
78
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
79
|
+
.to.be.eql(['tab@1', 'tab@3', 'tab@2']);
|
|
80
|
+
|
|
81
|
+
// --- two panels ----------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/* 2,3 → 4,5,6,1 */
|
|
84
|
+
restore(navLogic);
|
|
85
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@2', 'tab@6', 'right');
|
|
86
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
87
|
+
.to.be.eql(['tab@2', 'tab@3']);
|
|
88
|
+
expect(navLogic.state.panels['panel@2'].tabIds.toJS()) //
|
|
89
|
+
.to.be.eql(['tab@4', 'tab@5', 'tab@6', 'tab@1']);
|
|
90
|
+
|
|
91
|
+
/* 1,3 → 2,4,5,6 */
|
|
92
|
+
restore(navLogic);
|
|
93
|
+
navLogic.moveTab('panel@1', 'tab@2', 'panel@2', 'tab@4', 'left');
|
|
94
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
95
|
+
.to.be.eql(['tab@1', 'tab@3']);
|
|
96
|
+
expect(navLogic.state.panels['panel@2'].tabIds.toJS()) //
|
|
97
|
+
.to.be.eql(['tab@2', 'tab@4', 'tab@5', 'tab@6']);
|
|
98
|
+
|
|
99
|
+
/* 1,2 → 4,5,3,6 */
|
|
100
|
+
restore(navLogic);
|
|
101
|
+
navLogic.moveTab('panel@1', 'tab@3', 'panel@2', 'tab@5', 'right');
|
|
102
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
103
|
+
.to.be.eql(['tab@1', 'tab@2']);
|
|
104
|
+
expect(navLogic.state.panels['panel@2'].tabIds.toJS()) //
|
|
105
|
+
.to.be.eql(['tab@4', 'tab@5', 'tab@3', 'tab@6']);
|
|
106
|
+
|
|
107
|
+
/* . → 2,4,1,5,6,3 */
|
|
108
|
+
restore(navLogic);
|
|
109
|
+
navLogic.moveTab('panel@1', 'tab@2', 'panel@2', 'tab@4', 'left');
|
|
110
|
+
navLogic.moveTab('panel@1', 'tab@1', 'panel@2', 'tab@5', 'left');
|
|
111
|
+
navLogic.moveTab('panel@1', 'tab@3', 'panel@2', 'tab@6', 'right');
|
|
112
|
+
expect(navLogic.state.panels['panel@1'].tabIds.toJS()) //
|
|
113
|
+
.to.be.eql([]);
|
|
114
|
+
expect(navLogic.state.panels['panel@2'].tabIds.toJS()) //
|
|
115
|
+
.to.be.eql(['tab@2', 'tab@4', 'tab@1', 'tab@5', 'tab@6', 'tab@3']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Widget from 'goblin-laboratory/widgets/widget';
|
|
3
|
+
import Dialog from '../dialog/widget.js';
|
|
4
|
+
import ListenerStack from '../listener-stack/listener-stack.js';
|
|
5
|
+
|
|
6
|
+
const listenerStack = new ListenerStack();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dialog with custom event handling.
|
|
10
|
+
*
|
|
11
|
+
* When an html dialog with closedby="any" or closedby="closerequest" is closed using ESC,
|
|
12
|
+
* it's not always possible to cancel the 'cancel' event. See https://issues.chromium.org/issues/351867704.
|
|
13
|
+
* This component provides a dialog where the 'cancel' event can be canceled.
|
|
14
|
+
*/
|
|
15
|
+
export default class CancelableDialog extends Widget {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
/** @type {React.RefObject<Dialog>} */
|
|
19
|
+
this.dialog = React.createRef();
|
|
20
|
+
this.closeEnabled = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get dialogElement() {
|
|
24
|
+
return this.dialog.current.dialog.current;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
componentDidMount() {
|
|
28
|
+
this.updateEscHandler();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
componentDidUpdate() {
|
|
32
|
+
this.updateEscHandler();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
componentWillUnmount() {
|
|
36
|
+
this.removeEscHandler();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updateEscHandler() {
|
|
40
|
+
if (this.props.open) {
|
|
41
|
+
this.addEscHandler();
|
|
42
|
+
} else {
|
|
43
|
+
this.removeEscHandler();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addEscHandler() {
|
|
48
|
+
// Do action only on the latest opened dialog
|
|
49
|
+
listenerStack.push('keydown', this.handleKeyDown);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
removeEscHandler() {
|
|
53
|
+
listenerStack.pop('keydown', this.handleKeyDown);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
close = () => {
|
|
57
|
+
this.dialog.current?.close();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
cancel = (reason) => {
|
|
61
|
+
const event = new CustomEvent('cancel', {
|
|
62
|
+
detail: {reason},
|
|
63
|
+
cancelable: true,
|
|
64
|
+
});
|
|
65
|
+
this.props.onCancel?.(event);
|
|
66
|
+
if (!event.defaultPrevented) {
|
|
67
|
+
this.dialog.current.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
handlePointerDown = (event) => {
|
|
72
|
+
this.props.onPointerDown?.(event);
|
|
73
|
+
if (event.target === this.dialogElement) {
|
|
74
|
+
this.closeEnabled = true;
|
|
75
|
+
} else {
|
|
76
|
+
this.closeEnabled = false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {PointerEvent} event
|
|
82
|
+
*/
|
|
83
|
+
handlePointerUp = (event) => {
|
|
84
|
+
this.props.onPointerUp?.(event);
|
|
85
|
+
if (this.closeEnabled && event.target === this.dialogElement) {
|
|
86
|
+
this.cancel('outside-click');
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
}
|
|
89
|
+
this.closeEnabled = false;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {KeyboardEvent} event
|
|
94
|
+
*/
|
|
95
|
+
handleKeyDown = (event) => {
|
|
96
|
+
if (event.defaultPrevented) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (event.key === 'Escape') {
|
|
100
|
+
this.cancel('escape-key');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
render() {
|
|
105
|
+
const {onCancel, ...props} = this.props;
|
|
106
|
+
return (
|
|
107
|
+
<Dialog
|
|
108
|
+
closedby="none"
|
|
109
|
+
{...props}
|
|
110
|
+
ref={this.dialog}
|
|
111
|
+
onPointerDown={this.handlePointerDown}
|
|
112
|
+
onPointerUp={this.handlePointerUp}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/widgets/dialog/widget.js
CHANGED
|
@@ -11,13 +11,37 @@ export default class Dialog extends Widget {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
componentDidMount() {
|
|
14
|
+
this.updateToggleListener();
|
|
14
15
|
this.update();
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
componentDidUpdate() {
|
|
19
|
+
this.updateToggleListener();
|
|
18
20
|
this.update();
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
componentWillUnmount() {
|
|
24
|
+
this.removeToggleListener();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
updateToggleListener() {
|
|
28
|
+
// Using the `onToggle` prop on the <dialog> component does not work.
|
|
29
|
+
// So we register the 'toggle' event manually.
|
|
30
|
+
if (this.props.onToggle) {
|
|
31
|
+
this.addToggleListener();
|
|
32
|
+
} else {
|
|
33
|
+
this.removeToggleListener();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addToggleListener() {
|
|
38
|
+
this.dialog.current?.addEventListener('toggle', this.props.onToggle);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
removeToggleListener() {
|
|
42
|
+
this.dialog.current?.removeEventListener('toggle', this.props.onToggle);
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
close = () => {
|
|
22
46
|
this.dialog.current?.close();
|
|
23
47
|
};
|
|
@@ -27,23 +51,6 @@ export default class Dialog extends Widget {
|
|
|
27
51
|
event.stopPropagation();
|
|
28
52
|
};
|
|
29
53
|
|
|
30
|
-
handlePointerDown = (event) => {
|
|
31
|
-
this.props.onPointerDown?.(event);
|
|
32
|
-
if (event.target === this.dialog.current) {
|
|
33
|
-
this.closeEnabled = true;
|
|
34
|
-
} else {
|
|
35
|
-
this.closeEnabled = false;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
handlePointerUp = (event) => {
|
|
40
|
-
this.props.onPointerUp?.(event);
|
|
41
|
-
if (this.closeEnabled && event.target === this.dialog.current) {
|
|
42
|
-
this.dialog.current.close();
|
|
43
|
-
}
|
|
44
|
-
this.closeEnabled = false;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
54
|
update() {
|
|
48
55
|
if (this.props.open) {
|
|
49
56
|
if (!this.dialog.current?.open) {
|
|
@@ -59,15 +66,9 @@ export default class Dialog extends Widget {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
render() {
|
|
62
|
-
const {open, modal, portal = false, ...props} = this.props;
|
|
69
|
+
const {open, modal, portal = false, onToggle, ...props} = this.props;
|
|
63
70
|
const dialog = (
|
|
64
|
-
<dialog
|
|
65
|
-
{...props}
|
|
66
|
-
ref={this.dialog}
|
|
67
|
-
onClose={this.handleClose}
|
|
68
|
-
onPointerDown={this.handlePointerDown}
|
|
69
|
-
onPointerUp={this.handlePointerUp}
|
|
70
|
-
/>
|
|
71
|
+
<dialog {...props} ref={this.dialog} onClose={this.handleClose} />
|
|
71
72
|
);
|
|
72
73
|
|
|
73
74
|
if (portal) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export default class ListenerStack {
|
|
4
|
+
/** @type {Map<string,Function[]>} */
|
|
5
|
+
stacks = new Map();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} type
|
|
9
|
+
* @returns {Function[]}
|
|
10
|
+
*/
|
|
11
|
+
getStack(type) {
|
|
12
|
+
let stack = this.stacks.get(type);
|
|
13
|
+
if (!stack) {
|
|
14
|
+
stack = [];
|
|
15
|
+
this.stacks.set(type, stack);
|
|
16
|
+
}
|
|
17
|
+
return stack;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Event} event
|
|
22
|
+
*/
|
|
23
|
+
handleEvent = (event) => {
|
|
24
|
+
const stack = this.getStack(event.type);
|
|
25
|
+
const last = stack.at(-1);
|
|
26
|
+
if (!last) {
|
|
27
|
+
throw new Error('Empty stack');
|
|
28
|
+
}
|
|
29
|
+
last(event);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template {keyof WindowEventMap} T
|
|
34
|
+
* @param {T} type
|
|
35
|
+
* @param {(event: WindowEventMap[T]) => any} listener
|
|
36
|
+
*/
|
|
37
|
+
push(type, listener) {
|
|
38
|
+
const stack = this.getStack(type);
|
|
39
|
+
stack.push(listener);
|
|
40
|
+
if (stack.length === 1) {
|
|
41
|
+
window.addEventListener(type, this.handleEvent);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @template {keyof WindowEventMap} T
|
|
47
|
+
* @param {T} type
|
|
48
|
+
* @param {(event: WindowEventMap[T]) => any} listener
|
|
49
|
+
*/
|
|
50
|
+
pop(type, listener) {
|
|
51
|
+
const stack = this.getStack(type);
|
|
52
|
+
const index = stack.indexOf(listener);
|
|
53
|
+
if (index === -1) {
|
|
54
|
+
console.error(`Unknown listener for type ${type}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
stack.splice(index, 1);
|
|
58
|
+
if (stack.length === 0) {
|
|
59
|
+
window.removeEventListener(type, this.handleEvent);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Widget from 'goblin-laboratory/widgets/widget';
|
|
3
3
|
import * as styles from './styles.js';
|
|
4
|
-
import Dialog from '../dialog/widget.js';
|
|
5
4
|
import Movable from '../movable/widget.js';
|
|
6
5
|
import isEmptyAreaElement from '../element-helpers/is-empty-area-element.js';
|
|
6
|
+
import CancelableDialog from '../cancelable-dialog/widget.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {PointerEvent} event1
|
|
@@ -21,7 +21,7 @@ class MagicDialog extends Widget {
|
|
|
21
21
|
constructor() {
|
|
22
22
|
super(...arguments);
|
|
23
23
|
this.styles = styles;
|
|
24
|
-
/** @type {React.RefObject<
|
|
24
|
+
/** @type {React.RefObject<CancelableDialog>} */
|
|
25
25
|
this.dialog = React.createRef();
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -57,7 +57,7 @@ class MagicDialog extends Widget {
|
|
|
57
57
|
|
|
58
58
|
handlePointerDown = (event) => {
|
|
59
59
|
const {target} = event;
|
|
60
|
-
const dialog = this.dialog.current?.
|
|
60
|
+
const dialog = this.dialog.current?.dialogElement;
|
|
61
61
|
if (dialog && !dialog.contains(target)) {
|
|
62
62
|
if (isEmptyAreaElement(target)) {
|
|
63
63
|
this.downEvent = event;
|
|
@@ -76,30 +76,14 @@ class MagicDialog extends Widget {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
handleKeyDown = (event) => {
|
|
80
|
-
this.props.onKeyDown?.(event);
|
|
81
|
-
if (event.defaultPrevented) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (event.key === 'Escape') {
|
|
85
|
-
this.dialog.current.close();
|
|
86
|
-
event.stopPropagation();
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
79
|
render() {
|
|
91
80
|
const {className = '', ...props} = this.props;
|
|
92
81
|
return (
|
|
93
82
|
<Movable>
|
|
94
83
|
{(moveableProps) => (
|
|
95
|
-
<
|
|
84
|
+
<CancelableDialog
|
|
96
85
|
{...props}
|
|
97
86
|
{...moveableProps}
|
|
98
|
-
onKeyDown={
|
|
99
|
-
!this.props.modal && this.props.onClose
|
|
100
|
-
? this.handleKeyDown
|
|
101
|
-
: undefined
|
|
102
|
-
}
|
|
103
87
|
ref={this.dialog}
|
|
104
88
|
className={this.styles.classNames.dialog + ' ' + className}
|
|
105
89
|
open={this.props.open}
|
|
@@ -258,6 +258,40 @@ class MagicNavigationLogic extends Elf.Spirit {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* @param {id} srcPanelId
|
|
263
|
+
* @param {id} srcTabId
|
|
264
|
+
* @param {id} dstPanelId
|
|
265
|
+
* @param {id} dstTabId
|
|
266
|
+
* @param {`left`|`right`} side
|
|
267
|
+
*/
|
|
268
|
+
moveTab(srcPanelId, srcTabId, dstPanelId, dstTabId, side) {
|
|
269
|
+
const {state} = this;
|
|
270
|
+
const tabs = [...state.panels[dstPanelId].tabIds];
|
|
271
|
+
|
|
272
|
+
if (srcPanelId === dstPanelId) {
|
|
273
|
+
const srcIndex = state.panels[srcPanelId].tabIds.indexOf(srcTabId);
|
|
274
|
+
if (side === 'right') {
|
|
275
|
+
tabs.splice(srcIndex, 1);
|
|
276
|
+
} else {
|
|
277
|
+
tabs.splice(srcIndex, 1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (side === 'right') {
|
|
282
|
+
const dstIndex = tabs.indexOf(dstTabId);
|
|
283
|
+
tabs.splice(dstIndex + 1, 0, srcTabId);
|
|
284
|
+
} else {
|
|
285
|
+
const dstIndex = tabs.indexOf(dstTabId);
|
|
286
|
+
tabs.splice(dstIndex, 0, srcTabId);
|
|
287
|
+
}
|
|
288
|
+
state.panels[dstPanelId].tabIds = tabs;
|
|
289
|
+
|
|
290
|
+
if (srcPanelId !== dstPanelId) {
|
|
291
|
+
this._removeTabAndUpdatePanel(state, srcPanelId, srcTabId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
261
295
|
/**
|
|
262
296
|
* @param {typeof this["state"]} state
|
|
263
297
|
* @param {id} panelId
|
|
@@ -574,7 +608,7 @@ class MagicNavigation extends Elf {
|
|
|
574
608
|
* @param {id} parentId
|
|
575
609
|
* @param {string} prompt
|
|
576
610
|
* @param {object} [options]
|
|
577
|
-
* @param {'default' | 'yes-no'} [options.kind]
|
|
611
|
+
* @param {'default' | 'yes-no' | 'yes-no-cancel'} [options.kind]
|
|
578
612
|
* @param {string} [options.advice]
|
|
579
613
|
* @param {string} [options.okLabel]
|
|
580
614
|
* @param {string} [options.noLabel]
|
|
@@ -765,8 +799,14 @@ class MagicNavigation extends Elf {
|
|
|
765
799
|
}
|
|
766
800
|
|
|
767
801
|
async _hasSameWidget(view1, view2) {
|
|
768
|
-
const widget1 =
|
|
769
|
-
|
|
802
|
+
const widget1 =
|
|
803
|
+
view1.widget ||
|
|
804
|
+
getServiceName(view1.service) ||
|
|
805
|
+
view1.serviceId.split('@', 1)[0];
|
|
806
|
+
const widget2 =
|
|
807
|
+
view2.widget ||
|
|
808
|
+
getServiceName(view2.service) ||
|
|
809
|
+
view2.serviceId.split('@', 1)[0];
|
|
770
810
|
return (
|
|
771
811
|
widget1[0].toLowerCase() + widget1.slice(1) ===
|
|
772
812
|
widget2[0].toLowerCase() + widget2.slice(1)
|
|
@@ -949,6 +989,14 @@ class MagicNavigation extends Elf {
|
|
|
949
989
|
)?.[0];
|
|
950
990
|
}
|
|
951
991
|
|
|
992
|
+
/**
|
|
993
|
+
* @param {id} tabId
|
|
994
|
+
* @returns {Promise<string | null | undefined>}
|
|
995
|
+
*/
|
|
996
|
+
async getServiceId(tabId) {
|
|
997
|
+
return this.state.tabs[tabId]?.serviceId;
|
|
998
|
+
}
|
|
999
|
+
|
|
952
1000
|
/**
|
|
953
1001
|
* @param {id} panelId
|
|
954
1002
|
* @returns {Promise<DesktopId | undefined >}
|
|
@@ -985,6 +1033,26 @@ class MagicNavigation extends Elf {
|
|
|
985
1033
|
this.logic.activateTab(panelId, tabId, keepHistory);
|
|
986
1034
|
}
|
|
987
1035
|
|
|
1036
|
+
/**
|
|
1037
|
+
* @param {id} srcTabId
|
|
1038
|
+
* @param {id} dstTabId
|
|
1039
|
+
* @param {`left`|`right`} side
|
|
1040
|
+
*/
|
|
1041
|
+
async moveTab(srcTabId, dstTabId, side) {
|
|
1042
|
+
if (srcTabId === dstTabId) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const srcPanelId = await this.findPanelId(srcTabId);
|
|
1046
|
+
if (!srcPanelId) {
|
|
1047
|
+
throw new Error(`Unknown tab '${srcTabId}'`);
|
|
1048
|
+
}
|
|
1049
|
+
const dstPanelId = await this.findPanelId(dstTabId);
|
|
1050
|
+
if (!dstPanelId) {
|
|
1051
|
+
throw new Error(`Unknown tab '${dstTabId}'`);
|
|
1052
|
+
}
|
|
1053
|
+
this.logic.moveTab(srcPanelId, srcTabId, dstPanelId, dstTabId, side);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
988
1056
|
/**
|
|
989
1057
|
* @param {DesktopId} desktopId
|
|
990
1058
|
* @returns {Promise<id | null | undefined>}
|
|
@@ -1226,6 +1294,34 @@ class MagicNavigation extends Elf {
|
|
|
1226
1294
|
this.quest.evt(`${dialogId}-closed`, result);
|
|
1227
1295
|
}
|
|
1228
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* @param {id} viewId
|
|
1299
|
+
* @param {any} [result]
|
|
1300
|
+
* @returns {Promise<boolean>}
|
|
1301
|
+
*/
|
|
1302
|
+
async _onCloseRequested(viewId, result) {
|
|
1303
|
+
const view = this.views.get(viewId);
|
|
1304
|
+
if (!view) {
|
|
1305
|
+
throw new Error(`Missing view '${viewId}'`);
|
|
1306
|
+
}
|
|
1307
|
+
const serviceId = this.state.tabs[viewId].serviceId;
|
|
1308
|
+
if (view.service && serviceId) {
|
|
1309
|
+
if (typeof view.service === 'string') {
|
|
1310
|
+
const serviceAPI = this.quest.getAPI(serviceId);
|
|
1311
|
+
if (serviceAPI.onCloseRequested) {
|
|
1312
|
+
return await serviceAPI.onCloseRequested(result);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
const ServiceClass = view.service;
|
|
1316
|
+
const serviceAPI = await new ServiceClass(this).api(serviceId);
|
|
1317
|
+
if (serviceAPI.onCloseRequested) {
|
|
1318
|
+
return await serviceAPI.onCloseRequested(result);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1229
1325
|
/**
|
|
1230
1326
|
* @param {id} viewOrServiceId id of dialog, tab or service
|
|
1231
1327
|
* @returns {Promise<any>}
|
|
@@ -1283,6 +1379,18 @@ class MagicNavigation extends Elf {
|
|
|
1283
1379
|
}
|
|
1284
1380
|
}
|
|
1285
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* @param {DesktopId} desktopId
|
|
1384
|
+
* @param {id} viewId
|
|
1385
|
+
* @param {any} [result]
|
|
1386
|
+
*/
|
|
1387
|
+
async requestClose(desktopId, viewId, result) {
|
|
1388
|
+
const canBeClosed = await this._onCloseRequested(viewId, result);
|
|
1389
|
+
if (canBeClosed) {
|
|
1390
|
+
await this.closeView(desktopId, viewId, result);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1286
1394
|
/**
|
|
1287
1395
|
* @param {id} viewOrServiceId
|
|
1288
1396
|
* @param {DesktopId} desktopId
|
|
@@ -210,30 +210,35 @@ const MagicNavigationTab = withC(MagicNavigationTabNC);
|
|
|
210
210
|
let MagicNavigationTabs = class extends Widget {
|
|
211
211
|
constructor() {
|
|
212
212
|
super(...arguments);
|
|
213
|
-
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
214
|
-
this.setTab = this.setTab.bind(this);
|
|
215
|
-
this.handleAuxClick = this.handleAuxClick.bind(this);
|
|
216
213
|
}
|
|
217
214
|
|
|
218
|
-
handleKeyDown(event) {
|
|
215
|
+
handleKeyDown = (event) => {
|
|
219
216
|
if (event.key === 'ArrowLeft') {
|
|
220
217
|
this.doFor('magicNavigation@main', 'switchTab', {reverse: true});
|
|
221
218
|
} else if (event.key === 'ArrowRight') {
|
|
222
219
|
this.doFor('magicNavigation@main', 'switchTab');
|
|
223
220
|
}
|
|
224
|
-
}
|
|
221
|
+
};
|
|
225
222
|
|
|
226
|
-
setTab(tabId) {
|
|
223
|
+
setTab = (tabId) => {
|
|
227
224
|
this.doFor('magicNavigation@main', 'activateTab', {tabId});
|
|
228
|
-
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
moveTab = (srcTabId, dstTabId, side) => {
|
|
228
|
+
this.doFor('magicNavigation@main', 'moveTab', {
|
|
229
|
+
srcTabId,
|
|
230
|
+
dstTabId,
|
|
231
|
+
side,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
229
234
|
|
|
230
|
-
handleAuxClick(event) {
|
|
235
|
+
handleAuxClick = (event) => {
|
|
231
236
|
if (event.button === 1) {
|
|
232
237
|
// Middle click
|
|
233
238
|
const tabId = event.currentTarget.dataset.value;
|
|
234
239
|
this.doFor('magicNavigation@main', 'closeTab', {tabId});
|
|
235
240
|
}
|
|
236
|
-
}
|
|
241
|
+
};
|
|
237
242
|
|
|
238
243
|
render() {
|
|
239
244
|
const tabIds = this.props.tabIds;
|
|
@@ -242,6 +247,7 @@ let MagicNavigationTabs = class extends Widget {
|
|
|
242
247
|
<MainTabs
|
|
243
248
|
currentTab={currentTabId}
|
|
244
249
|
onTabClick={this.setTab}
|
|
250
|
+
onTabDrop={this.moveTab}
|
|
245
251
|
tabIndex="0"
|
|
246
252
|
onKeyDown={this.handleKeyDown}
|
|
247
253
|
>
|
|
@@ -365,23 +371,35 @@ MagicNavigationPanels = withC(MagicNavigationPanels);
|
|
|
365
371
|
let MagicNavigationDialog = class extends Widget {
|
|
366
372
|
constructor() {
|
|
367
373
|
super(...arguments);
|
|
368
|
-
this.handleClose = this.handleClose.bind(this);
|
|
369
374
|
}
|
|
370
375
|
|
|
371
|
-
handleClose(event) {
|
|
376
|
+
handleClose = (event) => {
|
|
372
377
|
const returnValue = event.currentTarget.returnValue;
|
|
373
378
|
const dialogId = this.props.dialogId;
|
|
374
379
|
this.doFor('magicNavigation@main', 'closeDialog', {
|
|
375
380
|
dialogId,
|
|
376
381
|
result: returnValue !== '' ? returnValue : undefined,
|
|
377
382
|
});
|
|
378
|
-
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
handleCancel = (event) => {
|
|
386
|
+
const dialogId = this.props.dialogId;
|
|
387
|
+
this.doFor('magicNavigation@main', 'requestClose', {
|
|
388
|
+
viewId: dialogId,
|
|
389
|
+
});
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
};
|
|
379
392
|
|
|
380
393
|
render() {
|
|
381
394
|
const dialogId = this.props.dialogId;
|
|
382
395
|
const widgetProps = this.props.view.get('widgetProps')?.toObject();
|
|
383
396
|
return (
|
|
384
|
-
<MagicDialog
|
|
397
|
+
<MagicDialog
|
|
398
|
+
modal={widgetProps?.modal}
|
|
399
|
+
open
|
|
400
|
+
onClose={this.handleClose}
|
|
401
|
+
onCancel={this.handleCancel}
|
|
402
|
+
>
|
|
385
403
|
<MagicNavigationView
|
|
386
404
|
id={this.props.id}
|
|
387
405
|
viewId={dialogId}
|
package/widgets/menu/widget.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Widget from 'goblin-laboratory/widgets/widget';
|
|
3
3
|
import * as styles from './styles.js';
|
|
4
|
-
import Dialog from '../dialog/widget.js';
|
|
5
4
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
|
6
5
|
import {faBars, faChevronRight} from '@fortawesome/free-solid-svg-icons';
|
|
7
6
|
import MagicButton from 'goblin-magic/widgets/magic-button/widget.js';
|
|
8
7
|
import WithComputedSize from '../with-computed-size/widget.js';
|
|
8
|
+
import MenuDialog from '../menu-dialog/widget.js';
|
|
9
9
|
|
|
10
10
|
const MenuContext = React.createContext();
|
|
11
11
|
const MenuStateContext = React.createContext();
|
|
@@ -254,6 +254,23 @@ class MenuContent extends Widget {
|
|
|
254
254
|
this.stopPropagation = this.stopPropagation.bind(this);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* @param {ToggleEvent} event
|
|
259
|
+
*/
|
|
260
|
+
setFocus = (event) => {
|
|
261
|
+
if (event.newState !== 'open') {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
/** @type {HTMLDialogElement} */
|
|
265
|
+
const dialog = event.currentTarget;
|
|
266
|
+
if (!document.activeElement || document.activeElement === document.body) {
|
|
267
|
+
const firstFocusable = dialog.querySelector('[tabindex]');
|
|
268
|
+
if (firstFocusable) {
|
|
269
|
+
firstFocusable.focus();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
257
274
|
/**
|
|
258
275
|
* @param {Event} event
|
|
259
276
|
* @param {Menu} menu
|
|
@@ -290,6 +307,7 @@ class MenuContent extends Widget {
|
|
|
290
307
|
} else if (event.key === 'ArrowUp') {
|
|
291
308
|
this.focusNext(event, -1);
|
|
292
309
|
}
|
|
310
|
+
event.stopPropagation();
|
|
293
311
|
};
|
|
294
312
|
|
|
295
313
|
getStyle(state, size) {
|
|
@@ -461,9 +479,10 @@ class MenuContent extends Widget {
|
|
|
461
479
|
...props
|
|
462
480
|
} = this.props;
|
|
463
481
|
return (
|
|
464
|
-
<
|
|
482
|
+
<MenuDialog
|
|
465
483
|
open={open}
|
|
466
484
|
modal={modal}
|
|
485
|
+
onToggle={this.setFocus}
|
|
467
486
|
onClose={(event) => this.handleClose(event, menu)}
|
|
468
487
|
className={this.styles.classNames.menuDialog}
|
|
469
488
|
portal={portal}
|
|
@@ -496,7 +515,7 @@ class MenuContent extends Widget {
|
|
|
496
515
|
}}
|
|
497
516
|
</WithComputedSize>
|
|
498
517
|
)}
|
|
499
|
-
</
|
|
518
|
+
</MenuDialog>
|
|
500
519
|
);
|
|
501
520
|
}
|
|
502
521
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Widget from 'goblin-laboratory/widgets/widget';
|
|
3
|
+
import Dialog from '../dialog/widget.js';
|
|
4
|
+
|
|
5
|
+
export default class MenuDialog extends Widget {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(...arguments);
|
|
8
|
+
/** @type {React.RefObject<Dialog>} */
|
|
9
|
+
this.dialog = React.createRef();
|
|
10
|
+
this.closeEnabled = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get dialogElement() {
|
|
14
|
+
return this.dialog.current.dialog.current;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
close = () => {
|
|
18
|
+
this.dialog.current?.close();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
handlePointerDown = (event) => {
|
|
22
|
+
this.props.onPointerDown?.(event);
|
|
23
|
+
if (event.target === this.dialogElement) {
|
|
24
|
+
this.closeEnabled = true;
|
|
25
|
+
} else {
|
|
26
|
+
this.closeEnabled = false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
handlePointerUp = (event) => {
|
|
31
|
+
this.props.onPointerUp?.(event);
|
|
32
|
+
if (this.closeEnabled && event.target === this.dialogElement) {
|
|
33
|
+
this.dialog.current.close();
|
|
34
|
+
}
|
|
35
|
+
this.closeEnabled = false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
const {onCancel, ...props} = this.props;
|
|
40
|
+
return (
|
|
41
|
+
<Dialog
|
|
42
|
+
closedby="any"
|
|
43
|
+
// Even with closedby="any", the dialog is not closed by clicking on the backdrop.
|
|
44
|
+
// So we add custom pointerDown and pointerUp handlers.
|
|
45
|
+
{...props}
|
|
46
|
+
ref={this.dialog}
|
|
47
|
+
onPointerDown={this.handlePointerDown}
|
|
48
|
+
onPointerUp={this.handlePointerUp}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -22,6 +22,22 @@ export default function styles() {
|
|
|
22
22
|
|
|
23
23
|
':active': {},
|
|
24
24
|
},
|
|
25
|
+
|
|
26
|
+
'& > .drop-left': {
|
|
27
|
+
willChange: 'box-shadow',
|
|
28
|
+
position: 'relative',
|
|
29
|
+
transition: 'box-shadow 0.2s ease',
|
|
30
|
+
boxShadow:
|
|
31
|
+
'inset 4px 0 0 color-mix(in srgb, var(--text-color), transparent 40%)',
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
'& > .drop-right': {
|
|
35
|
+
willChange: 'box-shadow',
|
|
36
|
+
position: 'relative',
|
|
37
|
+
transition: 'box-shadow 0.2s ease',
|
|
38
|
+
boxShadow:
|
|
39
|
+
'inset -4px 0 0 color-mix(in srgb, var(--text-color), transparent 40%)',
|
|
40
|
+
},
|
|
25
41
|
};
|
|
26
42
|
|
|
27
43
|
return {
|
|
@@ -26,15 +26,90 @@ class TabLayoutTabs extends Widget {
|
|
|
26
26
|
constructor() {
|
|
27
27
|
super(...arguments);
|
|
28
28
|
this.styles = styles;
|
|
29
|
-
this.handleTabClick = this.handleTabClick.bind(this);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
handleTabClick(value, event) {
|
|
31
|
+
handleTabClick = (value, event) => {
|
|
33
32
|
this.props.onTabClick?.(value, event);
|
|
34
|
-
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handleDragStart = (event, tabId) => {
|
|
36
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
37
|
+
event.dataTransfer.setData('text/plain', tabId);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
#applySideClass = (el, side) => {
|
|
41
|
+
if (!el) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (side === 'left') {
|
|
45
|
+
el.classList.add('drop-left');
|
|
46
|
+
el.classList.remove('drop-right');
|
|
47
|
+
} else {
|
|
48
|
+
el.classList.add('drop-right');
|
|
49
|
+
el.classList.remove('drop-left');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
#dropClasses = (el) => {
|
|
54
|
+
if (el) {
|
|
55
|
+
el.classList.remove('drop-left');
|
|
56
|
+
el.classList.remove('drop-right');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
#getSideFromEvent = (event) => {
|
|
61
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
62
|
+
const midX = rect.left + rect.width / 2;
|
|
63
|
+
return event.clientX < midX ? 'left' : 'right';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
handleDragEnter = (event) => {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
handleDragOver = (event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
event.dataTransfer.dropEffect = 'move';
|
|
75
|
+
|
|
76
|
+
const side = this.#getSideFromEvent(event);
|
|
77
|
+
if (side) {
|
|
78
|
+
this.#applySideClass(event.currentTarget, side);
|
|
79
|
+
} else {
|
|
80
|
+
this.#dropClasses(event.currentTarget);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
handleDragLeave = (event) => {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
|
|
88
|
+
const leaveToOutside = !event.currentTarget.contains(event.relatedTarget);
|
|
89
|
+
if (leaveToOutside) {
|
|
90
|
+
this.#dropClasses(event.currentTarget);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
handleDrop = (event, targetTabId) => {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
|
|
97
|
+
const el = event.currentTarget;
|
|
98
|
+
const side = this.#getSideFromEvent(event);
|
|
99
|
+
this.#dropClasses(el);
|
|
100
|
+
|
|
101
|
+
const draggedTabId = event.dataTransfer.getData('text/plain');
|
|
102
|
+
if (draggedTabId && targetTabId) {
|
|
103
|
+
this.props.onTabDrop?.(draggedTabId, targetTabId, side, event);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
handleDragEnd = (event) => {
|
|
108
|
+
this.#dropClasses(event?.currentTarget);
|
|
109
|
+
};
|
|
35
110
|
|
|
36
111
|
render() {
|
|
37
|
-
const {currentTab, onTabClick, ...props} = this.props;
|
|
112
|
+
const {currentTab, onTabClick, onTabDrop, ...props} = this.props;
|
|
38
113
|
const className = this.props.className || '';
|
|
39
114
|
return (
|
|
40
115
|
<div {...props} className={this.styles.classNames.tabs + ' ' + className}>
|
|
@@ -47,6 +122,13 @@ class TabLayoutTabs extends Widget {
|
|
|
47
122
|
'onClick': (event) =>
|
|
48
123
|
(child.props.onClick || this.handleTabClick)(value, event),
|
|
49
124
|
'data-active': currentTab === value,
|
|
125
|
+
'draggable': true,
|
|
126
|
+
'onDragStart': (e) => this.handleDragStart(e, value),
|
|
127
|
+
'onDragEnter': this.handleDragEnter,
|
|
128
|
+
'onDragOver': this.handleDragOver,
|
|
129
|
+
'onDragLeave': this.handleDragLeave,
|
|
130
|
+
'onDrop': (e) => this.handleDrop(e, value),
|
|
131
|
+
'onDragEnd': this.handleDragEnd,
|
|
50
132
|
});
|
|
51
133
|
})}
|
|
52
134
|
</div>
|