@cloudron/pankow 4.4.11 → 4.4.13
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/components/DirectoryViewGridItem.vue +1 -1
- package/components/FormGroup.vue +1 -2
- package/components/ResizeHandle.vue +118 -0
- package/components/SideBar.vue +272 -24
- package/components/SplitLayout.vue +7 -76
- package/gestures.js +96 -4
- package/package.json +7 -7
- package/types/gestures.d.ts +27 -3
- package/viewers/ImageViewer.vue +29 -9
package/components/FormGroup.vue
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
defineProps({
|
|
4
|
+
orientation: {
|
|
5
|
+
type: String,
|
|
6
|
+
default: 'horizontal'
|
|
7
|
+
},
|
|
8
|
+
opaque: {
|
|
9
|
+
type: Boolean,
|
|
10
|
+
default: false
|
|
11
|
+
},
|
|
12
|
+
dragging: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false
|
|
15
|
+
},
|
|
16
|
+
contrast: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: 'auto'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div
|
|
26
|
+
class="pankow-resize-handle"
|
|
27
|
+
:class="{
|
|
28
|
+
'pankow-resize-handle-vertical': orientation === 'vertical',
|
|
29
|
+
'pankow-resize-handle-opaque': opaque,
|
|
30
|
+
'pankow-resize-handle-dragging': dragging,
|
|
31
|
+
'pankow-resize-handle-contrast-light': contrast === 'light',
|
|
32
|
+
'pankow-resize-handle-contrast-dark': contrast === 'dark'
|
|
33
|
+
}"
|
|
34
|
+
>
|
|
35
|
+
<div class="pankow-resize-handle-grip"><span></span><span></span></div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
|
|
41
|
+
.pankow-resize-handle {
|
|
42
|
+
background: transparent;
|
|
43
|
+
position: relative;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.pankow-resize-handle:not(.pankow-resize-handle-vertical) {
|
|
51
|
+
width: 6px;
|
|
52
|
+
cursor: col-resize;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.pankow-resize-handle-vertical {
|
|
56
|
+
height: 6px;
|
|
57
|
+
cursor: row-resize;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.pankow-resize-handle-opaque {
|
|
61
|
+
background: var(--pankow-color-background);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.pankow-resize-handle:hover,
|
|
65
|
+
.pankow-resize-handle-dragging {
|
|
66
|
+
background: var(--pankow-input-border-color);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.pankow-resize-handle-grip {
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 50%;
|
|
72
|
+
left: 50%;
|
|
73
|
+
transform: translate(-50%, -50%);
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: 2px;
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transition: opacity 0.15s;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.pankow-resize-handle:not(.pankow-resize-handle-vertical) .pankow-resize-handle-grip {
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pankow-resize-handle-vertical .pankow-resize-handle-grip {
|
|
86
|
+
flex-direction: row;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.pankow-resize-handle:hover .pankow-resize-handle-grip,
|
|
90
|
+
.pankow-resize-handle-dragging .pankow-resize-handle-grip {
|
|
91
|
+
opacity: 0.5;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.pankow-resize-handle-grip span {
|
|
95
|
+
display: block;
|
|
96
|
+
border-radius: 1px;
|
|
97
|
+
background: currentColor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.pankow-resize-handle-contrast-light .pankow-resize-handle-grip span {
|
|
101
|
+
background: var(--pankow-text-color-white);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.pankow-resize-handle-contrast-dark .pankow-resize-handle-grip span {
|
|
105
|
+
background: var(--pankow-color-dark);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.pankow-resize-handle:not(.pankow-resize-handle-vertical) .pankow-resize-handle-grip span {
|
|
109
|
+
width: 2px;
|
|
110
|
+
height: 16px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.pankow-resize-handle-vertical .pankow-resize-handle-grip span {
|
|
114
|
+
width: 16px;
|
|
115
|
+
height: 2px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
</style>
|
package/components/SideBar.vue
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
|
|
3
|
-
import { ref, useTemplateRef, onMounted } from 'vue';
|
|
4
|
-
import {
|
|
3
|
+
import { ref, useTemplateRef, onMounted, onUnmounted, nextTick } from 'vue';
|
|
4
|
+
import { onPan } from '../gestures.js';
|
|
5
|
+
import ResizeHandle from './ResizeHandle.vue';
|
|
5
6
|
|
|
6
|
-
defineProps({
|
|
7
|
+
const props = defineProps({
|
|
7
8
|
showOpenAction: {
|
|
8
9
|
type: Boolean,
|
|
9
10
|
default: true
|
|
10
11
|
},
|
|
12
|
+
resizable: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false
|
|
15
|
+
},
|
|
16
|
+
storageKey: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: 'pankow-sidebar-width'
|
|
19
|
+
},
|
|
11
20
|
});
|
|
12
21
|
|
|
22
|
+
const container = useTemplateRef('container');
|
|
13
23
|
const sideBar = useTemplateRef('sideBar');
|
|
24
|
+
const backDrop = useTemplateRef('backDrop');
|
|
14
25
|
const isVisible = ref(false);
|
|
26
|
+
const dragging = ref(false);
|
|
27
|
+
const resizing = ref(false);
|
|
28
|
+
|
|
29
|
+
const SIDEBAR_MIN_WIDTH = 200;
|
|
30
|
+
const SIDEBAR_MAX_WIDTH = 600;
|
|
31
|
+
let resizeStartX = 0;
|
|
32
|
+
let resizeStartWidth = 0;
|
|
15
33
|
|
|
16
34
|
function open() {
|
|
17
35
|
isVisible.value = true;
|
|
@@ -21,10 +39,199 @@ function close() {
|
|
|
21
39
|
isVisible.value = false;
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
function width() {
|
|
43
|
+
return sideBar.value ? sideBar.value.offsetWidth : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clearPendingSettle() {
|
|
47
|
+
if (settleOnEnd && sideBar.value) sideBar.value.removeEventListener('transitionend', settleOnEnd);
|
|
48
|
+
settleOnEnd = null;
|
|
49
|
+
if (settleTimer) {
|
|
50
|
+
clearTimeout(settleTimer);
|
|
51
|
+
settleTimer = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dragStart() {
|
|
56
|
+
clearPendingSettle();
|
|
57
|
+
dragging.value = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function dragMove({ deltaX }) {
|
|
61
|
+
const offset = Math.max(-width(), Math.min(0, deltaX));
|
|
62
|
+
sideBar.value.style.transform = `translateX(${offset}px)`;
|
|
63
|
+
if (backDrop.value) backDrop.value.style.opacity = String(1 + offset / width());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function settle(shouldClose) {
|
|
67
|
+
dragging.value = false;
|
|
68
|
+
isVisible.value = shouldClose ? false : true;
|
|
69
|
+
|
|
70
|
+
nextTick(() => {
|
|
71
|
+
const el = sideBar.value;
|
|
72
|
+
const bd = backDrop.value;
|
|
73
|
+
if (!el) return;
|
|
74
|
+
|
|
75
|
+
clearPendingSettle();
|
|
76
|
+
|
|
77
|
+
el.style.transform = shouldClose ? 'translateX(-100%)' : 'translateX(0)';
|
|
78
|
+
if (bd) bd.style.opacity = '';
|
|
79
|
+
|
|
80
|
+
function cleanup() {
|
|
81
|
+
if (settleTimer) {
|
|
82
|
+
clearTimeout(settleTimer);
|
|
83
|
+
settleTimer = null;
|
|
84
|
+
}
|
|
85
|
+
if (settleOnEnd) {
|
|
86
|
+
el.removeEventListener('transitionend', onEnd);
|
|
87
|
+
settleOnEnd = null;
|
|
88
|
+
}
|
|
89
|
+
el.style.transform = '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onEnd(event) {
|
|
93
|
+
if (event.propertyName === 'transform') cleanup();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
settleOnEnd = onEnd;
|
|
97
|
+
el.addEventListener('transitionend', onEnd);
|
|
98
|
+
settleTimer = setTimeout(cleanup, 300);
|
|
27
99
|
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function suppressClick() {
|
|
103
|
+
suppressBackdropClick = true;
|
|
104
|
+
setTimeout(() => { suppressBackdropClick = false; }, 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onBackdropClick() {
|
|
108
|
+
if (suppressBackdropClick) {
|
|
109
|
+
suppressBackdropClick = false;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
close();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function dragEnd({ deltaX, velocity }) {
|
|
116
|
+
suppressClick();
|
|
117
|
+
const w = width();
|
|
118
|
+
settle(deltaX < -w / 2 || velocity < -0.5);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function dragCancel() {
|
|
122
|
+
suppressClick();
|
|
123
|
+
settle(false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resizeStart(event) {
|
|
127
|
+
if (!props.resizable) return;
|
|
128
|
+
resizing.value = true;
|
|
129
|
+
resizeStartX = event.clientX ?? event.touches?.[0]?.clientX ?? 0;
|
|
130
|
+
resizeStartWidth = container.value ? container.value.offsetWidth : 0;
|
|
131
|
+
document.body.style.cursor = 'col-resize';
|
|
132
|
+
document.body.style.userSelect = 'none';
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resizeMove(event) {
|
|
137
|
+
if (!resizing.value || !container.value) return;
|
|
138
|
+
const x = event.clientX ?? event.touches?.[0]?.clientX ?? 0;
|
|
139
|
+
const width = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, resizeStartWidth + (x - resizeStartX)));
|
|
140
|
+
applyWidth(width);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resizeEnd() {
|
|
144
|
+
if (!resizing.value) return;
|
|
145
|
+
resizing.value = false;
|
|
146
|
+
document.body.style.cursor = '';
|
|
147
|
+
document.body.style.userSelect = '';
|
|
148
|
+
saveWidth();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function applyWidth(width) {
|
|
152
|
+
if (!container.value) return;
|
|
153
|
+
container.value.style.width = `${width}px`;
|
|
154
|
+
container.value.style.minWidth = `${width}px`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function saveWidth() {
|
|
158
|
+
if (!props.resizable || !container.value || typeof window === 'undefined') return;
|
|
159
|
+
try {
|
|
160
|
+
window.localStorage.setItem(props.storageKey, String(container.value.offsetWidth));
|
|
161
|
+
} catch (e) { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function restoreWidth() {
|
|
165
|
+
if (!props.resizable || typeof window === 'undefined') return;
|
|
166
|
+
try {
|
|
167
|
+
const stored = window.localStorage.getItem(props.storageKey);
|
|
168
|
+
if (!stored) return;
|
|
169
|
+
const width = parseInt(stored, 10);
|
|
170
|
+
if (Number.isNaN(width)) return;
|
|
171
|
+
applyWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, width)));
|
|
172
|
+
} catch (e) { /* ignore */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clearResizeWidth() {
|
|
176
|
+
if (container.value) {
|
|
177
|
+
container.value.style.width = '';
|
|
178
|
+
container.value.style.minWidth = '';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let removePans = [];
|
|
183
|
+
let settleTimer = null;
|
|
184
|
+
let settleOnEnd = null;
|
|
185
|
+
let suppressBackdropClick = false;
|
|
186
|
+
const media = typeof window !== 'undefined' ? window.matchMedia('(max-width: 576px)') : null;
|
|
187
|
+
|
|
188
|
+
function setup() {
|
|
189
|
+
if (!media || !media.matches || !sideBar.value || removePans.length) return;
|
|
190
|
+
const handlers = {
|
|
191
|
+
onStart: dragStart,
|
|
192
|
+
onMove: dragMove,
|
|
193
|
+
onEnd: dragEnd,
|
|
194
|
+
onCancel: dragCancel
|
|
195
|
+
};
|
|
196
|
+
removePans.push(onPan(sideBar.value, handlers));
|
|
197
|
+
if (backDrop.value) removePans.push(onPan(backDrop.value, handlers));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function teardown() {
|
|
201
|
+
removePans.forEach((remove) => { remove(); });
|
|
202
|
+
removePans = [];
|
|
203
|
+
dragging.value = false;
|
|
204
|
+
clearPendingSettle();
|
|
205
|
+
if (sideBar.value) sideBar.value.style.transform = '';
|
|
206
|
+
if (backDrop.value) backDrop.value.style.opacity = '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function onMediaChange() {
|
|
210
|
+
if (media && media.matches) {
|
|
211
|
+
setup();
|
|
212
|
+
clearResizeWidth();
|
|
213
|
+
} else {
|
|
214
|
+
teardown();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onMounted(() => {
|
|
219
|
+
setup();
|
|
220
|
+
restoreWidth();
|
|
221
|
+
document.addEventListener('mousemove', resizeMove);
|
|
222
|
+
document.addEventListener('mouseup', resizeEnd);
|
|
223
|
+
document.addEventListener('touchmove', resizeMove, { passive: false });
|
|
224
|
+
document.addEventListener('touchend', resizeEnd);
|
|
225
|
+
media && media.addEventListener('change', onMediaChange);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
onUnmounted(() => {
|
|
229
|
+
media && media.removeEventListener('change', onMediaChange);
|
|
230
|
+
document.removeEventListener('mousemove', resizeMove);
|
|
231
|
+
document.removeEventListener('mouseup', resizeEnd);
|
|
232
|
+
document.removeEventListener('touchmove', resizeMove);
|
|
233
|
+
document.removeEventListener('touchend', resizeEnd);
|
|
234
|
+
teardown();
|
|
28
235
|
});
|
|
29
236
|
|
|
30
237
|
defineExpose({ open, close });
|
|
@@ -32,19 +239,26 @@ defineExpose({ open, close });
|
|
|
32
239
|
</script>
|
|
33
240
|
|
|
34
241
|
<template>
|
|
35
|
-
<div class="pankow-sidebar-container" :class="{ 'pankow-sidebar-open': isVisible }">
|
|
36
|
-
<
|
|
37
|
-
|
|
242
|
+
<div class="pankow-sidebar-container" ref="container" :class="{ 'pankow-sidebar-open': isVisible, 'pankow-sidebar-dragging': dragging, 'pankow-sidebar-resizing': resizing }">
|
|
243
|
+
<div class="pankow-sidebar-backdrop" ref="backDrop" @click="onBackdropClick"></div>
|
|
244
|
+
<Transition name="pankow-scale">
|
|
245
|
+
<div class="pankow-sidebar-close-action" v-if="isVisible" tabindex="0" role="button" aria-label="Close sidebar" @click="close()" @keydown.enter="close()" @keydown.space.prevent="close()"><i class="fa-solid fa-xmark"></i></div>
|
|
246
|
+
<div class="pankow-sidebar-open-action" v-else-if="showOpenAction" tabindex="0" role="button" aria-label="Open sidebar" @click="open()" @keydown.enter="open()" @keydown.space.prevent="open()"><i class="fa-solid fa-bars"></i></div>
|
|
38
247
|
</Transition>
|
|
39
|
-
<div class="pankow-sidebar" ref="sideBar"
|
|
40
|
-
<Transition name="pankow-scale">
|
|
41
|
-
<div class="pankow-sidebar-close-action" v-if="isVisible" tabindex="0" role="button" aria-label="Close sidebar" @click="close()" @keydown.enter="close()" @keydown.space.prevent="close()"><i class="fa-solid fa-xmark"></i></div>
|
|
42
|
-
<div class="pankow-sidebar-open-action" v-else-if="showOpenAction" tabindex="0" role="button" aria-label="Open sidebar" @click="open()" @keydown.enter="open()" @keydown.space.prevent="open()"><i class="fa-solid fa-bars"></i></div>
|
|
43
|
-
</Transition>
|
|
248
|
+
<div class="pankow-sidebar" ref="sideBar">
|
|
44
249
|
<div class="pankow-sidebar-inner">
|
|
45
250
|
<slot></slot>
|
|
46
251
|
</div>
|
|
47
252
|
</div>
|
|
253
|
+
<ResizeHandle
|
|
254
|
+
v-if="resizable"
|
|
255
|
+
class="pankow-sidebar-resize-handle"
|
|
256
|
+
orientation="horizontal"
|
|
257
|
+
contrast="light"
|
|
258
|
+
:dragging="resizing"
|
|
259
|
+
@mousedown="resizeStart"
|
|
260
|
+
@touchstart.prevent="resizeStart"
|
|
261
|
+
/>
|
|
48
262
|
</div>
|
|
49
263
|
</template>
|
|
50
264
|
|
|
@@ -53,13 +267,28 @@ defineExpose({ open, close });
|
|
|
53
267
|
.pankow-sidebar-container {
|
|
54
268
|
display: block;
|
|
55
269
|
height: 100%;
|
|
56
|
-
min-width:
|
|
270
|
+
min-width: 280px;
|
|
271
|
+
position: relative;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.pankow-sidebar-resize-handle {
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: 0;
|
|
277
|
+
right: 0;
|
|
278
|
+
bottom: 0;
|
|
279
|
+
z-index: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.pankow-sidebar-resizing {
|
|
283
|
+
user-select: none;
|
|
57
284
|
}
|
|
58
285
|
|
|
59
286
|
.pankow-sidebar {
|
|
60
287
|
display: block;
|
|
61
288
|
height: 100%;
|
|
62
289
|
overflow: auto;
|
|
290
|
+
background-color: #0a2e4c;
|
|
291
|
+
color: white;
|
|
63
292
|
}
|
|
64
293
|
|
|
65
294
|
.pankow-sidebar-inner {
|
|
@@ -91,7 +320,7 @@ defineExpose({ open, close });
|
|
|
91
320
|
|
|
92
321
|
.pankow-sidebar-backdrop {
|
|
93
322
|
display: none;
|
|
94
|
-
position:
|
|
323
|
+
position: fixed;
|
|
95
324
|
top: 0;
|
|
96
325
|
bottom: 0;
|
|
97
326
|
left: 0;
|
|
@@ -115,19 +344,34 @@ defineExpose({ open, close });
|
|
|
115
344
|
}
|
|
116
345
|
|
|
117
346
|
.pankow-sidebar {
|
|
118
|
-
position:
|
|
119
|
-
|
|
347
|
+
position: fixed;
|
|
348
|
+
top: 0;
|
|
120
349
|
left: 0;
|
|
121
|
-
|
|
350
|
+
bottom: 0;
|
|
351
|
+
width: calc(70%);
|
|
352
|
+
transform: translateX(-100%);
|
|
353
|
+
transition: transform 250ms ease-in-out;
|
|
122
354
|
}
|
|
123
355
|
|
|
124
|
-
.pankow-sidebar-open .pankow-sidebar
|
|
356
|
+
.pankow-sidebar-open .pankow-sidebar {
|
|
357
|
+
transform: translateX(0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.pankow-sidebar-dragging .pankow-sidebar,
|
|
361
|
+
.pankow-sidebar-dragging .pankow-sidebar-backdrop {
|
|
362
|
+
transition: none;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.pankow-sidebar-backdrop {
|
|
125
366
|
display: block;
|
|
367
|
+
opacity: 0;
|
|
368
|
+
pointer-events: none;
|
|
369
|
+
transition: opacity 250ms ease-in-out;
|
|
126
370
|
}
|
|
127
371
|
|
|
128
|
-
.pankow-sidebar-
|
|
129
|
-
|
|
130
|
-
|
|
372
|
+
.pankow-sidebar-open .pankow-sidebar-backdrop {
|
|
373
|
+
opacity: 1;
|
|
374
|
+
pointer-events: auto;
|
|
131
375
|
}
|
|
132
376
|
|
|
133
377
|
.pankow-sidebar-open-action {
|
|
@@ -141,6 +385,10 @@ defineExpose({ open, close });
|
|
|
141
385
|
.pankow-sidebar-close-action {
|
|
142
386
|
display: block;
|
|
143
387
|
}
|
|
388
|
+
|
|
389
|
+
.pankow-sidebar-resize-handle {
|
|
390
|
+
display: none;
|
|
391
|
+
}
|
|
144
392
|
}
|
|
145
393
|
|
|
146
|
-
</style>
|
|
394
|
+
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
|
|
3
|
+
import ResizeHandle from './ResizeHandle.vue';
|
|
3
4
|
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
orientation: {
|
|
@@ -142,20 +143,19 @@ const firstStyle = computed(() => {
|
|
|
142
143
|
class="pankow-split-layout"
|
|
143
144
|
:class="{
|
|
144
145
|
'pankow-split-layout-dragging': isDragging,
|
|
145
|
-
'pankow-split-layout-vertical': !isHorizontal
|
|
146
|
-
'pankow-split-layout-divider-opaque': opaque
|
|
146
|
+
'pankow-split-layout-vertical': !isHorizontal
|
|
147
147
|
}"
|
|
148
148
|
>
|
|
149
149
|
<div class="pankow-split-layout-first" :style="firstStyle">
|
|
150
150
|
<slot name="left" />
|
|
151
151
|
</div>
|
|
152
|
-
<
|
|
153
|
-
|
|
152
|
+
<ResizeHandle
|
|
153
|
+
:orientation="orientation"
|
|
154
|
+
:opaque="opaque"
|
|
155
|
+
:dragging="isDragging"
|
|
154
156
|
@mousedown="startDrag"
|
|
155
157
|
@touchstart.prevent="startDrag"
|
|
156
|
-
|
|
157
|
-
<div class="pankow-split-layout-handle"><span></span><span></span></div>
|
|
158
|
-
</div>
|
|
158
|
+
/>
|
|
159
159
|
<div class="pankow-split-layout-second">
|
|
160
160
|
<slot name="right" />
|
|
161
161
|
</div>
|
|
@@ -186,75 +186,6 @@ const firstStyle = computed(() => {
|
|
|
186
186
|
min-width: 0;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
.pankow-split-layout-divider {
|
|
190
|
-
background: transparent;
|
|
191
|
-
position: relative;
|
|
192
|
-
flex-shrink: 0;
|
|
193
|
-
display: flex;
|
|
194
|
-
align-items: center;
|
|
195
|
-
justify-content: center;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-divider {
|
|
199
|
-
width: 6px;
|
|
200
|
-
cursor: col-resize;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.pankow-split-layout-vertical .pankow-split-layout-divider {
|
|
204
|
-
height: 6px;
|
|
205
|
-
cursor: row-resize;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.pankow-split-layout-divider-opaque .pankow-split-layout-divider {
|
|
209
|
-
background: var(--pankow-color-background);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.pankow-split-layout-divider:hover,
|
|
213
|
-
.pankow-split-layout-dragging .pankow-split-layout-divider {
|
|
214
|
-
background: var(--pankow-input-border-color);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.pankow-split-layout-handle {
|
|
218
|
-
position: absolute;
|
|
219
|
-
top: 50%;
|
|
220
|
-
left: 50%;
|
|
221
|
-
transform: translate(-50%, -50%);
|
|
222
|
-
display: flex;
|
|
223
|
-
gap: 2px;
|
|
224
|
-
opacity: 0;
|
|
225
|
-
transition: opacity 0.15s;
|
|
226
|
-
pointer-events: none;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle {
|
|
230
|
-
flex-direction: column;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
.pankow-split-layout-vertical .pankow-split-layout-handle {
|
|
234
|
-
flex-direction: row;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.pankow-split-layout-divider:hover .pankow-split-layout-handle,
|
|
238
|
-
.pankow-split-layout-dragging .pankow-split-layout-handle {
|
|
239
|
-
opacity: 0.5;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.pankow-split-layout-handle span {
|
|
243
|
-
display: block;
|
|
244
|
-
border-radius: 1px;
|
|
245
|
-
background: var(--pankow-color-text);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle span {
|
|
249
|
-
width: 2px;
|
|
250
|
-
height: 16px;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
.pankow-split-layout-vertical .pankow-split-layout-handle span {
|
|
254
|
-
width: 16px;
|
|
255
|
-
height: 2px;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
189
|
.pankow-split-layout-second {
|
|
259
190
|
flex: 1;
|
|
260
191
|
min-width: 0;
|
package/gestures.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Gesture handlers
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* ```
|
|
6
|
-
* import { onSwipe } from 'pankow/gestures';
|
|
6
|
+
* import { onSwipe, onPan } from 'pankow/gestures';
|
|
7
7
|
* onSwipe(element, (direction) => {});
|
|
8
|
+
* onPan(element, { onStart, onMove, onEnd, onCancel });
|
|
8
9
|
* ```
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -55,10 +56,101 @@ function onSwipe(elem, callback, threshold = 50) {
|
|
|
55
56
|
elem.addEventListener('touchcancel', touchEnd);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Registers pointer event listeners on an element for interactive dragging (pan).
|
|
61
|
+
* The drag is only "claimed" once the pointer moves past a small threshold and
|
|
62
|
+
* horizontal movement dominates vertical movement, so vertical scrolling inside
|
|
63
|
+
* the element still works as expected.
|
|
64
|
+
*
|
|
65
|
+
* Callbacks:
|
|
66
|
+
* onStart({ x, y }) - drag claimed (horizontal intent confirmed)
|
|
67
|
+
* onMove({ deltaX, deltaY }) - pointer moved (called continuously while dragging)
|
|
68
|
+
* onEnd({ deltaX, velocity }) - pointer released; velocity in px/ms (negative = leftward)
|
|
69
|
+
* onCancel() - pointer cancelled
|
|
70
|
+
*
|
|
71
|
+
* @param {HTMLElement} elem
|
|
72
|
+
* @param {{ onStart?: Function, onMove?: Function, onEnd?: Function, onCancel?: Function }} handlers
|
|
73
|
+
* @param {number} [threshold=10] - Minimum pixel distance before the drag is claimed
|
|
74
|
+
*/
|
|
75
|
+
function onPan(elem, handlers = {}, threshold = 10) {
|
|
76
|
+
let pointerId = null;
|
|
77
|
+
let dragging = false;
|
|
78
|
+
let startX = 0;
|
|
79
|
+
let startY = 0;
|
|
80
|
+
let lastX = 0;
|
|
81
|
+
let lastTime = 0;
|
|
82
|
+
let lastVelocity = 0;
|
|
83
|
+
|
|
84
|
+
function pointerDown(event) {
|
|
85
|
+
if (dragging || !event.isPrimary) return;
|
|
86
|
+
pointerId = event.pointerId;
|
|
87
|
+
startX = event.clientX;
|
|
88
|
+
startY = event.clientY;
|
|
89
|
+
lastX = startX;
|
|
90
|
+
lastTime = event.timeStamp;
|
|
91
|
+
lastVelocity = 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pointerMove(event) {
|
|
95
|
+
if (pointerId === null || event.pointerId !== pointerId) return;
|
|
96
|
+
|
|
97
|
+
const deltaX = event.clientX - startX;
|
|
98
|
+
const deltaY = event.clientY - startY;
|
|
99
|
+
|
|
100
|
+
if (!dragging) {
|
|
101
|
+
const absX = Math.abs(deltaX);
|
|
102
|
+
const absY = Math.abs(deltaY);
|
|
103
|
+
if (absX < threshold && absY < threshold) return;
|
|
104
|
+
if (absY > absX) return; // vertical intent, keep it for scrolling
|
|
105
|
+
dragging = true;
|
|
106
|
+
try { elem.setPointerCapture(pointerId); } catch (e) { /* noop */ }
|
|
107
|
+
if (typeof handlers.onStart === 'function') handlers.onStart({ x: startX, y: startY });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const now = event.timeStamp;
|
|
111
|
+
const dt = now - lastTime;
|
|
112
|
+
if (dt > 0) {
|
|
113
|
+
lastVelocity = (event.clientX - lastX) / dt;
|
|
114
|
+
}
|
|
115
|
+
lastX = event.clientX;
|
|
116
|
+
lastTime = now;
|
|
117
|
+
|
|
118
|
+
if (typeof handlers.onMove === 'function') handlers.onMove({ deltaX, deltaY });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function end(event) {
|
|
122
|
+
if (pointerId === null || event.pointerId !== pointerId) return;
|
|
123
|
+
pointerId = null;
|
|
124
|
+
|
|
125
|
+
if (dragging) {
|
|
126
|
+
dragging = false;
|
|
127
|
+
if (event.type === 'pointercancel') {
|
|
128
|
+
if (typeof handlers.onCancel === 'function') handlers.onCancel();
|
|
129
|
+
} else if (typeof handlers.onEnd === 'function') {
|
|
130
|
+
handlers.onEnd({ deltaX: event.clientX - startX, velocity: lastVelocity });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
elem.addEventListener('pointerdown', pointerDown);
|
|
136
|
+
elem.addEventListener('pointermove', pointerMove);
|
|
137
|
+
elem.addEventListener('pointerup', end);
|
|
138
|
+
elem.addEventListener('pointercancel', end);
|
|
139
|
+
|
|
140
|
+
return function remove() {
|
|
141
|
+
elem.removeEventListener('pointerdown', pointerDown);
|
|
142
|
+
elem.removeEventListener('pointermove', pointerMove);
|
|
143
|
+
elem.removeEventListener('pointerup', end);
|
|
144
|
+
elem.removeEventListener('pointercancel', end);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
58
148
|
export default {
|
|
59
|
-
onSwipe
|
|
149
|
+
onSwipe,
|
|
150
|
+
onPan
|
|
60
151
|
};
|
|
61
152
|
|
|
62
153
|
export {
|
|
63
|
-
onSwipe
|
|
154
|
+
onSwipe,
|
|
155
|
+
onPan
|
|
64
156
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudron/pankow",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.13",
|
|
5
5
|
"description": "Vue 3 UI component library for Cloudron apps",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@fontsource/inter": "^5.3.0",
|
|
41
41
|
"@fortawesome/fontawesome-free": "^7.3.1",
|
|
42
|
-
"filesize": "^11.0.
|
|
42
|
+
"filesize": "^11.0.23",
|
|
43
43
|
"monaco-editor": "^0.56.0",
|
|
44
44
|
"online-3d-viewer": "^0.18.0"
|
|
45
45
|
},
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@highlightjs/vue-plugin": "^2.1.0",
|
|
51
|
-
"@unhead/vue": "^3.
|
|
51
|
+
"@unhead/vue": "^3.4.0",
|
|
52
52
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
53
|
-
"highlight.js": "^11.
|
|
53
|
+
"highlight.js": "^11.12.0",
|
|
54
54
|
"typescript": "^7.0.2",
|
|
55
|
-
"vite": "^8.2.
|
|
55
|
+
"vite": "^8.2.2",
|
|
56
56
|
"vite-ssg": "^28.3.0",
|
|
57
|
-
"vue": "^3.5.
|
|
58
|
-
"vue-router": "^5.
|
|
57
|
+
"vue": "^3.5.42",
|
|
58
|
+
"vue-router": "^5.3.1"
|
|
59
59
|
},
|
|
60
60
|
"allowScripts": {
|
|
61
61
|
"core-js": false
|
package/types/gestures.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Gesture handlers
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* ```
|
|
6
|
-
* import { onSwipe } from 'pankow/gestures';
|
|
6
|
+
* import { onSwipe, onPan } from 'pankow/gestures';
|
|
7
7
|
* onSwipe(element, (direction) => {});
|
|
8
|
+
* onPan(element, { onStart, onMove, onEnd, onCancel });
|
|
8
9
|
* ```
|
|
9
10
|
*/
|
|
10
11
|
/**
|
|
@@ -14,8 +15,31 @@
|
|
|
14
15
|
* @param {number} [threshold=50] - Minimum pixel distance to trigger a swipe
|
|
15
16
|
*/
|
|
16
17
|
declare function onSwipe(elem: HTMLElement, callback: Function, threshold?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Registers pointer event listeners on an element for interactive dragging (pan).
|
|
20
|
+
* The drag is only "claimed" once the pointer moves past a small threshold and
|
|
21
|
+
* horizontal movement dominates vertical movement, so vertical scrolling inside
|
|
22
|
+
* the element still works as expected.
|
|
23
|
+
*
|
|
24
|
+
* Callbacks:
|
|
25
|
+
* onStart({ x, y }) - drag claimed (horizontal intent confirmed)
|
|
26
|
+
* onMove({ deltaX, deltaY }) - pointer moved (called continuously while dragging)
|
|
27
|
+
* onEnd({ deltaX, velocity }) - pointer released; velocity in px/ms (negative = leftward)
|
|
28
|
+
* onCancel() - pointer cancelled
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLElement} elem
|
|
31
|
+
* @param {{ onStart?: Function, onMove?: Function, onEnd?: Function, onCancel?: Function }} handlers
|
|
32
|
+
* @param {number} [threshold=10] - Minimum pixel distance before the drag is claimed
|
|
33
|
+
*/
|
|
34
|
+
declare function onPan(elem: HTMLElement, handlers?: {
|
|
35
|
+
onStart?: Function;
|
|
36
|
+
onMove?: Function;
|
|
37
|
+
onEnd?: Function;
|
|
38
|
+
onCancel?: Function;
|
|
39
|
+
}, threshold?: number): () => void;
|
|
17
40
|
declare const _default: {
|
|
18
41
|
onSwipe: typeof onSwipe;
|
|
42
|
+
onPan: typeof onPan;
|
|
19
43
|
};
|
|
20
44
|
export default _default;
|
|
21
|
-
export { onSwipe };
|
|
45
|
+
export { onSwipe, onPan };
|
package/viewers/ImageViewer.vue
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<template>
|
|
3
3
|
<div ref="rootEl" @mousemove="onMouseMove" class="full-page" @keyup.esc.stop="onClose" @keyup.left.stop="onPrev" @keyup.right.stop="onNext" tabindex="1" :style="{ opacity: dismissDistance ? (200 - dismissDistance)/100 : 1 }">
|
|
4
4
|
<div class="image-layer" ref="imageContainer" @scroll="onScroll($event)">
|
|
5
|
-
<div class="image" v-for="(
|
|
6
|
-
<img :src="
|
|
7
|
-
<video v-if="getFileTypeGroup(
|
|
8
|
-
<source :src="
|
|
5
|
+
<div class="image" v-for="(imageEntry, index) in entries" :key="imageEntry.id" :id="`image${index}`" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" :style="{ marginTop: dismissDistance ? (dismissDistance + 'px') : 0 }">
|
|
6
|
+
<img :src="imageEntry.fullFileUrl" v-if="getFileTypeGroup(imageEntry) === 'image'" draggable="false" @load="onImageLoad(index)"/>
|
|
7
|
+
<video v-if="getFileTypeGroup(imageEntry) !== 'image'" :ref="addVideoRef" controls>
|
|
8
|
+
<source :src="imageEntry.fullFileUrl" />
|
|
9
9
|
</video>
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<script setup>
|
|
21
21
|
|
|
22
|
-
import { ref, computed, onBeforeUpdate,
|
|
22
|
+
import { ref, computed, onBeforeUpdate, useTemplateRef } from 'vue';
|
|
23
23
|
import { getFileTypeGroup } from '../utils.js';
|
|
24
24
|
import Button from '../components/Button.vue';
|
|
25
25
|
|
|
@@ -129,6 +129,18 @@ function onTouchEnd() {
|
|
|
129
129
|
dismissDistance.value = 0;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function scrollToCurrent(behavior) {
|
|
133
|
+
const el = document.getElementById(`image${curIndex.value}`);
|
|
134
|
+
if (el && imageContainer.value) imageContainer.value.scrollTo({ left: el.offsetLeft, behavior });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function onImageLoad(index) {
|
|
138
|
+
if (lockScroll.value && index === curIndex.value) {
|
|
139
|
+
scrollToCurrent('auto');
|
|
140
|
+
lockScroll.value = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
function open(e, fileList, instant = true) {
|
|
133
145
|
if (!e || e.isDirectory || !canHandle(e)) return;
|
|
134
146
|
|
|
@@ -139,8 +151,8 @@ function open(e, fileList, instant = true) {
|
|
|
139
151
|
curIndex.value = fileList.findIndex((item) => item.fileName === e.fileName);
|
|
140
152
|
|
|
141
153
|
setTimeout(() => {
|
|
142
|
-
|
|
143
|
-
rootEl.value.focus();
|
|
154
|
+
scrollToCurrent(instant ? 'auto' : 'smooth');
|
|
155
|
+
rootEl.value.focus({ preventScroll: true });
|
|
144
156
|
}, 0);
|
|
145
157
|
|
|
146
158
|
setTimeout(() => {
|
|
@@ -156,7 +168,7 @@ function onDownload() {
|
|
|
156
168
|
function onClose() {
|
|
157
169
|
emit('close');
|
|
158
170
|
|
|
159
|
-
if (videoElements.value && videoElements.value.length) videoElements.value.forEach(e => e.pause());
|
|
171
|
+
if (videoElements.value && videoElements.value.length) videoElements.value.forEach((e) => { e.pause(); });
|
|
160
172
|
|
|
161
173
|
setTimeout(() => {
|
|
162
174
|
entry.value = {};
|
|
@@ -218,12 +230,19 @@ defineExpose({ canHandle, open });
|
|
|
218
230
|
|
|
219
231
|
.image-layer {
|
|
220
232
|
display: flex;
|
|
221
|
-
position:
|
|
233
|
+
position: absolute;
|
|
234
|
+
top: 0;
|
|
235
|
+
left: 0;
|
|
222
236
|
height: 100%;
|
|
223
237
|
width: 100%;
|
|
224
238
|
overflow-y: hidden;
|
|
225
239
|
overflow-x: auto;
|
|
226
240
|
scroll-snap-type: x mandatory;
|
|
241
|
+
scrollbar-width: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.image-layer::-webkit-scrollbar {
|
|
245
|
+
display: none;
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
.image {
|
|
@@ -237,6 +256,7 @@ defineExpose({ canHandle, open });
|
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
img, video {
|
|
259
|
+
display: block;
|
|
240
260
|
object-fit: contain;
|
|
241
261
|
width: 100%;
|
|
242
262
|
height: 100%;
|