basefn 1.9.1 → 1.10.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 +2 -2
- package/src/Basefn.res +5 -0
- package/src/Basefn.res.mjs +4 -0
- package/src/components/Basefn__Resizable.css +156 -0
- package/src/components/Basefn__Resizable.res +289 -0
- package/src/components/Basefn__Resizable.res.mjs +313 -0
- package/src/components/Basefn__Spotlight.res +8 -0
- package/src/components/Basefn__Spotlight.res.mjs +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basefn",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/brnrdog/basefn.git"
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"@semantic-release/github": "^12.0.2",
|
|
81
81
|
"@semantic-release/npm": "^13.1.3",
|
|
82
82
|
"rescript": "^12.0.1",
|
|
83
|
-
"vite": "^
|
|
83
|
+
"vite": "^8.0.0",
|
|
84
84
|
"xote": "^4.12.0"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/Basefn.res
CHANGED
|
@@ -72,6 +72,8 @@ type gridJustifyContent = Basefn__Grid.justifyContent
|
|
|
72
72
|
type gridAlignContent = Basefn__Grid.alignContent
|
|
73
73
|
type gridItemColumnSpan = Basefn__Grid.Item.columnSpan
|
|
74
74
|
type gridItemRowSpan = Basefn__Grid.Item.rowSpan
|
|
75
|
+
type resizableDirection = Basefn__Resizable.direction
|
|
76
|
+
type resizablePanel = Basefn__Resizable.panel
|
|
75
77
|
type breakpoint = Basefn__Responsive.breakpoint
|
|
76
78
|
type currentBreakpoint = Basefn__Responsive.currentBreakpoint
|
|
77
79
|
type responsiveValue<'a> = Basefn__Responsive.responsiveValue<'a>
|
|
@@ -231,6 +233,9 @@ module ContextMenu = {
|
|
|
231
233
|
module Spotlight = {
|
|
232
234
|
include Basefn__Spotlight
|
|
233
235
|
}
|
|
236
|
+
module Resizable = {
|
|
237
|
+
include Basefn__Resizable
|
|
238
|
+
}
|
|
234
239
|
|
|
235
240
|
// Responsive Utilities
|
|
236
241
|
module Responsive = {
|
package/src/Basefn.res.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import * as Basefn__Timeline from "./components/Basefn__Timeline.res.mjs";
|
|
|
35
35
|
import * as Basefn__Accordion from "./components/Basefn__Accordion.res.mjs";
|
|
36
36
|
import * as Basefn__AppLayout from "./components/Basefn__AppLayout.res.mjs";
|
|
37
37
|
import * as Basefn__HoverCard from "./components/Basefn__HoverCard.res.mjs";
|
|
38
|
+
import * as Basefn__Resizable from "./components/Basefn__Resizable.res.mjs";
|
|
38
39
|
import * as Basefn__Separator from "./components/Basefn__Separator.res.mjs";
|
|
39
40
|
import * as Basefn__Spotlight from "./components/Basefn__Spotlight.res.mjs";
|
|
40
41
|
import * as Basefn__Breadcrumb from "./components/Basefn__Breadcrumb.res.mjs";
|
|
@@ -143,6 +144,8 @@ let ContextMenu = Basefn__ContextMenu;
|
|
|
143
144
|
|
|
144
145
|
let Spotlight = Basefn__Spotlight;
|
|
145
146
|
|
|
147
|
+
let Resizable = Basefn__Resizable;
|
|
148
|
+
|
|
146
149
|
let Responsive = Basefn__Responsive;
|
|
147
150
|
|
|
148
151
|
export {
|
|
@@ -192,6 +195,7 @@ export {
|
|
|
192
195
|
AlertDialog,
|
|
193
196
|
ContextMenu,
|
|
194
197
|
Spotlight,
|
|
198
|
+
Resizable,
|
|
195
199
|
Responsive,
|
|
196
200
|
}
|
|
197
201
|
/* Not a pure module */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
@import "../styles/variables.css";
|
|
2
|
+
|
|
3
|
+
/* Panel Group Container */
|
|
4
|
+
.basefn-resizable {
|
|
5
|
+
display: flex;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.basefn-resizable--horizontal {
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.basefn-resizable--vertical {
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Panel */
|
|
20
|
+
.basefn-resizable__panel {
|
|
21
|
+
overflow: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.basefn-resizable__panel--dragging {
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
user-select: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Handle */
|
|
30
|
+
.basefn-resizable__handle {
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
position: relative;
|
|
36
|
+
outline: none;
|
|
37
|
+
background: transparent;
|
|
38
|
+
border: 0;
|
|
39
|
+
padding: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.basefn-resizable__handle--horizontal {
|
|
43
|
+
width: 8px;
|
|
44
|
+
cursor: col-resize;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.basefn-resizable__handle--vertical {
|
|
48
|
+
height: 8px;
|
|
49
|
+
cursor: row-resize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Handle visible line */
|
|
53
|
+
.basefn-resizable__handle::after {
|
|
54
|
+
content: "";
|
|
55
|
+
position: absolute;
|
|
56
|
+
background-color: var(--basefn-border-primary);
|
|
57
|
+
transition: background-color var(--basefn-transition-fast);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.basefn-resizable__handle--horizontal::after {
|
|
61
|
+
width: 1px;
|
|
62
|
+
height: 100%;
|
|
63
|
+
left: 50%;
|
|
64
|
+
transform: translateX(-50%);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.basefn-resizable__handle--vertical::after {
|
|
68
|
+
height: 1px;
|
|
69
|
+
width: 100%;
|
|
70
|
+
top: 50%;
|
|
71
|
+
transform: translateY(-50%);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.basefn-resizable__handle:hover::after,
|
|
75
|
+
.basefn-resizable__handle:focus-visible::after {
|
|
76
|
+
background-color: var(--basefn-color-primary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Grip indicator */
|
|
80
|
+
.basefn-resizable__handle-grip {
|
|
81
|
+
position: relative;
|
|
82
|
+
z-index: 1;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
border-radius: var(--basefn-radius-sm);
|
|
87
|
+
background-color: var(--basefn-border-primary);
|
|
88
|
+
border: 1px solid var(--basefn-border-secondary);
|
|
89
|
+
transition:
|
|
90
|
+
background-color var(--basefn-transition-fast),
|
|
91
|
+
border-color var(--basefn-transition-fast);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.basefn-resizable__handle--horizontal .basefn-resizable__handle-grip {
|
|
95
|
+
width: 12px;
|
|
96
|
+
height: 24px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.basefn-resizable__handle--vertical .basefn-resizable__handle-grip {
|
|
100
|
+
width: 24px;
|
|
101
|
+
height: 12px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Grip dots pattern */
|
|
105
|
+
.basefn-resizable__handle-grip::after {
|
|
106
|
+
content: "";
|
|
107
|
+
display: block;
|
|
108
|
+
border-radius: var(--basefn-radius-full);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.basefn-resizable__handle--horizontal .basefn-resizable__handle-grip::after {
|
|
112
|
+
width: 4px;
|
|
113
|
+
height: 16px;
|
|
114
|
+
background-image: radial-gradient(
|
|
115
|
+
circle,
|
|
116
|
+
var(--basefn-text-muted) 1px,
|
|
117
|
+
transparent 1px
|
|
118
|
+
);
|
|
119
|
+
background-size: 4px 4px;
|
|
120
|
+
background-position: center;
|
|
121
|
+
background-repeat: repeat;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.basefn-resizable__handle--vertical .basefn-resizable__handle-grip::after {
|
|
125
|
+
width: 16px;
|
|
126
|
+
height: 4px;
|
|
127
|
+
background-image: radial-gradient(
|
|
128
|
+
circle,
|
|
129
|
+
var(--basefn-text-muted) 1px,
|
|
130
|
+
transparent 1px
|
|
131
|
+
);
|
|
132
|
+
background-size: 4px 4px;
|
|
133
|
+
background-position: center;
|
|
134
|
+
background-repeat: repeat;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Hover and focus states */
|
|
138
|
+
.basefn-resizable__handle:hover .basefn-resizable__handle-grip,
|
|
139
|
+
.basefn-resizable__handle:focus-visible .basefn-resizable__handle-grip {
|
|
140
|
+
background-color: var(--basefn-bg-tertiary);
|
|
141
|
+
border-color: var(--basefn-color-primary);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.basefn-resizable__handle:focus-visible .basefn-resizable__handle-grip {
|
|
145
|
+
box-shadow: 0 0 0 var(--basefn-focus-ring-width) var(--basefn-focus-ring-color);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Hide line when grip is present */
|
|
149
|
+
.basefn-resizable__handle--with-grip::after {
|
|
150
|
+
background-color: transparent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.basefn-resizable__handle--with-grip:hover::after,
|
|
154
|
+
.basefn-resizable__handle--with-grip:focus-visible::after {
|
|
155
|
+
background-color: transparent;
|
|
156
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
%%raw(`import './Basefn__Resizable.css'`)
|
|
2
|
+
|
|
3
|
+
open Xote
|
|
4
|
+
|
|
5
|
+
type direction = Horizontal | Vertical
|
|
6
|
+
|
|
7
|
+
type panel = {
|
|
8
|
+
content: Component.node,
|
|
9
|
+
defaultSize: float,
|
|
10
|
+
minSize?: float,
|
|
11
|
+
maxSize?: float,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let directionToString = (d: direction) =>
|
|
15
|
+
switch d {
|
|
16
|
+
| Horizontal => "horizontal"
|
|
17
|
+
| Vertical => "vertical"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// DOM helpers
|
|
21
|
+
let getClientX: Dom.event => float = %raw(`function(e) {
|
|
22
|
+
var t = e.touches ? e.touches[0] : e;
|
|
23
|
+
return t.clientX;
|
|
24
|
+
}`)
|
|
25
|
+
|
|
26
|
+
let getClientY: Dom.event => float = %raw(`function(e) {
|
|
27
|
+
var t = e.touches ? e.touches[0] : e;
|
|
28
|
+
return t.clientY;
|
|
29
|
+
}`)
|
|
30
|
+
|
|
31
|
+
let addDocListener: (string, Dom.event => unit) => unit = %raw(`function(ev, fn) {
|
|
32
|
+
document.addEventListener(ev, fn);
|
|
33
|
+
}`)
|
|
34
|
+
|
|
35
|
+
let removeDocListener: (string, Dom.event => unit) => unit = %raw(`function(ev, fn) {
|
|
36
|
+
document.removeEventListener(ev, fn);
|
|
37
|
+
}`)
|
|
38
|
+
|
|
39
|
+
let disableSelect: unit => unit = %raw(`function() {
|
|
40
|
+
document.body.style.userSelect = "none";
|
|
41
|
+
document.body.style.webkitUserSelect = "none";
|
|
42
|
+
}`)
|
|
43
|
+
|
|
44
|
+
let enableSelect: unit => unit = %raw(`function() {
|
|
45
|
+
document.body.style.userSelect = "";
|
|
46
|
+
document.body.style.webkitUserSelect = "";
|
|
47
|
+
}`)
|
|
48
|
+
|
|
49
|
+
let findClosest: (Dom.element, string) => Nullable.t<Dom.element> = %raw(`function(el, sel) {
|
|
50
|
+
return el.closest(sel);
|
|
51
|
+
}`)
|
|
52
|
+
|
|
53
|
+
let getElementRect: Dom.element => {..} = %raw(`function(el) {
|
|
54
|
+
return el.getBoundingClientRect();
|
|
55
|
+
}`)
|
|
56
|
+
|
|
57
|
+
let getKey: Dom.event => string = %raw(`function(e) { return e.key || "" }`)
|
|
58
|
+
|
|
59
|
+
let getEventTarget: Dom.event => Dom.element = %raw(`function(e) {
|
|
60
|
+
return e.target;
|
|
61
|
+
}`)
|
|
62
|
+
|
|
63
|
+
let preventDefault: Dom.event => unit = %raw(`function(e) { e.preventDefault() }`)
|
|
64
|
+
|
|
65
|
+
let genId: unit => string = %raw(`function() {
|
|
66
|
+
return "basefn-resizable-" + Math.random().toString(36).substr(2, 9);
|
|
67
|
+
}`)
|
|
68
|
+
|
|
69
|
+
// Set up mousedown/touchstart delegation on a container element by ID
|
|
70
|
+
let setupDragListeners: (string, (int, Dom.event) => unit) => unit = %raw(`function(containerId, onStart) {
|
|
71
|
+
requestAnimationFrame(function() {
|
|
72
|
+
var container = document.getElementById(containerId);
|
|
73
|
+
if (!container) return;
|
|
74
|
+
|
|
75
|
+
function handler(e) {
|
|
76
|
+
var target = e.target;
|
|
77
|
+
var handle = target.closest ? target.closest(".basefn-resizable__handle") : null;
|
|
78
|
+
if (!handle || !container.contains(handle)) return;
|
|
79
|
+
var handles = container.querySelectorAll(".basefn-resizable__handle");
|
|
80
|
+
for (var i = 0; i < handles.length; i++) {
|
|
81
|
+
if (handles[i] === handle) {
|
|
82
|
+
onStart(i, e);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
container.addEventListener("mousedown", handler);
|
|
89
|
+
container.addEventListener("touchstart", handler, { passive: false });
|
|
90
|
+
});
|
|
91
|
+
}`)
|
|
92
|
+
|
|
93
|
+
type dragInfo = {
|
|
94
|
+
handleIndex: int,
|
|
95
|
+
startPos: float,
|
|
96
|
+
startSizes: array<float>,
|
|
97
|
+
container: Dom.element,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@jsx.component
|
|
101
|
+
let make = (
|
|
102
|
+
~panels: array<panel>,
|
|
103
|
+
~direction: direction=Horizontal,
|
|
104
|
+
~withHandle: bool=true,
|
|
105
|
+
~onResize: option<array<float> => unit>=?,
|
|
106
|
+
~class: string="",
|
|
107
|
+
) => {
|
|
108
|
+
let containerId = genId()
|
|
109
|
+
let sizes = Signal.make(panels->Array.map(p => p.defaultSize))
|
|
110
|
+
let isDragging = Signal.make(false)
|
|
111
|
+
let dragRef: ref<option<dragInfo>> = ref(None)
|
|
112
|
+
|
|
113
|
+
let getPos = (evt: Dom.event) =>
|
|
114
|
+
switch direction {
|
|
115
|
+
| Horizontal => getClientX(evt)
|
|
116
|
+
| Vertical => getClientY(evt)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let getContainerDimension = (container: Dom.element) => {
|
|
120
|
+
let rect = getElementRect(container)
|
|
121
|
+
switch direction {
|
|
122
|
+
| Horizontal => Obj.magic(rect)["width"]
|
|
123
|
+
| Vertical => Obj.magic(rect)["height"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let clampSizes = (leftIdx: int, rightIdx: int, newLeft: float, newRight: float) => {
|
|
128
|
+
let lp = panels->Array.getUnsafe(leftIdx)
|
|
129
|
+
let rp = panels->Array.getUnsafe(rightIdx)
|
|
130
|
+
let lMin = lp.minSize->Option.getOr(0.0)
|
|
131
|
+
let lMax = lp.maxSize->Option.getOr(100.0)
|
|
132
|
+
let rMin = rp.minSize->Option.getOr(0.0)
|
|
133
|
+
let rMax = rp.maxSize->Option.getOr(100.0)
|
|
134
|
+
let total = newLeft +. newRight
|
|
135
|
+
|
|
136
|
+
if newLeft < lMin {
|
|
137
|
+
(lMin, total -. lMin)
|
|
138
|
+
} else if newLeft > lMax {
|
|
139
|
+
(lMax, total -. lMax)
|
|
140
|
+
} else if newRight < rMin {
|
|
141
|
+
(total -. rMin, rMin)
|
|
142
|
+
} else if newRight > rMax {
|
|
143
|
+
(total -. rMax, rMax)
|
|
144
|
+
} else {
|
|
145
|
+
(newLeft, newRight)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let applySizes = (newSizes: array<float>) => {
|
|
150
|
+
Signal.set(sizes, newSizes)
|
|
151
|
+
switch onResize {
|
|
152
|
+
| Some(cb) => cb(newSizes)
|
|
153
|
+
| None => ()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let onMouseMove = (evt: Dom.event) => {
|
|
158
|
+
switch dragRef.contents {
|
|
159
|
+
| None => ()
|
|
160
|
+
| Some(info) =>
|
|
161
|
+
let containerSize = getContainerDimension(info.container)
|
|
162
|
+
if containerSize > 0.0 {
|
|
163
|
+
let delta = getPos(evt) -. info.startPos
|
|
164
|
+
let deltaPercent = delta /. containerSize *. 100.0
|
|
165
|
+
let li = info.handleIndex
|
|
166
|
+
let ri = info.handleIndex + 1
|
|
167
|
+
let origLeft = info.startSizes->Array.getUnsafe(li)
|
|
168
|
+
let origRight = info.startSizes->Array.getUnsafe(ri)
|
|
169
|
+
let (nl, nr) = clampSizes(li, ri, origLeft +. deltaPercent, origRight -. deltaPercent)
|
|
170
|
+
let newSizes = Signal.get(sizes)->Array.copy
|
|
171
|
+
newSizes->Array.setUnsafe(li, nl)
|
|
172
|
+
newSizes->Array.setUnsafe(ri, nr)
|
|
173
|
+
applySizes(newSizes)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let rec onMouseUp = (_: Dom.event) => {
|
|
179
|
+
dragRef := None
|
|
180
|
+
Signal.set(isDragging, false)
|
|
181
|
+
enableSelect()
|
|
182
|
+
removeDocListener("mousemove", onMouseMove)
|
|
183
|
+
removeDocListener("mouseup", onMouseUp)
|
|
184
|
+
removeDocListener("touchmove", onMouseMove)
|
|
185
|
+
removeDocListener("touchend", onMouseUp)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let startDrag = (handleIndex: int, evt: Dom.event) => {
|
|
189
|
+
preventDefault(evt)
|
|
190
|
+
let target = getEventTarget(evt)
|
|
191
|
+
let maybeContainer = findClosest(target, ".basefn-resizable")
|
|
192
|
+
switch Nullable.toOption(maybeContainer) {
|
|
193
|
+
| None => ()
|
|
194
|
+
| Some(container) =>
|
|
195
|
+
dragRef := Some({
|
|
196
|
+
handleIndex,
|
|
197
|
+
startPos: getPos(evt),
|
|
198
|
+
startSizes: Signal.get(sizes)->Array.copy,
|
|
199
|
+
container,
|
|
200
|
+
})
|
|
201
|
+
Signal.set(isDragging, true)
|
|
202
|
+
disableSelect()
|
|
203
|
+
addDocListener("mousemove", onMouseMove)
|
|
204
|
+
addDocListener("mouseup", onMouseUp)
|
|
205
|
+
addDocListener("touchmove", onMouseMove)
|
|
206
|
+
addDocListener("touchend", onMouseUp)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let handleKeyDown = (handleIndex: int, evt: Dom.event) => {
|
|
211
|
+
let key = getKey(evt)
|
|
212
|
+
let step = 1.0
|
|
213
|
+
let li = handleIndex
|
|
214
|
+
let ri = handleIndex + 1
|
|
215
|
+
let currentSizes = Signal.get(sizes)
|
|
216
|
+
let left = currentSizes->Array.getUnsafe(li)
|
|
217
|
+
let right = currentSizes->Array.getUnsafe(ri)
|
|
218
|
+
|
|
219
|
+
let delta = switch (direction, key) {
|
|
220
|
+
| (Horizontal, "ArrowLeft") | (Vertical, "ArrowUp") => Some(-.step)
|
|
221
|
+
| (Horizontal, "ArrowRight") | (Vertical, "ArrowDown") => Some(step)
|
|
222
|
+
| _ => None
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
switch delta {
|
|
226
|
+
| Some(d) =>
|
|
227
|
+
preventDefault(evt)
|
|
228
|
+
let (nl, nr) = clampSizes(li, ri, left +. d, right -. d)
|
|
229
|
+
let newSizes = currentSizes->Array.copy
|
|
230
|
+
newSizes->Array.setUnsafe(li, nl)
|
|
231
|
+
newSizes->Array.setUnsafe(ri, nr)
|
|
232
|
+
applySizes(newSizes)
|
|
233
|
+
| None => ()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Set up mousedown/touchstart via event delegation
|
|
238
|
+
setupDragListeners(containerId, startDrag)
|
|
239
|
+
|
|
240
|
+
let containerClass =
|
|
241
|
+
"basefn-resizable basefn-resizable--" ++
|
|
242
|
+
directionToString(direction) ++
|
|
243
|
+
(class !== "" ? " " ++ class : "")
|
|
244
|
+
|
|
245
|
+
let elements: array<Component.node> = []
|
|
246
|
+
|
|
247
|
+
panels->Array.forEachWithIndex((panel, index) => {
|
|
248
|
+
let panelStyle = Computed.make(() => {
|
|
249
|
+
let size = Signal.get(sizes)->Array.getUnsafe(index)
|
|
250
|
+
"flex-basis:" ++ Float.toString(size) ++ "%;flex-grow:0;flex-shrink:0"
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
let panelClass = Computed.make(() => {
|
|
254
|
+
let base = "basefn-resizable__panel"
|
|
255
|
+
if Signal.get(isDragging) {
|
|
256
|
+
base ++ " basefn-resizable__panel--dragging"
|
|
257
|
+
} else {
|
|
258
|
+
base
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
elements->Array.push(
|
|
263
|
+
<div key={"panel-" ++ Int.toString(index)} class={panelClass} style={panelStyle}>
|
|
264
|
+
{panel.content}
|
|
265
|
+
</div>,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if index < Array.length(panels) - 1 {
|
|
269
|
+
let handleClass =
|
|
270
|
+
"basefn-resizable__handle basefn-resizable__handle--" ++
|
|
271
|
+
directionToString(direction) ++
|
|
272
|
+
(withHandle ? " basefn-resizable__handle--with-grip" : "")
|
|
273
|
+
|
|
274
|
+
elements->Array.push(
|
|
275
|
+
<div
|
|
276
|
+
key={"handle-" ++ Int.toString(index)}
|
|
277
|
+
class={handleClass}
|
|
278
|
+
onKeyDown={evt => handleKeyDown(index, evt)}
|
|
279
|
+
role="separator"
|
|
280
|
+
tabIndex={0}
|
|
281
|
+
>
|
|
282
|
+
{withHandle ? <div class="basefn-resizable__handle-grip" /> : <> </>}
|
|
283
|
+
</div>,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
<div id={containerId} class={containerClass}> {elements->Component.fragment} </div>
|
|
289
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Xote from "xote/src/Xote.res.mjs";
|
|
4
|
+
import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
|
|
5
|
+
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
6
|
+
|
|
7
|
+
import './Basefn__Resizable.css'
|
|
8
|
+
;
|
|
9
|
+
|
|
10
|
+
function directionToString(d) {
|
|
11
|
+
if (d === "Horizontal") {
|
|
12
|
+
return "horizontal";
|
|
13
|
+
} else {
|
|
14
|
+
return "vertical";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let getClientX = (function(e) {
|
|
19
|
+
var t = e.touches ? e.touches[0] : e;
|
|
20
|
+
return t.clientX;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
let getClientY = (function(e) {
|
|
24
|
+
var t = e.touches ? e.touches[0] : e;
|
|
25
|
+
return t.clientY;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let addDocListener = (function(ev, fn) {
|
|
29
|
+
document.addEventListener(ev, fn);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let removeDocListener = (function(ev, fn) {
|
|
33
|
+
document.removeEventListener(ev, fn);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let disableSelect = (function() {
|
|
37
|
+
document.body.style.userSelect = "none";
|
|
38
|
+
document.body.style.webkitUserSelect = "none";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let enableSelect = (function() {
|
|
42
|
+
document.body.style.userSelect = "";
|
|
43
|
+
document.body.style.webkitUserSelect = "";
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let findClosest = (function(el, sel) {
|
|
47
|
+
return el.closest(sel);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let getElementRect = (function(el) {
|
|
51
|
+
return el.getBoundingClientRect();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let getKey = (function(e) { return e.key || "" });
|
|
55
|
+
|
|
56
|
+
let getEventTarget = (function(e) {
|
|
57
|
+
return e.target;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let preventDefault = (function(e) { e.preventDefault() });
|
|
61
|
+
|
|
62
|
+
let genId = (function() {
|
|
63
|
+
return "basefn-resizable-" + Math.random().toString(36).substr(2, 9);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let setupDragListeners = (function(containerId, onStart) {
|
|
67
|
+
requestAnimationFrame(function() {
|
|
68
|
+
var container = document.getElementById(containerId);
|
|
69
|
+
if (!container) return;
|
|
70
|
+
|
|
71
|
+
function handler(e) {
|
|
72
|
+
var target = e.target;
|
|
73
|
+
var handle = target.closest ? target.closest(".basefn-resizable__handle") : null;
|
|
74
|
+
if (!handle || !container.contains(handle)) return;
|
|
75
|
+
var handles = container.querySelectorAll(".basefn-resizable__handle");
|
|
76
|
+
for (var i = 0; i < handles.length; i++) {
|
|
77
|
+
if (handles[i] === handle) {
|
|
78
|
+
onStart(i, e);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
container.addEventListener("mousedown", handler);
|
|
85
|
+
container.addEventListener("touchstart", handler, { passive: false });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function Basefn__Resizable(props) {
|
|
90
|
+
let __class = props.class;
|
|
91
|
+
let onResize = props.onResize;
|
|
92
|
+
let __withHandle = props.withHandle;
|
|
93
|
+
let __direction = props.direction;
|
|
94
|
+
let panels = props.panels;
|
|
95
|
+
let direction = __direction !== undefined ? __direction : "Horizontal";
|
|
96
|
+
let withHandle = __withHandle !== undefined ? __withHandle : true;
|
|
97
|
+
let $$class = __class !== undefined ? __class : "";
|
|
98
|
+
let containerId = genId();
|
|
99
|
+
let sizes = Xote.Signal.make(panels.map(p => p.defaultSize), undefined, undefined);
|
|
100
|
+
let isDragging = Xote.Signal.make(false, undefined, undefined);
|
|
101
|
+
let dragRef = {
|
|
102
|
+
contents: undefined
|
|
103
|
+
};
|
|
104
|
+
let getPos = evt => {
|
|
105
|
+
if (direction === "Horizontal") {
|
|
106
|
+
return getClientX(evt);
|
|
107
|
+
} else {
|
|
108
|
+
return getClientY(evt);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
let getContainerDimension = container => {
|
|
112
|
+
let rect = getElementRect(container);
|
|
113
|
+
if (direction === "Horizontal") {
|
|
114
|
+
return rect.width;
|
|
115
|
+
} else {
|
|
116
|
+
return rect.height;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
let clampSizes = (leftIdx, rightIdx, newLeft, newRight) => {
|
|
120
|
+
let lp = panels[leftIdx];
|
|
121
|
+
let rp = panels[rightIdx];
|
|
122
|
+
let lMin = Core__Option.getOr(lp.minSize, 0.0);
|
|
123
|
+
let lMax = Core__Option.getOr(lp.maxSize, 100.0);
|
|
124
|
+
let rMin = Core__Option.getOr(rp.minSize, 0.0);
|
|
125
|
+
let rMax = Core__Option.getOr(rp.maxSize, 100.0);
|
|
126
|
+
let total = newLeft + newRight;
|
|
127
|
+
if (newLeft < lMin) {
|
|
128
|
+
return [
|
|
129
|
+
lMin,
|
|
130
|
+
total - lMin
|
|
131
|
+
];
|
|
132
|
+
} else if (newLeft > lMax) {
|
|
133
|
+
return [
|
|
134
|
+
lMax,
|
|
135
|
+
total - lMax
|
|
136
|
+
];
|
|
137
|
+
} else if (newRight < rMin) {
|
|
138
|
+
return [
|
|
139
|
+
total - rMin,
|
|
140
|
+
rMin
|
|
141
|
+
];
|
|
142
|
+
} else if (newRight > rMax) {
|
|
143
|
+
return [
|
|
144
|
+
total - rMax,
|
|
145
|
+
rMax
|
|
146
|
+
];
|
|
147
|
+
} else {
|
|
148
|
+
return [
|
|
149
|
+
newLeft,
|
|
150
|
+
newRight
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
let applySizes = newSizes => {
|
|
155
|
+
Xote.Signal.set(sizes, newSizes);
|
|
156
|
+
if (onResize !== undefined) {
|
|
157
|
+
return onResize(newSizes);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
let onMouseMove = evt => {
|
|
161
|
+
let info = dragRef.contents;
|
|
162
|
+
if (info === undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let containerSize = getContainerDimension(info.container);
|
|
166
|
+
if (containerSize <= 0.0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
let delta = getPos(evt) - info.startPos;
|
|
170
|
+
let deltaPercent = delta / containerSize * 100.0;
|
|
171
|
+
let li = info.handleIndex;
|
|
172
|
+
let ri = info.handleIndex + 1 | 0;
|
|
173
|
+
let origLeft = info.startSizes[li];
|
|
174
|
+
let origRight = info.startSizes[ri];
|
|
175
|
+
let match = clampSizes(li, ri, origLeft + deltaPercent, origRight - deltaPercent);
|
|
176
|
+
let newSizes = Xote.Signal.get(sizes).slice();
|
|
177
|
+
newSizes[li] = match[0];
|
|
178
|
+
newSizes[ri] = match[1];
|
|
179
|
+
applySizes(newSizes);
|
|
180
|
+
};
|
|
181
|
+
let onMouseUp = param => {
|
|
182
|
+
dragRef.contents = undefined;
|
|
183
|
+
Xote.Signal.set(isDragging, false);
|
|
184
|
+
enableSelect();
|
|
185
|
+
removeDocListener("mousemove", onMouseMove);
|
|
186
|
+
removeDocListener("mouseup", onMouseUp);
|
|
187
|
+
removeDocListener("touchmove", onMouseMove);
|
|
188
|
+
removeDocListener("touchend", onMouseUp);
|
|
189
|
+
};
|
|
190
|
+
let startDrag = (handleIndex, evt) => {
|
|
191
|
+
preventDefault(evt);
|
|
192
|
+
let target = getEventTarget(evt);
|
|
193
|
+
let maybeContainer = findClosest(target, ".basefn-resizable");
|
|
194
|
+
if (!(maybeContainer == null)) {
|
|
195
|
+
dragRef.contents = {
|
|
196
|
+
handleIndex: handleIndex,
|
|
197
|
+
startPos: getPos(evt),
|
|
198
|
+
startSizes: Xote.Signal.get(sizes).slice(),
|
|
199
|
+
container: maybeContainer
|
|
200
|
+
};
|
|
201
|
+
Xote.Signal.set(isDragging, true);
|
|
202
|
+
disableSelect();
|
|
203
|
+
addDocListener("mousemove", onMouseMove);
|
|
204
|
+
addDocListener("mouseup", onMouseUp);
|
|
205
|
+
addDocListener("touchmove", onMouseMove);
|
|
206
|
+
return addDocListener("touchend", onMouseUp);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
setupDragListeners(containerId, startDrag);
|
|
210
|
+
let containerClass = "basefn-resizable basefn-resizable--" + directionToString(direction) + (
|
|
211
|
+
$$class !== "" ? " " + $$class : ""
|
|
212
|
+
);
|
|
213
|
+
let elements = [];
|
|
214
|
+
panels.forEach((panel, index) => {
|
|
215
|
+
let panelStyle = Xote.Computed.make(() => {
|
|
216
|
+
let size = Xote.Signal.get(sizes)[index];
|
|
217
|
+
return "flex-basis:" + size.toString() + "%;flex-grow:0;flex-shrink:0";
|
|
218
|
+
}, undefined);
|
|
219
|
+
let panelClass = Xote.Computed.make(() => {
|
|
220
|
+
let base = "basefn-resizable__panel";
|
|
221
|
+
if (Xote.Signal.get(isDragging)) {
|
|
222
|
+
return base + " basefn-resizable__panel--dragging";
|
|
223
|
+
} else {
|
|
224
|
+
return base;
|
|
225
|
+
}
|
|
226
|
+
}, undefined);
|
|
227
|
+
elements.push(Xote__JSX.Elements.jsxKeyed("div", {
|
|
228
|
+
class: panelClass,
|
|
229
|
+
style: panelStyle,
|
|
230
|
+
children: panel.content
|
|
231
|
+
}, "panel-" + index.toString(), undefined));
|
|
232
|
+
if (index >= (panels.length - 1 | 0)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let handleClass = "basefn-resizable__handle basefn-resizable__handle--" + directionToString(direction) + (
|
|
236
|
+
withHandle ? " basefn-resizable__handle--with-grip" : ""
|
|
237
|
+
);
|
|
238
|
+
elements.push(Xote__JSX.Elements.jsxKeyed("div", {
|
|
239
|
+
class: handleClass,
|
|
240
|
+
role: "separator",
|
|
241
|
+
tabIndex: 0,
|
|
242
|
+
onKeyDown: evt => {
|
|
243
|
+
let key = getKey(evt);
|
|
244
|
+
let ri = index + 1 | 0;
|
|
245
|
+
let currentSizes = Xote.Signal.get(sizes);
|
|
246
|
+
let left = currentSizes[index];
|
|
247
|
+
let right = currentSizes[ri];
|
|
248
|
+
let delta;
|
|
249
|
+
if (direction === "Horizontal") {
|
|
250
|
+
switch (key) {
|
|
251
|
+
case "ArrowLeft" :
|
|
252
|
+
delta = - 1.0;
|
|
253
|
+
break;
|
|
254
|
+
case "ArrowRight" :
|
|
255
|
+
delta = 1.0;
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
delta = undefined;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
switch (key) {
|
|
262
|
+
case "ArrowDown" :
|
|
263
|
+
delta = 1.0;
|
|
264
|
+
break;
|
|
265
|
+
case "ArrowUp" :
|
|
266
|
+
delta = - 1.0;
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
delta = undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (delta === undefined) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
preventDefault(evt);
|
|
276
|
+
let match = clampSizes(index, ri, left + delta, right - delta);
|
|
277
|
+
let newSizes = currentSizes.slice();
|
|
278
|
+
newSizes[index] = match[0];
|
|
279
|
+
newSizes[ri] = match[1];
|
|
280
|
+
applySizes(newSizes);
|
|
281
|
+
},
|
|
282
|
+
children: withHandle ? Xote__JSX.Elements.jsx("div", {
|
|
283
|
+
class: "basefn-resizable__handle-grip"
|
|
284
|
+
}) : Xote__JSX.jsx(Xote__JSX.jsxFragment, {})
|
|
285
|
+
}, "handle-" + index.toString(), undefined));
|
|
286
|
+
});
|
|
287
|
+
return Xote__JSX.Elements.jsx("div", {
|
|
288
|
+
id: containerId,
|
|
289
|
+
class: containerClass,
|
|
290
|
+
children: Xote.Component.fragment(elements)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let make = Basefn__Resizable;
|
|
295
|
+
|
|
296
|
+
export {
|
|
297
|
+
directionToString,
|
|
298
|
+
getClientX,
|
|
299
|
+
getClientY,
|
|
300
|
+
addDocListener,
|
|
301
|
+
removeDocListener,
|
|
302
|
+
disableSelect,
|
|
303
|
+
enableSelect,
|
|
304
|
+
findClosest,
|
|
305
|
+
getElementRect,
|
|
306
|
+
getKey,
|
|
307
|
+
getEventTarget,
|
|
308
|
+
preventDefault,
|
|
309
|
+
genId,
|
|
310
|
+
setupDragListeners,
|
|
311
|
+
make,
|
|
312
|
+
}
|
|
313
|
+
/* Not a pure module */
|
|
@@ -65,10 +65,18 @@ let make = (
|
|
|
65
65
|
| "ArrowDown" => {
|
|
66
66
|
let _ = Basefn__Dom.preventDefault(evt)
|
|
67
67
|
Signal.update(activeIndex, i => mod(i + 1, max(len, 1)))
|
|
68
|
+
let _ = %raw(`requestAnimationFrame(() => {
|
|
69
|
+
const el = document.querySelector(".basefn-spotlight__item--active");
|
|
70
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
71
|
+
})`)
|
|
68
72
|
}
|
|
69
73
|
| "ArrowUp" => {
|
|
70
74
|
let _ = Basefn__Dom.preventDefault(evt)
|
|
71
75
|
Signal.update(activeIndex, i => mod(i - 1 + max(len, 1), max(len, 1)))
|
|
76
|
+
let _ = %raw(`requestAnimationFrame(() => {
|
|
77
|
+
const el = document.querySelector(".basefn-spotlight__item--active");
|
|
78
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
79
|
+
})`)
|
|
72
80
|
}
|
|
73
81
|
| "Enter" =>
|
|
74
82
|
if len > 0 {
|
|
@@ -54,10 +54,20 @@ function Basefn__Spotlight(props) {
|
|
|
54
54
|
switch (k) {
|
|
55
55
|
case "ArrowDown" :
|
|
56
56
|
Basefn__Dom.preventDefault(evt);
|
|
57
|
-
|
|
57
|
+
Xote.Signal.update(activeIndex, i => Primitive_int.mod_(i + 1 | 0, Primitive_int.max(len, 1)));
|
|
58
|
+
((requestAnimationFrame(() => {
|
|
59
|
+
const el = document.querySelector(".basefn-spotlight__item--active");
|
|
60
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
61
|
+
})));
|
|
62
|
+
return;
|
|
58
63
|
case "ArrowUp" :
|
|
59
64
|
Basefn__Dom.preventDefault(evt);
|
|
60
|
-
|
|
65
|
+
Xote.Signal.update(activeIndex, i => Primitive_int.mod_((i - 1 | 0) + Primitive_int.max(len, 1) | 0, Primitive_int.max(len, 1)));
|
|
66
|
+
((requestAnimationFrame(() => {
|
|
67
|
+
const el = document.querySelector(".basefn-spotlight__item--active");
|
|
68
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
69
|
+
})));
|
|
70
|
+
return;
|
|
61
71
|
case "Enter" :
|
|
62
72
|
if (len <= 0) {
|
|
63
73
|
return;
|