@cloudron/pankow 3.2.15 → 3.2.17

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.
@@ -0,0 +1,40 @@
1
+ <script setup>
2
+
3
+ import { computed } from 'vue';
4
+
5
+ const props = defineProps({
6
+ values: Array,
7
+ });
8
+
9
+ // const normalizedValue = computed(() => {
10
+ // if (props.value < 0) return 0;
11
+ // if (props.value > 100) return 100;
12
+ // return props.value;
13
+ // });
14
+
15
+ // we want 100 to be the circumverence so we can neatly use percentage: radius = 100 / ( 3,14159 * 2 ) = 15,9155
16
+ const radius = 15.9155;
17
+ const stroke = 10;
18
+
19
+ function calculateViewbox() {
20
+ return `0 0 ${radius*2+stroke} ${radius*2+stroke}`;
21
+ }
22
+
23
+ function calculateArcPath() {
24
+ return `M${radius+stroke/2} ${stroke/2} a ${radius} ${radius} 0 0 1 0 ${radius*2} a ${radius} ${radius} 0 0 1 0 -${radius*2}`;
25
+ }
26
+
27
+ </script>
28
+
29
+ <template>
30
+ <div class="pankow-circle-chart">
31
+ <svg :viewBox="calculateViewbox()" xmlns="http://www.w3.org/2000/svg">
32
+ <path :d="calculateArcPath()" fill="none" stroke="red" :stroke-width="stroke" stroke-dasharray="75, 100" @click.stop="onClick('red')" />
33
+ <path :d="calculateArcPath()" fill="none" stroke="green" :stroke-width="stroke" stroke-dasharray="30, 100" @click.stop="onClick('green') "/>
34
+ </svg>
35
+ </div>
36
+ </template>
37
+
38
+ <style>
39
+
40
+ </style>
@@ -109,7 +109,7 @@ defineExpose({ open, close });
109
109
  <div class="pankow-dialog-backdrop" @click="onDismiss" v-show="visible" :style="{ 'z-index': zIndex }"></div>
110
110
  </Transition>
111
111
  <Transition name="pankow-bounce-center-top">
112
- <div ref="dialog" class="pankow-dialog" v-bind="$attrs" :class="{ 'pankow-dialog-center': center }" v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="mergedStyle">
112
+ <div ref="dialog" class="pankow-dialog" v-bind="$attrs" :class="{ 'pankow-dialog-center': center }" v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="mergedStyle">
113
113
  <div class="pankow-dialog-header" v-show="title">
114
114
  {{ title }}
115
115
  <Icon v-show="showX" icon="fa-solid fa-xmark" style="cursor: pointer;" @click="onReject"/>
@@ -167,31 +167,31 @@ defineExpose({ open, close });
167
167
 
168
168
  .pankow-dialog-header {
169
169
  font-size: 20px;
170
- padding: 20px;
170
+ padding: 15px;
171
171
  display: flex;
172
172
  justify-content: space-between;
173
173
  }
174
174
 
175
175
  .pankow-dialog-body {
176
- padding: 0 20px;
176
+ padding: 0 15px;
177
177
  overflow: auto;
178
178
  transition: all 250ms;
179
179
  }
180
180
 
181
181
  .pankow-dialog-body-no-header {
182
- padding: 20px;
182
+ padding: 15px;
183
183
  padding-bottom: 0;
184
184
  }
185
185
 
186
186
  .pankow-dialog-body-no-buttons {
187
- padding-bottom: 20px;
187
+ padding-bottom: 15px;
188
188
  }
189
189
 
190
190
  .pankow-dialog-buttons {
191
191
  display: flex;
192
192
  gap: 6px;
193
- padding: 20px;
193
+ padding: 15px;
194
194
  text-align: right;
195
195
  }
196
196
 
197
- </style>
197
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
 
3
- import { nextTick, useTemplateRef, computed, ref, onUnmounted } from 'vue';
3
+ import { nextTick, useTemplateRef, computed, ref } from 'vue';
4
4
 
5
5
  import MenuItem from './MenuItem.vue';
6
6
  import MenuItemLink from './MenuItemLink.vue';
@@ -70,6 +70,7 @@ const props = defineProps({
70
70
 
71
71
  const openEventTimeStamp = ref(0);
72
72
  const forElement = ref(null);
73
+ let prevFocusElement = null;
73
74
  const isOpen = ref(false);
74
75
  let pageX = 0;
75
76
  let targetBottom = 0;
@@ -145,6 +146,7 @@ async function open(event, element = null) {
145
146
  targetTop = element ? element.getBoundingClientRect().top : event.pageY;
146
147
  offsetY.value = element ? (element.getBoundingClientRect().height + 2) : 0; // offset in case we roll up
147
148
  forElement.value = element;
149
+ prevFocusElement = document.activeElement;
148
150
 
149
151
  if (!container.value) return;
150
152
 
@@ -162,10 +164,11 @@ async function open(event, element = null) {
162
164
  if (event.type === 'keydown' && event.key === 'ArrowUp') selectUp();
163
165
  else if (event.type === 'keydown') selectDown();
164
166
  else container.value.focus();
167
+ }
165
168
 
166
- setTimeout(() => {
167
- window.document.addEventListener('click', blurEventHandler);
168
- }, 0);
169
+ function onBackdrop(event) {
170
+ close();
171
+ event.preventDefault();
169
172
  }
170
173
 
171
174
  function selectUp() {
@@ -209,9 +212,13 @@ function selectDown() {
209
212
  }
210
213
 
211
214
  function close() {
212
- window.document.removeEventListener('click', blurEventHandler);
213
215
  isOpen.value = false;
214
216
  container.value.style.maxHeight = 'unset';
217
+
218
+ // restore focus
219
+ if (prevFocusElement) prevFocusElement.focus();
220
+ prevFocusElement = null;
221
+
215
222
  emit('close');
216
223
  }
217
224
 
@@ -267,10 +274,6 @@ function position() {
267
274
  }
268
275
  }
269
276
 
270
- onUnmounted(() => {
271
- window.document.removeEventListener('click', blurEventHandler);
272
- });
273
-
274
277
  defineExpose({
275
278
  isOpen,
276
279
  open,
@@ -281,9 +284,10 @@ defineExpose({
281
284
 
282
285
  <template>
283
286
  <teleport to="#app">
287
+ <div class="pankow-menu-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
284
288
  <Transition :name="rollUp ? 'pankow-roll-up' : 'pankow-roll-down'">
285
289
  <div class="pankow-menu" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown">
286
- <TextInput placeholder="Filter ..." style="border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
290
+ <TextInput placeholder="Filter ..." @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.stop @keydown.esc.stop="close()" @click.stop style="border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
287
291
  <component v-for="item in visibleItems" ref="itemElements" :is="item.type || (item.href ? MenuItemLink : MenuItem)" @activated="onItemActivated(item)" :item="item" :has-icons="hasIcons" />
288
292
  <MenuItem v-if="model.length === 0" :item="emptyItem"/>
289
293
  </div>
@@ -293,6 +297,16 @@ defineExpose({
293
297
 
294
298
  <style>
295
299
 
300
+ .pankow-menu-backdrop {
301
+ position: fixed;
302
+ height: 100%;
303
+ width: 100%;
304
+ left: 0px;
305
+ top: 0px;
306
+ z-index: 3001;
307
+ background-color: transparent;
308
+ }
309
+
296
310
  .pankow-menu {
297
311
  position: fixed;
298
312
  box-shadow: var(--pankow-menu-shadow);
@@ -0,0 +1,336 @@
1
+ <script setup>
2
+
3
+ import { nextTick, useTemplateRef, computed, ref } from 'vue';
4
+
5
+ import MenuItem from './MenuItem.vue';
6
+ import MenuItemLink from './MenuItemLink.vue';
7
+ import TextInput from './TextInput.vue';
8
+
9
+ function getViewport() {
10
+ const win = window,
11
+ d = document,
12
+ e = d.documentElement,
13
+ g = d.getElementsByTagName('body')[0],
14
+ w = win.innerWidth || e.clientWidth || g.clientWidth,
15
+ h = win.innerHeight || e.clientHeight || g.clientHeight;
16
+
17
+ return {
18
+ width: w,
19
+ height: h
20
+ };
21
+ }
22
+
23
+ function getHiddenElementSize(element) {
24
+ if (element) {
25
+ const originalVisibility = element.style.visibility;
26
+ const originalDisplay = element.style.display;
27
+
28
+ element.style.visibility = 'hidden';
29
+ element.style.display = 'block';
30
+
31
+ element.clientHeight; // force reflow
32
+
33
+ const height = element.offsetHeight;
34
+ const width = element.offsetWidth;
35
+
36
+ element.style.display = originalDisplay;
37
+ element.style.visibility = originalVisibility;
38
+
39
+ return { height, width };
40
+ }
41
+ return { height: 0, width: 0 };
42
+ }
43
+
44
+
45
+ function getActiveElement(children) {
46
+ for (const child of children) {
47
+ if (child === document.activeElement) return child;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ // outsideClickListener: null,
53
+ const container = useTemplateRef('container');
54
+ const itemElements = useTemplateRef('itemElements');
55
+
56
+ const emit = defineEmits([ 'close' ]);
57
+
58
+ const props = defineProps({
59
+ icon: String,
60
+ model: Array,
61
+ closeOnActivation: {
62
+ type: Boolean,
63
+ default: true,
64
+ },
65
+ searchThreshold: {
66
+ type: Number,
67
+ default: Infinity,
68
+ }
69
+ });
70
+
71
+ const openEventTimeStamp = ref(0);
72
+ const forElement = ref(null);
73
+ const isOpen = ref(false);
74
+ let pageX = 0;
75
+ let targetBottom = 0;
76
+ let targetTop = 0;
77
+ const offsetY = ref(0);
78
+ const rollUp = ref(false);
79
+ const searchString = ref('');
80
+ const emptyItem = ref({
81
+ label: '',
82
+ disabled: true,
83
+ });
84
+
85
+ const hasIcons = computed(() => {
86
+ return !!props.model.find((item) => !!item.icon);
87
+ });
88
+
89
+ const visibleItems = computed(() => {
90
+ if (!searchString.value) return props.model;
91
+ const s = searchString.value.toLowerCase();
92
+ return props.model.filter((item) => item.label.toLowerCase().indexOf(s) !== -1);
93
+ });
94
+
95
+ // select menu item by .label
96
+ function select(elem) {
97
+ let next = container.value.children[0];
98
+ while (true) {
99
+ if (!next) return;
100
+
101
+ if (next.innerText !== elem.label) {
102
+ next = next.nextElementSibling;
103
+ continue;
104
+ }
105
+
106
+ next.focus();
107
+ break;
108
+ }
109
+ }
110
+
111
+ let search = '';
112
+ let debounceTimer = null;
113
+ function onKeyDown(event) {
114
+ // only handle alphanumerics
115
+ if (!/^[a-zA-Z0-9]$/.test(event.key)) return;
116
+
117
+ const key = event.key.toLowerCase();
118
+
119
+ if (debounceTimer) clearTimeout(debounceTimer);
120
+ debounceTimer = setTimeout(() => search = '', 500);
121
+
122
+ search += key;
123
+
124
+ const found = visibleItems.value.find(elem => {
125
+ if (!elem.label) return false;
126
+ return elem.label.toLowerCase().indexOf(search) === 0;
127
+ });
128
+
129
+ if (found) select(found)
130
+ }
131
+
132
+ function onItemActivated(item) {
133
+ if (item.action instanceof Function) item.action(item);
134
+ if (props.closeOnActivation) close();
135
+ }
136
+
137
+ async function open(event, element = null) {
138
+ isOpen.value = true;
139
+ pageX = element ? element.getBoundingClientRect().left : event.pageX;
140
+ targetBottom = element ? element.getBoundingClientRect().bottom : event.pageY;
141
+ targetTop = element ? element.getBoundingClientRect().top : event.pageY;
142
+ offsetY.value = element ? (element.getBoundingClientRect().height + 2) : 0; // offset in case we roll up
143
+ forElement.value = element;
144
+
145
+ if (!container.value) return;
146
+
147
+ if (element) container.value.style.minWidth = element.getBoundingClientRect().width + 'px';
148
+
149
+ await nextTick();
150
+
151
+ position();
152
+
153
+ openEventTimeStamp.value = event.timeStamp;
154
+
155
+ event.preventDefault();
156
+
157
+ // if opened from a key action, immediately select first
158
+ if (event.type === 'keydown' && event.key === 'ArrowUp') selectUp();
159
+ else if (event.type === 'keydown') selectDown();
160
+ else container.value.focus();
161
+ <<<<<<< HEAD
162
+
163
+ setTimeout(() => {
164
+ window.document.addEventListener('click', blurEventHandler);
165
+ }, 0);
166
+ =======
167
+ >>>>>>> parent of c42ecf7 (Use click events on document.body instead of a backdrop for menus)
168
+ }
169
+
170
+ function selectUp() {
171
+ if (!container.value.children[0]) return;
172
+
173
+ const active = getActiveElement(container.value.children);
174
+
175
+ if (!active) return container.value.lastElementChild.focus();
176
+
177
+ let prev = active;
178
+ while (true) {
179
+ prev = prev.previousElementSibling;
180
+ if (!prev) return;
181
+
182
+ if (prev.getAttribute('separator') === 'true') continue;
183
+ else if (prev.classList.contains('pankow-menu-item-disabled')) continue;
184
+
185
+ prev.focus();
186
+ break;
187
+ }
188
+ }
189
+
190
+ function selectDown() {
191
+ if (!container.value.children[0]) return;
192
+
193
+ const active = getActiveElement(container.value.children);
194
+
195
+ if (!active) return container.value.firstElementChild.focus();
196
+
197
+ let next = active;
198
+ while (true) {
199
+ next = next.nextElementSibling;
200
+ if (!next) return;
201
+
202
+ if (next.getAttribute('separator') === 'true') continue;
203
+ else if (next.classList.contains('pankow-menu-item-disabled')) continue;
204
+
205
+ next.focus();
206
+ break;
207
+ }
208
+ }
209
+
210
+ function onBackdrop(event) {
211
+ close();
212
+ event.preventDefault();
213
+ }
214
+
215
+ function close() {
216
+ <<<<<<< HEAD
217
+ window.document.removeEventListener('click', blurEventHandler);
218
+ =======
219
+ >>>>>>> parent of c42ecf7 (Use click events on document.body instead of a backdrop for menus)
220
+ isOpen.value = false;
221
+ container.value.style.maxHeight = 'unset';
222
+ emit('close');
223
+ }
224
+
225
+ function position() {
226
+ if (!container.value) return;
227
+
228
+ const size = getHiddenElementSize(container.value);
229
+
230
+ let left = pageX;
231
+ let top = targetBottom + 1;
232
+ let width = container.value.offsetParent ? container.value.offsetWidth : size.width;
233
+ let height = container.value.offsetParent ? container.value.offsetHeight : size.height;
234
+ let viewport = getViewport();
235
+
236
+ let bottom = viewport.height - targetTop + 1;
237
+
238
+ //flip
239
+ if (left + width - document.body.scrollLeft > viewport.width) {
240
+ // if this is like a dropdown right align instead of flip
241
+ if (forElement.value) {
242
+ left = forElement.value.getBoundingClientRect().left + (forElement.value.getBoundingClientRect().width - width);
243
+ } else {
244
+ left -= width;
245
+ }
246
+ }
247
+
248
+ //flip
249
+ if (top + height - document.body.scrollTop > viewport.height) {
250
+ if (top - document.body.scrollTop > viewport.height/2) {
251
+ rollUp.value = true;
252
+ } else {
253
+ rollUp.value = false;
254
+ }
255
+ } else {
256
+ rollUp.value = false;
257
+ }
258
+
259
+ //fit
260
+ if (left < document.body.scrollLeft) {
261
+ left = document.body.scrollLeft;
262
+ }
263
+
264
+ container.value.style.left = left + 'px';
265
+ if (rollUp.value) {
266
+ container.value.style.top = 'unset';
267
+ container.value.style.bottom = bottom + 'px';
268
+ container.value.style.maxHeight = viewport.height - bottom + 'px';
269
+ container.value.style.height = 'auto';
270
+ } else {
271
+ container.value.style.top = top + 'px';
272
+ container.value.style.bottom = 'unset';
273
+ container.value.style.maxHeight = viewport.height - top + 'px';
274
+ }
275
+ }
276
+
277
+ <<<<<<< HEAD
278
+ onUnmounted(() => {
279
+ window.document.removeEventListener('click', blurEventHandler);
280
+ });
281
+
282
+ =======
283
+ >>>>>>> parent of c42ecf7 (Use click events on document.body instead of a backdrop for menus)
284
+ defineExpose({
285
+ isOpen,
286
+ open,
287
+ close,
288
+ });
289
+
290
+ </script>
291
+
292
+ <template>
293
+ <teleport to="#app">
294
+ <div class="pankow-menu-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
295
+ <Transition :name="rollUp ? 'pankow-roll-up' : 'pankow-roll-down'">
296
+ <div class="pankow-menu" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown">
297
+ <TextInput placeholder="Filter ..." @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.stop @keydown.esc.stop="close()" @click.stop style="border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
298
+ <component v-for="item in visibleItems" ref="itemElements" :is="item.type || (item.href ? MenuItemLink : MenuItem)" @activated="onItemActivated(item)" :item="item" :has-icons="hasIcons" />
299
+ <MenuItem v-if="model.length === 0" :item="emptyItem"/>
300
+ </div>
301
+ </Transition>
302
+ </teleport>
303
+ </template>
304
+
305
+ <style>
306
+
307
+ .pankow-menu-backdrop {
308
+ position: fixed;
309
+ height: 100%;
310
+ width: 100%;
311
+ left: 0px;
312
+ top: 0px;
313
+ z-index: 3001;
314
+ background-color: transparent;
315
+ }
316
+
317
+ .pankow-menu {
318
+ position: fixed;
319
+ box-shadow: var(--pankow-menu-shadow);
320
+ border-radius: var(--pankow-border-radius);
321
+ background-color: var(--pankow-input-background-color);
322
+ min-width: 120px;
323
+ z-index: 3001;
324
+ color: var(--pankow-color-dark);
325
+ overflow: auto;
326
+ /*max-height: 100%;*/
327
+ outline: none;
328
+ }
329
+
330
+ @media (prefers-color-scheme: dark) {
331
+ .pankow-menu {
332
+ color: var(--pankow-color-light-dark);
333
+ }
334
+ }
335
+
336
+ </style>