panel-splitjs 0.0.1a0__py3-none-any.whl → 0.1.0__py3-none-any.whl
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.
- panel_splitjs/__init__.py +2 -2
- panel_splitjs/_version.py +2 -2
- panel_splitjs/base.py +94 -70
- panel_splitjs/dist/css/splitjs.css +16 -12
- panel_splitjs/dist/panel-splitjs.bundle.js +1 -1
- panel_splitjs/models/multi_split.js +52 -0
- panel_splitjs/models/split.js +176 -0
- {panel_splitjs-0.0.1a0.dist-info → panel_splitjs-0.1.0.dist-info}/METADATA +115 -40
- panel_splitjs-0.1.0.dist-info/RECORD +17 -0
- panel_splitjs/models/splitjs.js +0 -215
- panel_splitjs-0.0.1a0.dist-info/RECORD +0 -16
- {panel_splitjs-0.0.1a0.dist-info → panel_splitjs-0.1.0.dist-info}/WHEEL +0 -0
panel_splitjs/__init__.py
CHANGED
panel_splitjs/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
panel_splitjs/base.py
CHANGED
|
@@ -3,7 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
import param
|
|
4
4
|
from bokeh.embed.bundle import extension_dirs
|
|
5
5
|
from panel.custom import Children, JSComponent
|
|
6
|
-
from panel.layout import Spacer
|
|
7
6
|
from panel.layout.base import ListLike
|
|
8
7
|
|
|
9
8
|
BASE_PATH = Path(__file__).parent
|
|
@@ -12,35 +11,61 @@ DIST_PATH = BASE_PATH / 'dist'
|
|
|
12
11
|
extension_dirs['panel-splitjs'] = DIST_PATH
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class
|
|
16
|
-
"""A Children parameter that only allows at most two items."""
|
|
14
|
+
class Size(param.Parameter):
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if len(val) == 1:
|
|
24
|
-
val.append(Spacer())
|
|
25
|
-
if len(val) == 0:
|
|
26
|
-
val.extend([Spacer(), Spacer()])
|
|
27
|
-
val = super()._transform_value(val)
|
|
28
|
-
return val
|
|
16
|
+
__slots__ = ['length']
|
|
17
|
+
|
|
18
|
+
def __init__(self, default=None, length=None, **params):
|
|
19
|
+
super().__init__(default=default, **params)
|
|
20
|
+
self.length = length
|
|
29
21
|
|
|
30
22
|
def _validate(self, val):
|
|
31
23
|
super()._validate(val)
|
|
32
|
-
if
|
|
24
|
+
if val is None:
|
|
33
25
|
return
|
|
34
|
-
if self.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
if self.length is not None and isinstance(val, tuple) and len(val) != self.length:
|
|
27
|
+
raise ValueError(f"Size parameter {self.name!r} must have length {self.length}")
|
|
28
|
+
if not (isinstance(val, (int, float)) or (isinstance(val, tuple) and all(isinstance(v, (int, float)) for v in val))):
|
|
29
|
+
raise ValueError(f"Size parameter {self.name!r} only takes int or float values")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SplitBase(JSComponent, ListLike):
|
|
33
|
+
|
|
34
|
+
max_size = Size(default=None, doc="""
|
|
35
|
+
The maximum sizes of the panels (in pixels) either as a single value or a tuple.""")
|
|
36
|
+
|
|
37
|
+
min_size = Size(default=None, doc="""
|
|
38
|
+
The minimum sizes of the panels (in pixels) either as a single value or a tuple.""")
|
|
39
|
+
|
|
40
|
+
objects = Children(doc="""
|
|
41
|
+
The list of child objects that make up the layout.""")
|
|
42
|
+
|
|
43
|
+
orientation = param.Selector(default="horizontal", objects=["horizontal", "vertical"], doc="""
|
|
44
|
+
The orientation of the split panel. Default is horizontal.""")
|
|
45
|
+
|
|
46
|
+
sizes = param.NumericTuple(default=None, length=0, doc="""
|
|
47
|
+
The sizes of the panels (as percentages) on initialization. The value is automatically
|
|
48
|
+
synced to the sizes of the panels in the frontend.""")
|
|
41
49
|
|
|
50
|
+
step_size = param.Integer(default=1, doc="""
|
|
51
|
+
The step size (in pixels) at which the size of the panels can be changed.""")
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
snap_size = param.Integer(default=30, doc="""
|
|
54
|
+
Snap to minimum size at this offset in pixels.""")
|
|
55
|
+
|
|
56
|
+
_bundle = DIST_PATH / "panel-splitjs.bundle.js"
|
|
57
|
+
_stylesheets = [DIST_PATH / "css" / "splitjs.css"]
|
|
58
|
+
|
|
59
|
+
__abstract = True
|
|
60
|
+
|
|
61
|
+
def _process_property_change(self, props):
|
|
62
|
+
props = super()._process_property_change(props)
|
|
63
|
+
if 'sizes' in props:
|
|
64
|
+
props['sizes'] = tuple(props['sizes'])
|
|
65
|
+
return props
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Split(SplitBase):
|
|
44
69
|
"""
|
|
45
70
|
Split is a component for creating a responsive split panel layout.
|
|
46
71
|
|
|
@@ -57,83 +82,82 @@ class Split(JSComponent, ListLike):
|
|
|
57
82
|
and a secondary panel that can be toggled (like a chat interface with output display).
|
|
58
83
|
"""
|
|
59
84
|
|
|
60
|
-
collapsed = param.
|
|
61
|
-
Whether the
|
|
62
|
-
When True, only one panel is visible (determined by invert).
|
|
63
|
-
When False, both panels are visible according to expanded_sizes.""")
|
|
85
|
+
collapsed = param.Integer(default=None, doc="""
|
|
86
|
+
Whether the first or second panel is collapsed. 0 for first panel, 1 for second panel, None for not collapsed.""")
|
|
64
87
|
|
|
65
88
|
expanded_sizes = param.NumericTuple(default=(50, 50), length=2, doc="""
|
|
66
89
|
The sizes of the two panels when expanded (as percentages).
|
|
67
|
-
Default is (50, 50)
|
|
68
|
-
and the right panel takes up 65% when expanded.
|
|
90
|
+
Default is (50, 50) .
|
|
69
91
|
When invert=True, these percentages are automatically swapped.""")
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
max_size = Size(default=None, length=2, doc="""
|
|
94
|
+
The maximum sizes of the panels (in pixels) either as a single value or a tuple of two values.""")
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
The minimum sizes of the
|
|
76
|
-
Default is (0, 0) which allows both panels to fully collapse.
|
|
77
|
-
Set to (300, 0) or similar values if you want to enforce minimum widths during dragging.
|
|
78
|
-
When invert=True, these values are automatically swapped.""")
|
|
96
|
+
min_size = Size(default=0, length=2, doc="""
|
|
97
|
+
The minimum sizes of the panels (in pixels) either as a single value or a tuple of two values.""")
|
|
79
98
|
|
|
80
|
-
objects =
|
|
99
|
+
objects = Children(doc="""
|
|
81
100
|
The component to place in the left panel.
|
|
82
101
|
When invert=True, this will appear on the right side.""")
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
The orientation of the split panel. Default is horizontal.""")
|
|
86
|
-
|
|
87
|
-
show_buttons = param.Boolean(default=False, doc="""
|
|
103
|
+
show_buttons = param.Boolean(default=True, doc="""
|
|
88
104
|
Whether to show the toggle buttons on the divider.
|
|
89
105
|
When False, the buttons are hidden and panels can only be resized by dragging.""")
|
|
90
106
|
|
|
91
|
-
sizes = param.NumericTuple(default=(
|
|
107
|
+
sizes = param.NumericTuple(default=(50, 50), length=2, doc="""
|
|
92
108
|
The initial sizes of the two panels (as percentages).
|
|
93
|
-
Default is (
|
|
109
|
+
Default is (50, 50) which means the left panel takes up 50% of the space
|
|
94
110
|
and the right panel is not visible.""")
|
|
95
111
|
|
|
96
|
-
|
|
97
|
-
_esm = Path(__file__).parent / "models" / "splitjs.js"
|
|
98
|
-
|
|
99
|
-
_stylesheets = [DIST_PATH / "css" / "splitjs.css"]
|
|
112
|
+
_esm = Path(__file__).parent / "models" / "split.js"
|
|
100
113
|
|
|
101
114
|
def __init__(self, *objects, **params):
|
|
102
115
|
if objects:
|
|
103
116
|
params["objects"] = list(objects)
|
|
117
|
+
if "objects" in params:
|
|
118
|
+
objects = params["objects"]
|
|
119
|
+
if len(objects) > 2:
|
|
120
|
+
raise ValueError("Split component must have at most two children.")
|
|
104
121
|
super().__init__(**params)
|
|
105
|
-
if self.invert:
|
|
106
|
-
# Swap min_sizes when inverted
|
|
107
|
-
left_min, right_min = self.min_sizes
|
|
108
|
-
self.min_sizes = (right_min, left_min)
|
|
109
|
-
|
|
110
|
-
# Swap expanded_sizes when inverted
|
|
111
|
-
left_exp, right_exp = self.expanded_sizes
|
|
112
|
-
self.expanded_sizes = (right_exp, left_exp)
|
|
113
|
-
|
|
114
|
-
@param.depends("collapsed", watch=True)
|
|
115
|
-
def _send_collapsed_update(self):
|
|
116
|
-
"""Send message to JS when collapsed state changes in Python"""
|
|
117
|
-
self._send_msg({"type": "update_collapsed", "collapsed": self.collapsed})
|
|
118
|
-
|
|
119
|
-
def _handle_msg(self, msg):
|
|
120
|
-
"""Handle messages from JS"""
|
|
121
|
-
if 'collapsed' in msg:
|
|
122
|
-
collapsed = msg['collapsed']
|
|
123
|
-
with param.discard_events(self):
|
|
124
|
-
# Important to discard so when user drags the panel, it doesn't
|
|
125
|
-
# expand to the expanded sizes
|
|
126
|
-
self.collapsed = collapsed
|
|
127
122
|
|
|
128
123
|
|
|
129
124
|
class HSplit(Split):
|
|
125
|
+
"""
|
|
126
|
+
HSplit is a component for creating a responsive horizontal split panel layout.
|
|
127
|
+
"""
|
|
130
128
|
|
|
131
129
|
orientation = param.Selector(default="horizontal", objects=["horizontal"], readonly=True)
|
|
132
130
|
|
|
133
131
|
|
|
134
132
|
class VSplit(Split):
|
|
133
|
+
"""
|
|
134
|
+
VSplit is a component for creating a responsive vertical split panel layout.
|
|
135
|
+
"""
|
|
135
136
|
|
|
136
137
|
orientation = param.Selector(default="vertical", objects=["vertical"], readonly=True)
|
|
137
138
|
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
class MultiSplit(SplitBase):
|
|
141
|
+
"""
|
|
142
|
+
MultiSplit is a component for creating a responsive multi-split panel layout.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
min_size = Size(default=100, length=None, doc="""
|
|
146
|
+
The minimum sizes of the panels (in pixels) either as a single value or a tuple.""")
|
|
147
|
+
|
|
148
|
+
_esm = Path(__file__).parent / "models" / "multi_split.js"
|
|
149
|
+
|
|
150
|
+
def __init__(self, *objects, **params):
|
|
151
|
+
if objects:
|
|
152
|
+
params["objects"] = list(objects)
|
|
153
|
+
if "objects" in params:
|
|
154
|
+
objects = params["objects"]
|
|
155
|
+
self.param.sizes.length = len(objects)
|
|
156
|
+
super().__init__(**params)
|
|
157
|
+
|
|
158
|
+
@param.depends("objects", watch=True)
|
|
159
|
+
def _update_sizes(self):
|
|
160
|
+
self.param.sizes.length = len(self.objects)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ["HSplit", "MultiSplit", "Split", "VSplit"]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
height: 100%;
|
|
4
4
|
width: 100%;
|
|
5
|
-
overflow: clip;
|
|
5
|
+
overflow: clip;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.split.horizontal {
|
|
@@ -29,12 +29,26 @@
|
|
|
29
29
|
position: relative;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
.split > div:nth-child(
|
|
32
|
+
.split.single-split > div:nth-child(1) {
|
|
33
|
+
overflow: clip;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.split.single-split > div:nth-child(2) {
|
|
33
37
|
overflow: visible;
|
|
34
38
|
position: relative;
|
|
35
39
|
width: 100%;
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/* Ensure buttons stay visible even when panels are collapsed */
|
|
43
|
+
.split.single-split > div:first-child {
|
|
44
|
+
min-width: 0 !important; /* Override any minimum width */
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.split.single-split > div:nth-child(2) {
|
|
48
|
+
overflow: visible !important; /* Ensure buttons remain visible */
|
|
49
|
+
position: relative !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/* Content wrapper styles */
|
|
39
53
|
.content-wrapper {
|
|
40
54
|
width: 100%;
|
|
@@ -88,16 +102,6 @@
|
|
|
88
102
|
transform: translateX(-50%);
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
/* Ensure buttons stay visible even when panels are collapsed */
|
|
92
|
-
.split > div:first-child {
|
|
93
|
-
min-width: 0 !important; /* Override any minimum width */
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.split > div:nth-child(2) {
|
|
97
|
-
overflow: visible !important; /* Ensure buttons remain visible */
|
|
98
|
-
position: relative !important;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
105
|
/* Collapsed state */
|
|
102
106
|
.collapsed-content {
|
|
103
107
|
display: none;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var ye=Object.defineProperty;var ce=(t,i)=>{for(var a in i)ye(t,a,{get:i[a],enumerable:!0})};var I={};ce(I,{render:()=>Oe});var _=typeof window<"u"?window:null,ee=_===null,Z=ee?void 0:_.document,w="addEventListener",E="removeEventListener",T="getBoundingClientRect",H="_a",x="_b",D="_c",Y="horizontal",O=function(){return!1},_e=ee?"calc":["","-webkit-","-moz-","-o-"].filter(function(t){var i=Z.createElement("div");return i.style.cssText="width:"+t+"calc(9px)",!!i.style.length}).shift()+"calc",ue=function(t){return typeof t=="string"||t instanceof String},oe=function(t){if(ue(t)){var i=Z.querySelector(t);if(!i)throw new Error("Selector "+t+" did not match a DOM element");return i}return t},d=function(t,i,a){var c=t[i];return c!==void 0?c:a},J=function(t,i,a,c){if(i){if(c==="end")return 0;if(c==="center")return t/2}else if(a){if(c==="start")return 0;if(c==="center")return t/2}return t},be=function(t,i){var a=Z.createElement("div");return a.className="gutter gutter-"+i,a},we=function(t,i,a){var c={};return ue(i)?c[t]=i:c[t]=_e+"("+i+"% - "+a+"px)",c},Ee=function(t,i){var a;return a={},a[t]=i+"px",a},xe=function(t,i){if(i===void 0&&(i={}),ee)return{};var a=t,c,z,p,S,g,l;Array.from&&(a=Array.from(a));var y=oe(a[0]),b=y.parentNode,$=getComputedStyle?getComputedStyle(b):null,f=$?$.flexDirection:null,M=d(i,"sizes")||a.map(function(){return 100/a.length}),B=d(i,"minSize",100),C=Array.isArray(B)?B:a.map(function(){return B}),N=d(i,"maxSize",1/0),q=Array.isArray(N)?N:a.map(function(){return N}),o=d(i,"expandToMin",!1),m=d(i,"gutterSize",10),F=d(i,"gutterAlign","center"),P=d(i,"snapOffset",30),j=Array.isArray(P)?P:a.map(function(){return P}),L=d(i,"dragInterval",1),U=d(i,"direction",Y),Q=d(i,"cursor",U===Y?"col-resize":"row-resize"),fe=d(i,"gutter",be),ne=d(i,"elementStyle",we),ve=d(i,"gutterStyle",Ee);U===Y?(c="width",z="clientX",p="left",S="right",g="clientWidth"):U==="vertical"&&(c="height",z="clientY",p="top",S="bottom",g="clientHeight");function R(r,e,n,s){var v=ne(c,e,n,s);Object.keys(v).forEach(function(u){r.style[u]=v[u]})}function pe(r,e,n){var s=ve(c,e,n);Object.keys(s).forEach(function(v){r.style[v]=s[v]})}function V(){return l.map(function(r){return r.size})}function re(r){return"touches"in r?r.touches[0][z]:r[z]}function se(r){var e=l[this.a],n=l[this.b],s=e.size+n.size;e.size=r/this.size*s,n.size=s-r/this.size*s,R(e.element,e.size,this[x],e.i),R(n.element,n.size,this[D],n.i)}function de(r){var e,n=l[this.a],s=l[this.b];this.dragging&&(e=re(r)-this.start+(this[x]-this.dragOffset),L>1&&(e=Math.round(e/L)*L),e<=n.minSize+n.snapOffset+this[x]?e=n.minSize+this[x]:e>=this.size-(s.minSize+s.snapOffset+this[D])&&(e=this.size-(s.minSize+this[D])),e>=n.maxSize-n.snapOffset+this[x]?e=n.maxSize+this[x]:e<=this.size-(s.maxSize-s.snapOffset+this[D])&&(e=this.size-(s.maxSize+this[D])),se.call(this,e),d(i,"onDrag",O)(V()))}function ie(){var r=l[this.a].element,e=l[this.b].element,n=r[T](),s=e[T]();this.size=n[c]+s[c]+this[x]+this[D],this.start=n[p],this.end=n[S]}function ge(r){if(!getComputedStyle)return null;var e=getComputedStyle(r);if(!e)return null;var n=r[g];return n===0?null:(U===Y?n-=parseFloat(e.paddingLeft)+parseFloat(e.paddingRight):n-=parseFloat(e.paddingTop)+parseFloat(e.paddingBottom),n)}function ae(r){var e=ge(b);if(e===null||C.reduce(function(u,h){return u+h},0)>e)return r;var n=0,s=[],v=r.map(function(u,h){var A=e*u/100,W=J(m,h===0,h===r.length-1,F),X=C[h]+W;return A<X?(n+=X-A,s.push(0),X):(s.push(A-X),A)});return n===0?r:v.map(function(u,h){var A=u;if(n>0&&s[h]-n>0){var W=Math.min(n,s[h]-n);n-=W,A=u-W}return A/e*100})}function ze(){var r=this,e=l[r.a].element,n=l[r.b].element;r.dragging&&d(i,"onDragEnd",O)(V()),r.dragging=!1,_[E]("mouseup",r.stop),_[E]("touchend",r.stop),_[E]("touchcancel",r.stop),_[E]("mousemove",r.move),_[E]("touchmove",r.move),r.stop=null,r.move=null,e[E]("selectstart",O),e[E]("dragstart",O),n[E]("selectstart",O),n[E]("dragstart",O),e.style.userSelect="",e.style.webkitUserSelect="",e.style.MozUserSelect="",e.style.pointerEvents="",n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.gutter.style.cursor="",r.parent.style.cursor="",Z.body.style.cursor=""}function me(r){if(!("button"in r&&r.button!==0)){var e=this,n=l[e.a].element,s=l[e.b].element;e.dragging||d(i,"onDragStart",O)(V()),r.preventDefault(),e.dragging=!0,e.move=de.bind(e),e.stop=ze.bind(e),_[w]("mouseup",e.stop),_[w]("touchend",e.stop),_[w]("touchcancel",e.stop),_[w]("mousemove",e.move),_[w]("touchmove",e.move),n[w]("selectstart",O),n[w]("dragstart",O),s[w]("selectstart",O),s[w]("dragstart",O),n.style.userSelect="none",n.style.webkitUserSelect="none",n.style.MozUserSelect="none",n.style.pointerEvents="none",s.style.userSelect="none",s.style.webkitUserSelect="none",s.style.MozUserSelect="none",s.style.pointerEvents="none",e.gutter.style.cursor=Q,e.parent.style.cursor=Q,Z.body.style.cursor=Q,ie.call(e),e.dragOffset=re(r)-e.end}}M=ae(M);var k=[];l=a.map(function(r,e){var n={element:oe(r),size:M[e],minSize:C[e],maxSize:q[e],snapOffset:j[e],i:e},s;if(e>0&&(s={a:e-1,b:e,dragging:!1,direction:U,parent:b},s[x]=J(m,e-1===0,!1,F),s[D]=J(m,!1,e===a.length-1,F),f==="row-reverse"||f==="column-reverse")){var v=s.a;s.a=s.b,s.b=v}if(e>0){var u=fe(e,U,n.element);pe(u,m,e),s[H]=me.bind(s),u[w]("mousedown",s[H]),u[w]("touchstart",s[H]),b.insertBefore(u,n.element),s.gutter=u}return R(n.element,n.size,J(m,e===0,e===a.length-1,F),e),e>0&&k.push(s),n});function le(r){var e=r.i===k.length,n=e?k[r.i-1]:k[r.i];ie.call(n);var s=e?n.size-r.minSize-n[D]:r.minSize+n[x];se.call(n,s)}l.forEach(function(r){var e=r.element[T]()[c];e<r.minSize&&(o?le(r):r.minSize=e)});function he(r){var e=ae(r);e.forEach(function(n,s){if(s>0){var v=k[s-1],u=l[v.a],h=l[v.b];u.size=e[s-1],h.size=n,R(u.element,u.size,v[x],u.i),R(h.element,h.size,v[D],h.i)}})}function Se(r,e){k.forEach(function(n){if(e!==!0?n.parent.removeChild(n.gutter):(n.gutter[E]("mousedown",n[H]),n.gutter[E]("touchstart",n[H])),r!==!0){var s=ne(c,n.a.size,n[x]);Object.keys(s).forEach(function(v){l[n.a].element.style[v]="",l[n.b].element.style[v]=""})}})}return{setSizes:he,getSizes:V,collapse:function(e){le(l[e])},destroy:Se,parent:b,pairs:k}},K=xe;var G=5;function Oe({model:t,el:i}){let a=document.createElement("div");a.className=`split single-split ${t.orientation}`,a.classList.add("loading");let c=document.createElement("div");c.className="split-panel";let z=document.createElement("div");z.className="split-panel",a.append(c,z);let p=document.createElement("div"),S=document.createElement("div");if(p.className=t.collapsed===0?"collapsed-content":"content-wrapper",S.className=t.collapsed===1?"collapsed-content":"content-wrapper",t.objects!=null&&t.objects.length==2){let[o,m]=t.get_child("objects");p.append(o),S.append(m)}c.append(p),z.append(S);let g,l,y=0,b=0;function $(){y=b=0}t.show_buttons&&(g=document.createElement("div"),l=document.createElement("div"),t.orientation==="horizontal"?(g.className="toggle-button-left",l.className="toggle-button-right"):(g.className="toggle-button-up",l.className="toggle-button-down"),z.append(g,l),g.addEventListener("click",()=>{y++,b=0;let o;y===1&&t.sizes[1]<t.expanded_sizes[1]?(o=t.expanded_sizes,f=null):(f=0,o=[0,100],y=0),t.collapsed=f,N(o,!0)}),l.addEventListener("click",()=>{b++,y=0;let o;b===1&&t.sizes[0]<t.expanded_sizes[0]?(o=t.expanded_sizes,f=null):(f=1,o=[100,0],b=0),t.collapsed=f,N(o,!0)})),i.append(a);let f=t.collapsed,M=t.sizes,B=f?[100,0]:t.sizes,C=K([c,z],{sizes:B,minSize:t.min_size,maxSize:t.max_size||+"Infinity",dragInterval:t.step_size,snapOffset:t.snap_size,gutterSize:8,direction:t.orientation,onDrag:o=>{let m=o[0]<=G?0:o[1]<=G?1:null;f!==m&&(f=m,N(o))},onDragEnd:o=>{f=o[0]<=G?0:o[1]<=G?1:null,t.collapsed=f,N(o,!0),$()}});function N(o=null,m=!1){let F=o?o[0]<=G:!1,P=o?o[1]<=G:!1,[j,L]=o;P?(S.className="collapsed-content",[j,L]=[100,0]):S.className="content-wrapper",F?(p.className="collapsed-content",[j,L]=[0,100]):p.className="content-wrapper",m&&(C.setSizes([j,L]),o=[j,L],t.sizes=[j,L],window.dispatchEvent(new Event("resize")))}t.on("sizes",()=>{M!==t.sizes&&(M=t.sizes,N(M,!0))}),t.on("collapsed",()=>{if(f===t.collapsed)return;f=t.collapsed;let o=f===0?[0,100]:f===1?[100,0]:t.expanded_sizes;N(o,!0)});let q=!1;t.on("after_layout",()=>{q||(q=!0,t.show_buttons&&(g.classList.add("animated"),l.classList.add("animated"),setTimeout(()=>{g.classList.remove("animated"),l.classList.remove("animated")},1500)),window.dispatchEvent(new Event("resize")),a.classList.remove("loading"))}),t.on("remove",()=>C.destroy())}var te={};ce(te,{render:()=>Ne});function Ne({model:t,el:i}){let a=document.createElement("div");a.className=`split multi-split ${t.orientation}`,a.classList.add("loading");let c=t.objects?t.get_child("objects"):[],z=[];for(let l=0;l<c.length;l++){let y=document.createElement("div");y.className="split-panel",a.append(y),z.push(y),y.append(c[l])}i.append(a);let p=t.sizes,S=K(z,{sizes:p,minSize:t.min_size||0,maxSize:t.max_size||+"Infinity",dragInterval:t.step_size||1,snapOffset:t.snap_size||30,gutterSize:8,direction:t.orientation,onDragEnd:l=>{p=l,this.model.sizes=p}});t.on("sizes",()=>{p!==t.sizes&&(p=t.sizes,S.setSizes(p))});let g=!1;t.on("after_layout",()=>{g||(g=!0,a.classList.remove("loading"))}),t.on("remove",()=>S.destroy())}var ke={HSplit:I,MultiSplit:te,Split:I,VSplit:I};export{ke as default};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Split from "https://esm.sh/split.js@1.6.5"
|
|
2
|
+
|
|
3
|
+
export function render({ model, el }) {
|
|
4
|
+
const split_div = document.createElement("div")
|
|
5
|
+
split_div.className = `split multi-split ${model.orientation}`
|
|
6
|
+
split_div.classList.add("loading")
|
|
7
|
+
|
|
8
|
+
const objects = model.objects ? model.get_child("objects") : []
|
|
9
|
+
const split_items = []
|
|
10
|
+
for (let i = 0; i < objects.length; i++) {
|
|
11
|
+
const split_item = document.createElement("div")
|
|
12
|
+
split_item.className = "split-panel"
|
|
13
|
+
split_div.append(split_item)
|
|
14
|
+
split_items.push(split_item)
|
|
15
|
+
split_item.append(objects[i])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
el.append(split_div)
|
|
19
|
+
|
|
20
|
+
let sizes = model.sizes
|
|
21
|
+
const split = Split(split_items, {
|
|
22
|
+
sizes: sizes,
|
|
23
|
+
minSize: model.min_size || 0,
|
|
24
|
+
maxSize: model.max_size || Number("Infinity"),
|
|
25
|
+
dragInterval: model.step_size || 1,
|
|
26
|
+
snapOffset: model.snap_size || 30,
|
|
27
|
+
gutterSize: 8,
|
|
28
|
+
direction: model.orientation,
|
|
29
|
+
onDragEnd: (new_sizes) => {
|
|
30
|
+
sizes = new_sizes
|
|
31
|
+
this.model.sizes = sizes
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
model.on("sizes", () => {
|
|
36
|
+
if (sizes === model.sizes) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
sizes = model.sizes
|
|
40
|
+
split.setSizes(sizes)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
let initialized = false
|
|
44
|
+
model.on("after_layout", () => {
|
|
45
|
+
if (!initialized) {
|
|
46
|
+
initialized = true
|
|
47
|
+
split_div.classList.remove("loading")
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
model.on("remove", () => split.destroy())
|
|
52
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import Split from "https://esm.sh/split.js@1.6.5"
|
|
2
|
+
|
|
3
|
+
const COLLAPSED_SIZE = 5
|
|
4
|
+
|
|
5
|
+
export function render({ model, el }) {
|
|
6
|
+
const split_div = document.createElement("div")
|
|
7
|
+
split_div.className = `split single-split ${model.orientation}`
|
|
8
|
+
split_div.classList.add("loading")
|
|
9
|
+
|
|
10
|
+
const split0 = document.createElement("div")
|
|
11
|
+
split0.className = "split-panel"
|
|
12
|
+
const split1 = document.createElement("div")
|
|
13
|
+
split1.className = "split-panel"
|
|
14
|
+
split_div.append(split0, split1)
|
|
15
|
+
|
|
16
|
+
const left_content_wrapper = document.createElement("div")
|
|
17
|
+
const right_content_wrapper = document.createElement("div")
|
|
18
|
+
left_content_wrapper.className = model.collapsed === 0 ? "collapsed-content" : "content-wrapper"
|
|
19
|
+
right_content_wrapper.className = model.collapsed === 1 ? "collapsed-content" : "content-wrapper"
|
|
20
|
+
|
|
21
|
+
if (model.objects != null && model.objects.length == 2) {
|
|
22
|
+
const [left, right] = model.get_child("objects")
|
|
23
|
+
left_content_wrapper.append(left)
|
|
24
|
+
right_content_wrapper.append(right)
|
|
25
|
+
}
|
|
26
|
+
split0.append(left_content_wrapper)
|
|
27
|
+
split1.append(right_content_wrapper)
|
|
28
|
+
|
|
29
|
+
let left_arrow_button, right_arrow_button
|
|
30
|
+
let left_click_count = 0
|
|
31
|
+
let right_click_count = 0
|
|
32
|
+
function reset_click_counts() {
|
|
33
|
+
left_click_count = right_click_count = 0
|
|
34
|
+
}
|
|
35
|
+
if (model.show_buttons) {
|
|
36
|
+
left_arrow_button = document.createElement("div")
|
|
37
|
+
right_arrow_button = document.createElement("div")
|
|
38
|
+
if (model.orientation === "horizontal") {
|
|
39
|
+
left_arrow_button.className = "toggle-button-left"
|
|
40
|
+
right_arrow_button.className = "toggle-button-right"
|
|
41
|
+
} else {
|
|
42
|
+
left_arrow_button.className = "toggle-button-up"
|
|
43
|
+
right_arrow_button.className = "toggle-button-down"
|
|
44
|
+
}
|
|
45
|
+
split1.append(left_arrow_button, right_arrow_button)
|
|
46
|
+
|
|
47
|
+
left_arrow_button.addEventListener("click", () => {
|
|
48
|
+
left_click_count++
|
|
49
|
+
right_click_count = 0
|
|
50
|
+
|
|
51
|
+
let new_sizes
|
|
52
|
+
if (left_click_count === 1 && model.sizes[1] < model.expanded_sizes[1]) {
|
|
53
|
+
new_sizes = model.expanded_sizes
|
|
54
|
+
is_collapsed = null
|
|
55
|
+
} else {
|
|
56
|
+
is_collapsed = 0
|
|
57
|
+
new_sizes = [0, 100]
|
|
58
|
+
left_click_count = 0
|
|
59
|
+
}
|
|
60
|
+
model.collapsed = is_collapsed
|
|
61
|
+
sync_ui(new_sizes, true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
right_arrow_button.addEventListener("click", () => {
|
|
65
|
+
right_click_count++
|
|
66
|
+
left_click_count = 0
|
|
67
|
+
|
|
68
|
+
let new_sizes
|
|
69
|
+
if (right_click_count === 1 && model.sizes[0] < model.expanded_sizes[0]) {
|
|
70
|
+
new_sizes = model.expanded_sizes
|
|
71
|
+
is_collapsed = null
|
|
72
|
+
} else {
|
|
73
|
+
is_collapsed = 1
|
|
74
|
+
new_sizes = [100, 0]
|
|
75
|
+
right_click_count = 0
|
|
76
|
+
}
|
|
77
|
+
model.collapsed = is_collapsed
|
|
78
|
+
sync_ui(new_sizes, true)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
el.append(split_div)
|
|
83
|
+
|
|
84
|
+
let is_collapsed = model.collapsed
|
|
85
|
+
let sizes = model.sizes
|
|
86
|
+
const init_sizes = is_collapsed ? [100, 0] : model.sizes
|
|
87
|
+
const split_instance = Split([split0, split1], {
|
|
88
|
+
sizes: init_sizes,
|
|
89
|
+
minSize: model.min_size,
|
|
90
|
+
maxSize: model.max_size || Number("Infinity"),
|
|
91
|
+
dragInterval: model.step_size,
|
|
92
|
+
snapOffset: model.snap_size,
|
|
93
|
+
gutterSize: 8,
|
|
94
|
+
direction: model.orientation,
|
|
95
|
+
onDrag: (sizes) => {
|
|
96
|
+
const new_collapsed_state = sizes[0] <= COLLAPSED_SIZE ? 0 : (sizes[1] <= COLLAPSED_SIZE ? 1 : null)
|
|
97
|
+
if (is_collapsed !== new_collapsed_state) {
|
|
98
|
+
is_collapsed = new_collapsed_state
|
|
99
|
+
sync_ui(sizes)
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onDragEnd: (sizes) => {
|
|
103
|
+
const new_collapsed_state = sizes[0] <= COLLAPSED_SIZE ? 0 : (sizes[1] <= COLLAPSED_SIZE ? 1 : null)
|
|
104
|
+
is_collapsed = new_collapsed_state
|
|
105
|
+
model.collapsed = is_collapsed
|
|
106
|
+
sync_ui(sizes, true)
|
|
107
|
+
reset_click_counts()
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
function sync_ui(sizes = null, resize = false) {
|
|
112
|
+
const left_panel_hidden = sizes ? sizes[0] <= COLLAPSED_SIZE : false
|
|
113
|
+
const right_panel_hidden = sizes ? sizes[1] <= COLLAPSED_SIZE : false
|
|
114
|
+
|
|
115
|
+
let [ls, rs] = sizes
|
|
116
|
+
if (right_panel_hidden) {
|
|
117
|
+
right_content_wrapper.className = "collapsed-content";
|
|
118
|
+
[ls, rs] = [100, 0]
|
|
119
|
+
} else {
|
|
120
|
+
right_content_wrapper.className = "content-wrapper"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (left_panel_hidden) {
|
|
124
|
+
left_content_wrapper.className = "collapsed-content";
|
|
125
|
+
[ls, rs] = [0, 100]
|
|
126
|
+
} else {
|
|
127
|
+
left_content_wrapper.className = "content-wrapper"
|
|
128
|
+
}
|
|
129
|
+
if (resize) {
|
|
130
|
+
split_instance.setSizes([ls, rs])
|
|
131
|
+
sizes = [ls, rs]
|
|
132
|
+
model.sizes = [ls, rs]
|
|
133
|
+
window.dispatchEvent(new Event('resize'))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
model.on("sizes", () => {
|
|
138
|
+
if (sizes === model.sizes) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
sizes = model.sizes
|
|
142
|
+
sync_ui(sizes, true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
model.on("collapsed", () => {
|
|
146
|
+
if (is_collapsed === model.collapsed) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
is_collapsed = model.collapsed
|
|
150
|
+
const new_sizes = is_collapsed === 0 ? [0, 100] : (is_collapsed === 1 ? [100, 0] : model.expanded_sizes)
|
|
151
|
+
sync_ui(new_sizes, true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
let initialized = false
|
|
155
|
+
model.on("after_layout", () => {
|
|
156
|
+
if (initialized) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
initialized = true
|
|
160
|
+
if (model.show_buttons) {
|
|
161
|
+
// Add animation on first load only
|
|
162
|
+
left_arrow_button.classList.add("animated")
|
|
163
|
+
right_arrow_button.classList.add("animated")
|
|
164
|
+
|
|
165
|
+
// Remove animation after it completes
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
left_arrow_button.classList.remove("animated")
|
|
168
|
+
right_arrow_button.classList.remove("animated")
|
|
169
|
+
}, 1500)
|
|
170
|
+
}
|
|
171
|
+
window.dispatchEvent(new Event('resize'))
|
|
172
|
+
split_div.classList.remove("loading")
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
model.on("remove", () => split_instance.destroy())
|
|
176
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: panel-splitjs
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Provides split.js components for Panel.
|
|
5
5
|
Project-URL: Homepage, https://github.com/panel-extensions/panel-splitjs
|
|
6
6
|
Project-URL: Source, https://github.com/panel-extensions/panel-splitjs
|
|
@@ -48,13 +48,13 @@ A responsive, draggable split panel component for [Panel](https://panel.holoviz.
|
|
|
48
48
|
|
|
49
49
|
## Features
|
|
50
50
|
|
|
51
|
-
- **Draggable
|
|
52
|
-
- **Collapsible panels** -
|
|
51
|
+
- **Draggable dividers** - Resize panels by dragging the divider between them
|
|
52
|
+
- **Collapsible panels** - Collapse individual panels with toggle buttons
|
|
53
53
|
- **Flexible orientation** - Support for both horizontal and vertical splits
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
54
|
+
- **Size constraints** - Enforce minimum and maximum panel sizes
|
|
55
|
+
- **Snap behavior** - Smart snapping to minimum sizes for better UX
|
|
56
56
|
- **Customizable sizes** - Control initial and expanded panel sizes
|
|
57
|
-
- **
|
|
57
|
+
- **Multi-panel support** - Create layouts with 2+ panels using `MultiSplit`
|
|
58
58
|
|
|
59
59
|
## Installation
|
|
60
60
|
|
|
@@ -82,7 +82,8 @@ pn.extension()
|
|
|
82
82
|
split = Split(
|
|
83
83
|
pn.pane.Markdown("## Left Panel\nContent here"),
|
|
84
84
|
pn.pane.Markdown("## Right Panel\nMore content"),
|
|
85
|
-
sizes=(50, 50), # Equal sizing
|
|
85
|
+
sizes=(50, 50), # Equal sizing initially
|
|
86
|
+
min_size=100, # Minimum 100px for each panel
|
|
86
87
|
show_buttons=True
|
|
87
88
|
)
|
|
88
89
|
|
|
@@ -114,6 +115,7 @@ split = HSplit(
|
|
|
114
115
|
left_panel,
|
|
115
116
|
right_panel,
|
|
116
117
|
sizes=(70, 30), # 70% left, 30% right
|
|
118
|
+
min_size=200, # Minimum 200px for each panel
|
|
117
119
|
show_buttons=True
|
|
118
120
|
)
|
|
119
121
|
|
|
@@ -135,7 +137,7 @@ split = VSplit(
|
|
|
135
137
|
top_panel,
|
|
136
138
|
bottom_panel,
|
|
137
139
|
sizes=(60, 40),
|
|
138
|
-
|
|
140
|
+
min_size=150
|
|
139
141
|
)
|
|
140
142
|
|
|
141
143
|
split.servable()
|
|
@@ -149,72 +151,90 @@ from panel_splitjs import Split
|
|
|
149
151
|
|
|
150
152
|
pn.extension()
|
|
151
153
|
|
|
152
|
-
# Start with
|
|
154
|
+
# Start with right panel collapsed
|
|
153
155
|
split = Split(
|
|
154
156
|
pn.pane.Markdown("## Main Content"),
|
|
155
157
|
pn.pane.Markdown("## Collapsible Sidebar"),
|
|
156
|
-
collapsed=
|
|
158
|
+
collapsed=1, # 0 for first panel, 1 for second panel, None for not collapsed
|
|
157
159
|
expanded_sizes=(65, 35), # When expanded, 65% main, 35% sidebar
|
|
158
160
|
show_buttons=True,
|
|
159
|
-
|
|
161
|
+
min_size=(200, 200) # Minimum 200px for each panel
|
|
160
162
|
)
|
|
161
163
|
|
|
162
164
|
# Toggle collapse programmatically
|
|
163
165
|
button = pn.widgets.Button(name="Toggle Sidebar")
|
|
164
|
-
|
|
166
|
+
def toggle(event):
|
|
167
|
+
split.collapsed = None if split.collapsed == 1 else 1
|
|
168
|
+
button.on_click(toggle)
|
|
165
169
|
|
|
166
170
|
pn.Column(button, split).servable()
|
|
167
171
|
```
|
|
168
172
|
|
|
169
|
-
###
|
|
173
|
+
### Multi-Panel Split
|
|
170
174
|
|
|
171
175
|
```python
|
|
172
176
|
import panel as pn
|
|
173
|
-
from panel_splitjs import
|
|
177
|
+
from panel_splitjs import MultiSplit
|
|
174
178
|
|
|
175
179
|
pn.extension()
|
|
176
180
|
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
pn.pane.Markdown("##
|
|
180
|
-
pn.pane.Markdown("##
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
# Create a layout with three panels
|
|
182
|
+
multi = MultiSplit(
|
|
183
|
+
pn.pane.Markdown("## Panel 1"),
|
|
184
|
+
pn.pane.Markdown("## Panel 2"),
|
|
185
|
+
pn.pane.Markdown("## Panel 3"),
|
|
186
|
+
sizes=(30, 40, 30), # Three panels with custom sizing
|
|
187
|
+
min_size=100, # Minimum 100px for each panel
|
|
188
|
+
orientation="horizontal"
|
|
185
189
|
)
|
|
186
190
|
|
|
187
|
-
|
|
191
|
+
multi.servable()
|
|
188
192
|
```
|
|
189
193
|
|
|
190
194
|
## API Reference
|
|
191
195
|
|
|
192
196
|
### Split
|
|
193
197
|
|
|
194
|
-
The main split panel component with
|
|
198
|
+
The main split panel component for creating two-panel layouts with collapsible functionality.
|
|
195
199
|
|
|
196
200
|
**Parameters:**
|
|
197
201
|
|
|
198
202
|
- `objects` (list): Two Panel components to display in the split panels
|
|
199
|
-
- `collapsed` (
|
|
200
|
-
- `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when
|
|
201
|
-
- `
|
|
202
|
-
- `
|
|
203
|
-
- `orientation` (str, default="horizontal"): Either "horizontal" or "vertical"
|
|
204
|
-
- `show_buttons` (bool, default=
|
|
205
|
-
- `sizes` (tuple, default=(
|
|
203
|
+
- `collapsed` (int | None, default=None): Which panel is collapsed - `0` for first panel, `1` for second panel, `None` for not collapsed
|
|
204
|
+
- `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when both panels are expanded
|
|
205
|
+
- `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to both panels, tuple for individual sizes
|
|
206
|
+
- `min_size` (int | tuple, default=0): Minimum sizes in pixels - single value applies to both panels, tuple for individual sizes
|
|
207
|
+
- `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
|
|
208
|
+
- `show_buttons` (bool, default=True): Show collapse/expand toggle buttons on the divider
|
|
209
|
+
- `sizes` (tuple, default=(50, 50)): Initial percentage sizes of the panels
|
|
210
|
+
- `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
|
|
211
|
+
- `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
|
|
206
212
|
|
|
207
213
|
### HSplit
|
|
208
214
|
|
|
209
215
|
Horizontal split panel (convenience class).
|
|
210
216
|
|
|
211
|
-
Same parameters as `Split` but `orientation` is locked to "horizontal"
|
|
217
|
+
Same parameters as `Split` but `orientation` is locked to `"horizontal"`.
|
|
212
218
|
|
|
213
219
|
### VSplit
|
|
214
220
|
|
|
215
221
|
Vertical split panel (convenience class).
|
|
216
222
|
|
|
217
|
-
Same parameters as `Split` but `orientation` is locked to "vertical"
|
|
223
|
+
Same parameters as `Split` but `orientation` is locked to `"vertical"`.
|
|
224
|
+
|
|
225
|
+
### MultiSplit
|
|
226
|
+
|
|
227
|
+
Multi-panel split component for creating layouts with three or more panels.
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
|
|
231
|
+
- `objects` (list): List of Panel components to display (3 or more)
|
|
232
|
+
- `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to all panels, tuple for individual sizes
|
|
233
|
+
- `min_size` (int | tuple, default=100): Minimum sizes in pixels - single value applies to all panels, tuple for individual sizes
|
|
234
|
+
- `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
|
|
235
|
+
- `sizes` (tuple, default=None): Initial percentage sizes of the panels (length must match number of objects)
|
|
236
|
+
- `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
|
|
237
|
+
- `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
|
|
218
238
|
|
|
219
239
|
## Common Use Cases
|
|
220
240
|
|
|
@@ -232,10 +252,10 @@ output = pn.Column("# Output Area")
|
|
|
232
252
|
split = Split(
|
|
233
253
|
chat,
|
|
234
254
|
output,
|
|
235
|
-
collapsed=
|
|
255
|
+
collapsed=None, # Both panels visible
|
|
236
256
|
expanded_sizes=(50, 50),
|
|
237
257
|
show_buttons=True,
|
|
238
|
-
|
|
258
|
+
min_size=(300, 300) # Minimum 300px for each panel
|
|
239
259
|
)
|
|
240
260
|
|
|
241
261
|
split.servable()
|
|
@@ -260,16 +280,16 @@ visualization = pn.pane.Markdown("## Main Visualization Area")
|
|
|
260
280
|
split = Split(
|
|
261
281
|
controls,
|
|
262
282
|
visualization,
|
|
263
|
-
collapsed=
|
|
283
|
+
collapsed=0, # Start with controls collapsed
|
|
264
284
|
expanded_sizes=(25, 75),
|
|
265
285
|
show_buttons=True,
|
|
266
|
-
|
|
286
|
+
min_size=(250, 400) # Minimum sizes for each panel
|
|
267
287
|
)
|
|
268
288
|
|
|
269
289
|
split.servable()
|
|
270
290
|
```
|
|
271
291
|
|
|
272
|
-
### Responsive Layout
|
|
292
|
+
### Responsive Layout with Size Constraints
|
|
273
293
|
|
|
274
294
|
```python
|
|
275
295
|
import panel as pn
|
|
@@ -277,18 +297,73 @@ from panel_splitjs import Split
|
|
|
277
297
|
|
|
278
298
|
pn.extension()
|
|
279
299
|
|
|
280
|
-
# Automatically adjust to available space
|
|
281
300
|
split = Split(
|
|
282
301
|
pn.pane.Markdown("## Panel 1\nResponsive content"),
|
|
283
302
|
pn.pane.Markdown("## Panel 2\nMore responsive content"),
|
|
284
303
|
sizes=(50, 50),
|
|
285
|
-
|
|
304
|
+
min_size=200, # Minimum 200px per panel
|
|
305
|
+
max_size=800, # Maximum 800px per panel
|
|
306
|
+
snap_size=50, # Snap to min size when within 50px
|
|
286
307
|
show_buttons=True
|
|
287
308
|
)
|
|
288
309
|
|
|
289
310
|
split.servable()
|
|
290
311
|
```
|
|
291
312
|
|
|
313
|
+
### Complex Multi-Panel Layout
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
import panel as pn
|
|
317
|
+
from panel_splitjs import MultiSplit
|
|
318
|
+
|
|
319
|
+
pn.extension()
|
|
320
|
+
|
|
321
|
+
# Create a four-panel layout
|
|
322
|
+
sidebar = pn.Column("## Sidebar", pn.widgets.Select(options=["A", "B", "C"]))
|
|
323
|
+
main = pn.pane.Markdown("## Main Content Area")
|
|
324
|
+
detail = pn.pane.Markdown("## Detail Panel")
|
|
325
|
+
console = pn.pane.Markdown("## Console Output")
|
|
326
|
+
|
|
327
|
+
multi = MultiSplit(
|
|
328
|
+
sidebar,
|
|
329
|
+
main,
|
|
330
|
+
detail,
|
|
331
|
+
console,
|
|
332
|
+
sizes=(15, 40, 25, 20), # Custom sizing for each panel
|
|
333
|
+
min_size=(150, 300, 200, 150), # Individual minimums
|
|
334
|
+
orientation="horizontal"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
multi.servable()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Nested Splits
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
import panel as pn
|
|
344
|
+
from panel_splitjs import HSplit, VSplit
|
|
345
|
+
|
|
346
|
+
pn.extension()
|
|
347
|
+
|
|
348
|
+
# Create a nested layout: horizontal split with vertical split on right
|
|
349
|
+
left = pn.pane.Markdown("## Left Panel")
|
|
350
|
+
|
|
351
|
+
# Right side has a vertical split
|
|
352
|
+
top_right = pn.pane.Markdown("## Top Right")
|
|
353
|
+
bottom_right = pn.pane.Markdown("## Bottom Right")
|
|
354
|
+
right = VSplit(top_right, bottom_right, sizes=(60, 40))
|
|
355
|
+
|
|
356
|
+
# Main horizontal split
|
|
357
|
+
layout = HSplit(
|
|
358
|
+
left,
|
|
359
|
+
right,
|
|
360
|
+
sizes=(30, 70),
|
|
361
|
+
min_size=200
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
layout.servable()
|
|
365
|
+
```
|
|
366
|
+
|
|
292
367
|
## Development
|
|
293
368
|
|
|
294
369
|
This project is managed by [pixi](https://pixi.sh).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
panel_splitjs/__init__.py,sha256=BKEeHeKAv90kV3GybL6FH8RCwFZM4B6gffr7T8242Qk,150
|
|
2
|
+
panel_splitjs/__version.py,sha256=rpIYpR7RLjD5NBNasrT3f60C0qTL-nnHBifOofA_qyo,1778
|
|
3
|
+
panel_splitjs/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
|
|
4
|
+
panel_splitjs/base.py,sha256=x3ynR2CaKUQxhQOGvKyje7zaR8UDppvOygQG89stzTA,5994
|
|
5
|
+
panel_splitjs/dist/panel-splitjs.bundle.js,sha256=rsJvlKUnpPwOQ6q0Axqve8eLesHk1Y8GjMxbIkwjkag,9380
|
|
6
|
+
panel_splitjs/dist/css/arrow_down.svg,sha256=pPSAQIH-UkJf1HbWJhAapaT-rROqn6JVPQldOT3Al0I,214
|
|
7
|
+
panel_splitjs/dist/css/arrow_left.svg,sha256=XZ1Qf7CzKTij-Fa4Up51Gbu_WnBcScK7Qx-tOprvEzE,215
|
|
8
|
+
panel_splitjs/dist/css/arrow_right.svg,sha256=_-3m5dLhPwH9caIEk8XLp3Z8-xQHHvTBny6AcyFVRb4,214
|
|
9
|
+
panel_splitjs/dist/css/arrow_up.svg,sha256=FoVQYz0kh1SAf7EJAs2ZI-ZuuQjcPzR3uM4uViZ87Qs,215
|
|
10
|
+
panel_splitjs/dist/css/handle.svg,sha256=tEQAE3lNBVzPigcp9Z0SQZCW0bSzfECYIvpl19NOd0E,899
|
|
11
|
+
panel_splitjs/dist/css/handle_vertical.svg,sha256=2QZdZzNiLaJp93Ot4tJBhfGjF07EiMfN-Hq3Uf5Z_BI,801
|
|
12
|
+
panel_splitjs/dist/css/splitjs.css,sha256=L80iIafpH9Ii5qy0duO-KNroG6VaP7AzQImZR3d7zgI,5281
|
|
13
|
+
panel_splitjs/models/multi_split.js,sha256=U9TQcCK5Ho0GElYaMCOzwEosnBv40vN7XOLzsGzlISk,1348
|
|
14
|
+
panel_splitjs/models/split.js,sha256=zJefB8ivxMi-j7oKgTc9wF5K3CIxSKommQPfYL0tpR8,5470
|
|
15
|
+
panel_splitjs-0.1.0.dist-info/METADATA,sha256=FMJQQJtzyj-5RTFnmLQlivB1weEzww7P3QcmSJ_ZnAg,10877
|
|
16
|
+
panel_splitjs-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
panel_splitjs-0.1.0.dist-info/RECORD,,
|
panel_splitjs/models/splitjs.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import Split from "https://esm.sh/split.js@1.6.5"
|
|
2
|
-
|
|
3
|
-
export function render({ model, view }) {
|
|
4
|
-
const splitDiv = document.createElement("div")
|
|
5
|
-
splitDiv.className = `split ${model.orientation}`
|
|
6
|
-
splitDiv.classList.add("loading")
|
|
7
|
-
|
|
8
|
-
const split0 = document.createElement("div")
|
|
9
|
-
const split1 = document.createElement("div")
|
|
10
|
-
splitDiv.append(split0, split1)
|
|
11
|
-
|
|
12
|
-
// Create content wrapper for right panel
|
|
13
|
-
const contentWrapper = document.createElement("div")
|
|
14
|
-
contentWrapper.classList.add("content-wrapper")
|
|
15
|
-
|
|
16
|
-
if (model.show_buttons) {
|
|
17
|
-
// Create toggle icons for both sides of the divider
|
|
18
|
-
const leftArrowButton = document.createElement("div") // < button
|
|
19
|
-
const rightArrowButton = document.createElement("div") // > button
|
|
20
|
-
|
|
21
|
-
// Set button icons based on orientation
|
|
22
|
-
if (model.orientation === "horizontal") {
|
|
23
|
-
leftArrowButton.className = "toggle-button-left"
|
|
24
|
-
rightArrowButton.className = "toggle-button-right"
|
|
25
|
-
} else {
|
|
26
|
-
leftArrowButton.className = "toggle-button-up"
|
|
27
|
-
rightArrowButton.className = "toggle-button-down"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Add both buttons to the right panel (split1) so they're positioned relative to the divider
|
|
31
|
-
split1.appendChild(leftArrowButton)
|
|
32
|
-
split1.appendChild(rightArrowButton)
|
|
33
|
-
|
|
34
|
-
// Left/Up arrow button event listener - two-step toggle
|
|
35
|
-
leftArrowButton.addEventListener("click", () => {
|
|
36
|
-
leftClickCount++
|
|
37
|
-
rightClickCount = 0 // Reset other button's count
|
|
38
|
-
|
|
39
|
-
let newSizes
|
|
40
|
-
|
|
41
|
-
if (leftClickCount === 1) {
|
|
42
|
-
// First tap: make sizes 50, 50
|
|
43
|
-
newSizes = [50, 50]
|
|
44
|
-
} else {
|
|
45
|
-
// Second tap (or more): make sizes 0, 100
|
|
46
|
-
newSizes = [0, 100]
|
|
47
|
-
leftClickCount = 0 // Reset after second tap
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
splitInstance.setSizes(newSizes)
|
|
51
|
-
|
|
52
|
-
// Update collapsed state based on new sizes
|
|
53
|
-
const newCollapsedState = newSizes[1] <= 5
|
|
54
|
-
model.send_msg({ collapsed: newCollapsedState })
|
|
55
|
-
model.collapsed = newCollapsedState
|
|
56
|
-
|
|
57
|
-
updateUIForCollapsedState(newCollapsedState, newSizes)
|
|
58
|
-
view.invalidate_layout()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Right/Down arrow button event listener - two-step toggle
|
|
62
|
-
rightArrowButton.addEventListener("click", () => {
|
|
63
|
-
rightClickCount++
|
|
64
|
-
leftClickCount = 0 // Reset other button's count
|
|
65
|
-
|
|
66
|
-
let newSizes
|
|
67
|
-
|
|
68
|
-
if (rightClickCount === 1) {
|
|
69
|
-
// First tap: make sizes 50, 50
|
|
70
|
-
newSizes = [50, 50]
|
|
71
|
-
} else {
|
|
72
|
-
// Second tap (or more): make sizes 100, 0
|
|
73
|
-
newSizes = [100, 0]
|
|
74
|
-
rightClickCount = 0 // Reset after second tap
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
splitInstance.setSizes(newSizes)
|
|
78
|
-
|
|
79
|
-
// Update collapsed state based on new sizes
|
|
80
|
-
const newCollapsedState = newSizes[1] <= 5
|
|
81
|
-
model.send_msg({ collapsed: newCollapsedState })
|
|
82
|
-
model.collapsed = newCollapsedState
|
|
83
|
-
|
|
84
|
-
updateUIForCollapsedState(newCollapsedState, newSizes)
|
|
85
|
-
view.invalidate_layout()
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Determine initial state - collapsed means right panel is hidden
|
|
90
|
-
const initSizes = model.collapsed ? [100, 0] : model.expanded_sizes
|
|
91
|
-
|
|
92
|
-
// Track click counts for toggle behavior
|
|
93
|
-
let leftClickCount = 0 // For < button
|
|
94
|
-
let rightClickCount = 0 // For > button
|
|
95
|
-
|
|
96
|
-
// Function to reset click counts when sizes change via dragging
|
|
97
|
-
function resetClickCounts() {
|
|
98
|
-
leftClickCount = 0
|
|
99
|
-
rightClickCount = 0
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Use minSize of 0 to allow full collapse via buttons
|
|
103
|
-
const splitInstance = Split([split0, split1], {
|
|
104
|
-
sizes: initSizes,
|
|
105
|
-
minSize: [0, 0], // Allow full collapse for both panels
|
|
106
|
-
gutterSize: 8, // Match the 8px width in CSS
|
|
107
|
-
direction: model.orientation,
|
|
108
|
-
onDragEnd: (sizes) => {
|
|
109
|
-
view.invalidate_layout()
|
|
110
|
-
|
|
111
|
-
// Determine the new collapsed state based on panel sizes
|
|
112
|
-
const rightPanelCollapsed = sizes[1] <= 5
|
|
113
|
-
const leftPanelCollapsed = sizes[0] <= 5
|
|
114
|
-
|
|
115
|
-
// The model's collapsed state represents whether the right panel is collapsed
|
|
116
|
-
const newCollapsedState = rightPanelCollapsed
|
|
117
|
-
|
|
118
|
-
if (model.collapsed !== newCollapsedState) {
|
|
119
|
-
// Send message to Python about collapsed state change
|
|
120
|
-
model.send_msg({ collapsed: newCollapsedState })
|
|
121
|
-
model.collapsed = newCollapsedState
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Update UI based on current sizes
|
|
125
|
-
updateUIForCollapsedState(newCollapsedState, sizes)
|
|
126
|
-
|
|
127
|
-
// Reset click counts when user drags the splitter
|
|
128
|
-
resetClickCounts()
|
|
129
|
-
},
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
// Function to update UI elements based on collapsed state
|
|
133
|
-
function updateUIForCollapsedState(isCollapsed, sizes = null) {
|
|
134
|
-
// Determine current panel state
|
|
135
|
-
const leftPanelHidden = sizes ? sizes[0] <= 5 : false
|
|
136
|
-
const rightPanelHidden = sizes ? sizes[1] <= 5 : false
|
|
137
|
-
|
|
138
|
-
// Update content visibility
|
|
139
|
-
if (rightPanelHidden) {
|
|
140
|
-
contentWrapper.className = "collapsed-content"
|
|
141
|
-
} else {
|
|
142
|
-
contentWrapper.className = "content-wrapper"
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (leftPanelHidden) {
|
|
146
|
-
leftContentWrapper.className = "collapsed-content"
|
|
147
|
-
} else {
|
|
148
|
-
leftContentWrapper.className = "content-wrapper"
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Listen for collapsed state changes from Python
|
|
153
|
-
model.on("msg:custom", (event) => {
|
|
154
|
-
if (event.type === "update_collapsed") {
|
|
155
|
-
const newCollapsedState = event.collapsed
|
|
156
|
-
|
|
157
|
-
model.collapsed = newCollapsedState
|
|
158
|
-
|
|
159
|
-
// Update split sizes based on new collapsed state
|
|
160
|
-
if (newCollapsedState) {
|
|
161
|
-
// Collapse right panel (show only left)
|
|
162
|
-
splitInstance.setSizes([100, 0])
|
|
163
|
-
} else {
|
|
164
|
-
// Expand to show both panels
|
|
165
|
-
splitInstance.setSizes(model.expanded_sizes)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
updateUIForCollapsedState(newCollapsedState)
|
|
169
|
-
view.invalidate_layout()
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
model.on("after_layout", () => {
|
|
174
|
-
setTimeout(() => {
|
|
175
|
-
splitDiv.classList.remove("loading")
|
|
176
|
-
|
|
177
|
-
// Only add animation on initial load
|
|
178
|
-
if (model.show_buttons && !window._toggleAnimationShown) {
|
|
179
|
-
// Add animation on first load only
|
|
180
|
-
leftArrowButton.classList.add("animated")
|
|
181
|
-
rightArrowButton.classList.add("animated")
|
|
182
|
-
|
|
183
|
-
// Remove animation after it completes and set flag
|
|
184
|
-
setTimeout(() => {
|
|
185
|
-
leftArrowButton.classList.remove("animated")
|
|
186
|
-
rightArrowButton.classList.remove("animated")
|
|
187
|
-
window._toggleAnimationShown = true
|
|
188
|
-
}, 1500)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
window.dispatchEvent(new Event("resize"))
|
|
192
|
-
}, 100)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// Create a centered content wrapper for the left panel
|
|
196
|
-
const leftContentWrapper = document.createElement("div")
|
|
197
|
-
leftContentWrapper.classList.add("left-content-wrapper")
|
|
198
|
-
|
|
199
|
-
// Set initial display based on collapsed state
|
|
200
|
-
if (model.collapsed) {
|
|
201
|
-
contentWrapper.className = "collapsed-content"
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Apply left-panel-content class to the left panel
|
|
205
|
-
leftContentWrapper.classList.add("left-panel-content")
|
|
206
|
-
|
|
207
|
-
// Append children to the appropriate containers
|
|
208
|
-
const [left, right] = model.get_child("objects")
|
|
209
|
-
leftContentWrapper.append(left)
|
|
210
|
-
split0.append(leftContentWrapper)
|
|
211
|
-
contentWrapper.append(right)
|
|
212
|
-
split1.append(contentWrapper)
|
|
213
|
-
|
|
214
|
-
return splitDiv
|
|
215
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
panel_splitjs/__init__.py,sha256=3o1ZsgXufccivyXhnMunBBNQZliCwQY4GW9XKQ9gRDM,124
|
|
2
|
-
panel_splitjs/__version.py,sha256=rpIYpR7RLjD5NBNasrT3f60C0qTL-nnHBifOofA_qyo,1778
|
|
3
|
-
panel_splitjs/_version.py,sha256=N6jqqryygxntTpQZELt2H0LAGZ4wKgVPTWGAhPzx98U,712
|
|
4
|
-
panel_splitjs/base.py,sha256=OcdprAQyY--hTgiMCUVOFw2RsuknlS46o3DUv89lO9E,5337
|
|
5
|
-
panel_splitjs/dist/panel-splitjs.bundle.js,sha256=tcWSuP8-WKzPVq1qsV_FnnFBq_eGB32AqnKu3cwIXpc,8466
|
|
6
|
-
panel_splitjs/dist/css/arrow_down.svg,sha256=pPSAQIH-UkJf1HbWJhAapaT-rROqn6JVPQldOT3Al0I,214
|
|
7
|
-
panel_splitjs/dist/css/arrow_left.svg,sha256=XZ1Qf7CzKTij-Fa4Up51Gbu_WnBcScK7Qx-tOprvEzE,215
|
|
8
|
-
panel_splitjs/dist/css/arrow_right.svg,sha256=_-3m5dLhPwH9caIEk8XLp3Z8-xQHHvTBny6AcyFVRb4,214
|
|
9
|
-
panel_splitjs/dist/css/arrow_up.svg,sha256=FoVQYz0kh1SAf7EJAs2ZI-ZuuQjcPzR3uM4uViZ87Qs,215
|
|
10
|
-
panel_splitjs/dist/css/handle.svg,sha256=tEQAE3lNBVzPigcp9Z0SQZCW0bSzfECYIvpl19NOd0E,899
|
|
11
|
-
panel_splitjs/dist/css/handle_vertical.svg,sha256=2QZdZzNiLaJp93Ot4tJBhfGjF07EiMfN-Hq3Uf5Z_BI,801
|
|
12
|
-
panel_splitjs/dist/css/splitjs.css,sha256=CIo6tHTT0_NpYjoJFxtCB60uKBUBEFYtvyphDsQpviM,5257
|
|
13
|
-
panel_splitjs/models/splitjs.js,sha256=kDkD-CTnPhRmsE1y6MCIMr31y90LS2r2J3EkFbDUZu0,6965
|
|
14
|
-
panel_splitjs-0.0.1a0.dist-info/METADATA,sha256=9TkauebpnY0rouEEtKJq0sb1-Bt9VtJfLrYGPaEBkr0,8178
|
|
15
|
-
panel_splitjs-0.0.1a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
panel_splitjs-0.0.1a0.dist-info/RECORD,,
|
|
File without changes
|