@cloudron/pankow 3.2.0 → 3.2.2

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.
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
 
3
- import { ref, useTemplateRef } from 'vue';
3
+ import { ref, useTemplateRef, nextTick } from 'vue';
4
4
  import Button from './Button.vue';
5
5
  import Icon from './Icon.vue';
6
6
 
@@ -15,10 +15,6 @@ const props = defineProps({
15
15
  type: Number,
16
16
  default: 2000
17
17
  },
18
- modal: {
19
- type: Boolean,
20
- default: false
21
- },
22
18
  showX: {
23
19
  type: Boolean,
24
20
  default: false
@@ -52,14 +48,13 @@ const visible = ref(false);
52
48
  const dialog = useTemplateRef('dialog');
53
49
  const dialogBody = useTemplateRef('dialogBody');
54
50
 
55
- function open() {
51
+ async function open() {
56
52
  visible.value = true;
57
53
 
58
54
  // try to focus some form elements else just the dialog
59
- setTimeout(() => {
60
- const focusElement = dialogBody.value.querySelector('input:not([disabled]):not([style*="display:none"]):not([style*="display: none"])') || dialog.value;
61
- focusElement.focus();
62
- }, 100);
55
+ await nextTick();
56
+ const focusElement = dialogBody.value.querySelector('input:not([disabled]):not([style*="display:none"]):not([style*="display: none"])') || dialog.value;
57
+ focusElement.focus();
63
58
  }
64
59
 
65
60
  function close() {
@@ -71,7 +66,6 @@ function onConfirm() {
71
66
  }
72
67
 
73
68
  function onDismiss() {
74
- if (props.modal) return;
75
69
  onReject();
76
70
  }
77
71
 
@@ -100,7 +94,7 @@ defineExpose({ open, close });
100
94
  <div class="pankow-dialog-backdrop" @click="onDismiss" v-show="visible" :style="{ 'z-index': zIndex }"></div>
101
95
  </Transition>
102
96
  <Transition name="pankow-bounce-center-top">
103
- <div ref="dialog" class="pankow-dialog" :class="{ 'pankow-dialog-center': center }" @click.stop v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="{ 'z-index': zIndex+1 }">
97
+ <div ref="dialog" class="pankow-dialog" v-bind="$attrs" :class="{ 'pankow-dialog-center': center }" @click.stop v-show="visible" @keydown.esc="onDismiss" tabindex="0" :style="{ 'z-index': zIndex+1 }">
104
98
  <div class="pankow-dialog-header" v-show="title">
105
99
  {{ title }}
106
100
  <Icon v-show="showX" icon="fa-solid fa-xmark" style="cursor: pointer;" @click="onReject"/>
@@ -15,7 +15,6 @@ const confirmLabel = ref('Yes');
15
15
  const rejectLabel = ref('No');
16
16
  const rejectStyle = ref('');
17
17
  const isPrompt = ref(false);
18
- const isModal = ref(true);
19
18
  const isMultiValue = ref(false);
20
19
  const value = ref([]);
21
20
  const required = ref([]);
@@ -46,7 +45,6 @@ function reset() {
46
45
  rejectLabel.value = 'No';
47
46
  rejectStyle.value = '';
48
47
  isPrompt.value = false;
49
- isModal.value = true;
50
48
  isMultiValue.value = false;
51
49
  }
52
50
 
@@ -62,9 +60,6 @@ function info(options) {
62
60
  if (options.confirmStyle) confirmStyle.value = options.confirmStyle;
63
61
  rejectLabel.value = '';
64
62
 
65
- if ('modal' in options) isModal.value = options.modal;
66
- else isModal.value = true;
67
-
68
63
  dialog.value.open();
69
64
 
70
65
  return new Promise((resolve, reject) => {
@@ -86,9 +81,6 @@ function confirm(options) {
86
81
  if (options.confirmStyle) confirmStyle.value = options.confirmStyle;
87
82
  if (options.rejectStyle) rejectStyle.value = options.rejectStyle;
88
83
 
89
- if ('modal' in options) isModal.value = options.modal;
90
- else isModal.value = true;
91
-
92
84
  dialog.value.open();
93
85
 
94
86
  return new Promise((resolve, reject) => {
@@ -115,9 +107,6 @@ function prompt(options) {
115
107
  if (options.confirmStyle) confirmStyle.value = options.confirmStyle;
116
108
  if (options.rejectStyle) rejectStyle.value = options.rejectStyle;
117
109
 
118
- if ('modal' in options) isModal.value = options.modal;
119
- else isModal.value = true;
120
-
121
110
  dialog.value.open();
122
111
 
123
112
  setTimeout(() => { document.getElementById(internalId.value+0).focus(); }, 100);
@@ -149,14 +138,14 @@ defineExpose({
149
138
  </script>
150
139
 
151
140
  <template>
152
- <Dialog ref="dialog" :title="title" :modal="isModal" :confirm-active="isFormValid" :confirm-label="confirmLabel" :rejectLabel="rejectLabel" :rejectStyle="rejectStyle" :confirmStyle="confirmStyle" @confirm="onConfirmed()" @close="onRejected()" :z-index="2003">
153
- <div v-show="!isPrompt" v-html="message"></div>
154
- <div v-show="isPrompt" class="pankow-input-dialog-prompt" v-for="(msg, index) in message">
141
+ <Dialog ref="dialog" :title="title" :confirm-active="isFormValid" :confirm-label="confirmLabel" :rejectLabel="rejectLabel" :rejectStyle="rejectStyle" :confirmStyle="confirmStyle" @confirm="onConfirmed()" @close="onRejected()" :z-index="2003">
142
+ <div v-if="isPrompt" class="pankow-input-dialog-prompt" v-for="(msg, index) in message">
155
143
  <label :for="internalId+index" v-html="msg"></label>
156
144
  <PasswordInput :id="internalId+index" v-if="type[index] === 'password'" v-model="value[index]" :placeholder="placeholder[index]" @keydown.enter="onConfirmed()"/>
157
145
  <NumberInput :id="internalId+index" v-else-if="type[index] === 'number'" v-model="value[index]" :placeholder="placeholder[index]" @keydown.enter="onConfirmed()"/>
158
146
  <TextInput :id="internalId+index" v-else v-model="value[index]" :placeholder="placeholder[index]" @keydown.enter="onConfirmed()"/>
159
147
  </div>
148
+ <div v-else v-html="message"></div>
160
149
  </Dialog>
161
150
  </template>
162
151
 
@@ -71,8 +71,9 @@ const props = defineProps({
71
71
  const openEventTimeStamp = ref(0);
72
72
  const forElement = ref(null);
73
73
  const isOpen = ref(false);
74
- const pageX = ref(0);
75
- const pageY = ref(0);
74
+ let pageX = 0;
75
+ let targetBottom = 0;
76
+ let targetTop = 0;
76
77
  const offsetY = ref(0);
77
78
  const rollUp = ref(false);
78
79
  const searchString = ref('');
@@ -115,8 +116,9 @@ function onItemActivated(item) {
115
116
 
116
117
  async function open(event, element = null) {
117
118
  isOpen.value = true;
118
- pageX.value = element ? element.getBoundingClientRect().left : event.pageX;
119
- pageY.value = element ? element.getBoundingClientRect().bottom : event.pageY;
119
+ pageX = element ? element.getBoundingClientRect().left : event.pageX;
120
+ targetBottom = element ? element.getBoundingClientRect().bottom : event.pageY;
121
+ targetTop = element ? element.getBoundingClientRect().top : event.pageY;
120
122
  offsetY.value = element ? (element.getBoundingClientRect().height + 2) : 0; // offset in case we roll up
121
123
  forElement.value = element;
122
124
 
@@ -183,6 +185,9 @@ function onBackdrop(event) {
183
185
 
184
186
  function close() {
185
187
  isOpen.value = false;
188
+ container.value.style.maxHeight = 'unset';
189
+ container.value.style.bottom = 'unset';
190
+ container.value.style.top = 'unset';
186
191
  container.value = null;
187
192
  emit('close');
188
193
  }
@@ -192,12 +197,14 @@ function position() {
192
197
 
193
198
  const size = getHiddenElementSize(container.value);
194
199
 
195
- let left = pageX.value + 1;
196
- let top = pageY.value + 1;
200
+ let left = pageX;
201
+ let top = targetBottom + 1;
197
202
  let width = container.value.offsetParent ? container.value.offsetWidth : size.width;
198
203
  let height = container.value.offsetParent ? container.value.offsetHeight : size.height;
199
204
  let viewport = getViewport();
200
205
 
206
+ let bottom = viewport.height - targetTop + 1;
207
+
201
208
  //flip
202
209
  if (left + width - document.body.scrollLeft > viewport.width) {
203
210
  // if this is like a dropdown right align instead of flip
@@ -210,8 +217,11 @@ function position() {
210
217
 
211
218
  //flip
212
219
  if (top + height - document.body.scrollTop > viewport.height) {
213
- top -= height + offsetY.value;
214
- rollUp.value = true;
220
+ if (top - document.body.scrollTop > viewport.height/2) {
221
+ rollUp.value = true;
222
+ } else {
223
+ rollUp.value = false;
224
+ }
215
225
  } else {
216
226
  rollUp.value = false;
217
227
  }
@@ -221,13 +231,17 @@ function position() {
221
231
  left = document.body.scrollLeft;
222
232
  }
223
233
 
224
- //fit
225
- if (top < document.body.scrollTop) {
226
- top = document.body.scrollTop;
227
- }
228
-
229
234
  container.value.style.left = left + 'px';
230
- container.value.style.top = top + 'px';
235
+ if (rollUp.value) {
236
+ container.value.style.top = 'unset';
237
+ container.value.style.bottom = bottom + 'px';
238
+ container.value.style.maxHeight = viewport.height - bottom + 'px';
239
+ container.value.style.height = 'auto';
240
+ } else {
241
+ container.value.style.top = top + 'px';
242
+ container.value.style.bottom = 'unset';
243
+ container.value.style.maxHeight = viewport.height - top + 'px';
244
+ }
231
245
  }
232
246
 
233
247
  defineExpose({
@@ -271,7 +285,7 @@ defineExpose({
271
285
  z-index: 3001;
272
286
  color: var(--pankow-color-dark);
273
287
  overflow: auto;
274
- max-height: 100%;
288
+ /*max-height: 100%;*/
275
289
  outline: none;
276
290
  }
277
291
 
@@ -54,7 +54,7 @@ function onActivated() {
54
54
  display: flex;
55
55
  align-items: center;
56
56
  user-select: none;
57
- padding: 8px 12px;
57
+ padding: 6px 10px;
58
58
  cursor: pointer;
59
59
  font-size: 14px; /* this also defines the overall widget size as all sizes are in rem */
60
60
  font-family: var(--font-family);
package/gallery/Index.vue CHANGED
@@ -1,3 +1,326 @@
1
+ <script setup>
2
+
3
+ import { ref, useTemplateRef, onMounted } from 'vue';
4
+
5
+ import {
6
+ Button,
7
+ ButtonGroup,
8
+ Dialog,
9
+ EmailInput,
10
+ InputDialog,
11
+ MainLayout,
12
+ Notification,
13
+ NumberInput,
14
+ TextInput,
15
+ PasswordInput,
16
+ Icon,
17
+ Breadcrumb,
18
+ Checkbox,
19
+ Radiobutton,
20
+ OfflineBanner,
21
+ Popover,
22
+ ProgressBar,
23
+ SideBar,
24
+ TabView,
25
+ TableView,
26
+ TagInput,
27
+ Menu,
28
+ SingleSelect,
29
+ MultiSelect,
30
+ Spinner,
31
+ Switch,
32
+ FormGroup,
33
+ InputGroup,
34
+ } from '../index.js';
35
+
36
+ import { prettyDate, prettyLongDate } from '../utils.js';
37
+
38
+ import DirectoryViewDemo from './DirectoryViewDemo.vue';
39
+ import CustomMenuItem from './CustomMenuItem.vue';
40
+
41
+ const dateTime = ref('2024-05-01T01:13');
42
+ const buttonTypeToggle = ref(false);
43
+ const largeDialog = ref(false);
44
+ const checkbox1Enabled = ref(true);
45
+ const checkbox2Enabled = ref(false);
46
+ const switch1 = ref(true);
47
+ const switch2 = ref(false);
48
+ const offlineBannerActive = ref(false);
49
+ const textInputValue = ref('');
50
+ const textInputErrorValue = ref('');
51
+ const textInputDisabledValue = ref('do not edit');
52
+ const passwordInputValue = ref('');
53
+ const passwordInputDisabledValue = ref('something');
54
+ const emailInputValue = ref('');
55
+ const numberInputValue = ref(0);
56
+ const numberInputDisabledValue = ref(1337);
57
+ const tagInputValue = ref([ 'pecan', 'walnut' ]);
58
+ const radiobutton = ref('one');
59
+ const progressValue = ref(44);
60
+
61
+ const breadcrumbHomeItem = {
62
+ label: 'Home',
63
+ icon: 'fa-solid fa-house',
64
+ route: '#'
65
+ };
66
+ const breadcrumbItems = [{
67
+ icon: '',
68
+ label: 'Cloudron',
69
+ action: () => { console.log('Breadcrumb Cloudron'); }
70
+ }, {
71
+ icon: 'fa-solid fa-user-astronaut',
72
+ label: 'Founders',
73
+ action: () => { console.log('Breadcrumb Founders'); }
74
+ }, {
75
+ icon: 'fa-regular fa-file-pdf',
76
+ label: 'Documents',
77
+ action: () => { console.log('Breadcrumb Documents'); }
78
+ }, {
79
+ icon: '',
80
+ label: 'Tax',
81
+ action: () => { console.log('Breadcrumb Tax'); }
82
+ }];
83
+ const singleselectModel = [{
84
+ display: 'Pizza',
85
+ id: 'pizza'
86
+ }, {
87
+ display: 'Pasta',
88
+ id: 'pasta'
89
+ }, {
90
+ display: 'Pizza',
91
+ id: 'pizza2'
92
+ }, {
93
+ display: 'Falafel',
94
+ id: 'falafel'
95
+ }, {
96
+ display: 'Menemen',
97
+ id: 'menemen'
98
+ },{
99
+ display: 'Pizza',
100
+ id: 'pizza11'
101
+ }, {
102
+ display: 'Pasta',
103
+ id: 'pasta11'
104
+ }, {
105
+ display: 'Pizza',
106
+ id: 'pizza211'
107
+ }, {
108
+ display: 'Falafel',
109
+ id: 'falafel11'
110
+ }, {
111
+ display: 'Menemen',
112
+ id: 'menemen11'
113
+ },{
114
+ display: 'Pizza',
115
+ id: 'pizza22'
116
+ }, {
117
+ display: 'Pasta',
118
+ id: 'pasta22'
119
+ }, {
120
+ display: 'Pizza',
121
+ id: 'pizza222'
122
+ }, {
123
+ display: 'Falafel',
124
+ id: 'falafel22'
125
+ }, {
126
+ display: 'Menemen',
127
+ id: 'menemen22'
128
+ }];
129
+ const singleselectValue = ref({
130
+ display: 'Pasta',
131
+ id: 'pasta'
132
+ });
133
+ const singleselectValueWithKey = ref('falafel');
134
+ const singleselectValueNoInit = ref('');
135
+ const multiselectModel = [{
136
+ display: 'Pizza',
137
+ id: 'pizza'
138
+ }, {
139
+ display: 'Pasta',
140
+ id: 'pasta'
141
+ }, {
142
+ separator: true,
143
+ }, {
144
+ display: 'Pizza',
145
+ id: 'pizza2'
146
+ }, {
147
+ display: 'Falafel',
148
+ id: 'falafel'
149
+ }, {
150
+ display: 'Menemen',
151
+ id: 'menemen'
152
+ }];
153
+ const multiselectValue = ref([]);
154
+ const multiselectValueWithCustomLabel = ref([]);
155
+ const multiselectValueWithOptionKey = ref([ 'falafel' ]);
156
+ const multiselectValueWithSelectAll = ref([]);
157
+ const menuModelItemsSingle = [{
158
+ label: 'Single Item',
159
+ action: () => { console.log('Single Menu Item'); }
160
+ }];
161
+ const menuModelItems = [{
162
+ label: 'Item 1',
163
+ action: () => { console.log('Menu Item 1 clicked'); }
164
+ }, {
165
+ label: 'Item 2',
166
+ action: () => { console.log('Menu Item 2 clicked'); }
167
+ }, {
168
+ separator: true
169
+ }, {
170
+ label: 'Item 3',
171
+ action: () => { console.log('Menu Item 3 clicked'); }
172
+ }];
173
+ const menuModelItemsWithIcons = [{
174
+ icon: 'fa-solid fa-circle-notch fa-spin',
175
+ label: 'Item 1',
176
+ action: () => { console.log('Menu Item 1 clicked'); }
177
+ }, {
178
+ icon: '',
179
+ label: 'Item 2',
180
+ checkbox: true,
181
+ action: () => { console.log('Menu Item 2 clicked'); }
182
+ }, {
183
+ separator: true
184
+ }, {
185
+ href: 'https://cloudron.io',
186
+ target: '_blank',
187
+ label: 'Item as link',
188
+ }, {
189
+ separator: true
190
+ }, {
191
+ icon: 'fa-regular fa-file-pdf',
192
+ label: 'Item 3',
193
+ action: () => { console.log('Menu Item 3 clicked'); }
194
+ }];
195
+ const menuModelWithDOMElements = [{
196
+ label: 'Normal Item',
197
+ action: () => { console.log('Normal Item clicked'); }
198
+ }, {
199
+ type: CustomMenuItem,
200
+ action: () => { console.log('Large Item clicked'); }
201
+ }, {
202
+ label: 'This is a link',
203
+ href: 'https://cloudron.io',
204
+ target: '_blank',
205
+ }];
206
+ const notificationPosition = ref('top-center');
207
+ const tableColumns = {
208
+ status: {
209
+ label: 'Status',
210
+ sort(a, b) {
211
+ return a.index < b.index ? -1 : 1;
212
+ }
213
+ },
214
+ filename: {
215
+ label: 'File name',
216
+ icon: 'fa-solid fa-lock',
217
+ sort: true,
218
+ },
219
+ extra: {
220
+ label: 'Extra',
221
+ hideMobile: true,
222
+ },
223
+ action: {
224
+ label: 'Actions',
225
+ sort: false
226
+ }
227
+ };
228
+ const tableModel = ref([]);
229
+ const tabs = {
230
+ tab0: 'Fruit',
231
+ tab1: 'Veggies',
232
+ tab2: 'Meat',
233
+ tab3: 'Drinks',
234
+ };
235
+
236
+ const myDialog = useTemplateRef('myDialog');
237
+ function onButtonClick() {
238
+ myDialog.value.open();
239
+ console.log('Button clicked');
240
+ }
241
+
242
+ function onPopover(popover, event, elem = null) {
243
+ popover.open(event, elem);
244
+ }
245
+
246
+ const myInputDialog = useTemplateRef('myInputDialog');
247
+ async function onAskForConfirmation() {
248
+ console.log('User answered with:', await myInputDialog.value.confirm({
249
+ message: 'Really delete this or that?',
250
+ confirmStyle: 'danger',
251
+ confirmLabel: 'yeah sure',
252
+ rejectLabel: 'hm no'
253
+ }) ? 'ok' : 'nay');
254
+ }
255
+
256
+ async function onAskForString() {
257
+ console.log('User answered with:', await myInputDialog.value.prompt({
258
+ message: 'New Filename',
259
+ value: 'newfile.txt',
260
+ confirmStyle: 'success',
261
+ confirmLabel: 'Save',
262
+ rejectLabel: 'Close'
263
+ }));
264
+ }
265
+
266
+ function onOpenMenu(menu, event, elem = null) {
267
+ menu.open(event, elem);
268
+ }
269
+
270
+ const sideBar = useTemplateRef('sideBar');
271
+ function onSideBarEntry() {
272
+ sideBar.value.close();
273
+ }
274
+
275
+ function onRaiseNotification(text, type) {
276
+ if (type) window.pankow.notify({ text, type });
277
+ else window.pankow.notify(text);
278
+ }
279
+
280
+ function onRaisePersistentNotification(text) {
281
+ window.pankow.notify({
282
+ text,
283
+ persistent: true
284
+ });
285
+ }
286
+
287
+ const tabview = useTemplateRef('tabview');
288
+ function onChangeTab(tab) {
289
+ tabview.value.open(tab);
290
+ }
291
+
292
+ function onSwitchChange(value) {
293
+ console.log('switch changed to', value);
294
+ }
295
+
296
+ onMounted(async () => {
297
+ console.log('Pankow widget gallery');
298
+
299
+ for (let i = 0; i < 100; ++i) {
300
+ tableModel.value.push({
301
+ status: {
302
+ label: i + ' some state',
303
+ index: i
304
+ },
305
+ filename: `Test file ${i}.txt`,
306
+ extra: 'Only visible on desktop',
307
+ action: 'foo'
308
+ });
309
+ }
310
+
311
+ document.getElementById('borderRadiusInput').addEventListener('change', (event) => {
312
+ let root = document.documentElement;
313
+ root.style.setProperty('--pankow-border-radius', event.target.value + 'px');
314
+ });
315
+
316
+ setTimeout(() => {
317
+ radiobutton.value = 'two';
318
+ singleselectValueWithKey.value = 'pizza';
319
+ }, 2000 );
320
+ });
321
+
322
+ </script>
323
+
1
324
  <template>
2
325
 
3
326
  <Notification :position="notificationPosition"/>
@@ -42,8 +365,8 @@
42
365
  </SideBar>
43
366
  <MainLayout>
44
367
  <template #dialogs>
45
- <Dialog ref="myDialog" title="My Dialog" :modal="modalIsDialog" :show-x="showXInDialog" rejectLabel="Done">
46
- This is a <b>{{ modalIsDialog ? 'modal' : 'normal' }}</b> dialog.
368
+ <Dialog ref="myDialog" title="My Dialog" :show-x="true" rejectLabel="Done">
369
+ This is a dialog.
47
370
  <Button @click="largeDialog = !largeDialog">Large/auto</Button>
48
371
  <div :style="{ width: largeDialog ? '720px' : 'auto', height: largeDialog ? '400px' : 'auto' }"></div>
49
372
  </Dialog>
@@ -73,8 +396,10 @@
73
396
  </div>
74
397
 
75
398
  <div>
76
- <h2>utils.prettyDate():</h2>
77
- <p><input type="datetime-local" v-model="dateTime"/> {{ prettyDate(dateTime) }}</p>
399
+ <h2>utils pretty functions:</h2>
400
+ <input type="datetime-local" v-model="dateTime"/>
401
+ <p>prettyDate(): {{ prettyDate(dateTime) }}</p>
402
+ <p>prettyLongDate(): {{ prettyLongDate(dateTime) }}</p>
78
403
  </div>
79
404
 
80
405
  <h2 id="button">Button</h2>
@@ -281,8 +606,7 @@
281
606
 
282
607
  <h2 id="dialog">Dialog</h2>
283
608
  <div style="display: flex; gap: 10px">
284
- <Button @click="onButtonClick(false)">Open Dialog</Button>
285
- <Button @click="onButtonClick(true)">Open Modal Dialog</Button>
609
+ <Button @click="onButtonClick()">Open Dialog</Button>
286
610
  <Button @click="onAskForConfirmation()">Confirm</Button>
287
611
  <Button @click="onAskForString()">Prompt</Button>
288
612
  </div>
@@ -408,330 +732,6 @@
408
732
 
409
733
  </template>
410
734
 
411
- <script>
412
-
413
- 'use strict';
414
-
415
- import {
416
- Button,
417
- ButtonGroup,
418
- Dialog,
419
- EmailInput,
420
- InputDialog,
421
- MainLayout,
422
- Notification,
423
- NumberInput,
424
- TextInput,
425
- PasswordInput,
426
- Icon,
427
- Breadcrumb,
428
- Checkbox,
429
- Radiobutton,
430
- OfflineBanner,
431
- Popover,
432
- ProgressBar,
433
- SideBar,
434
- TabView,
435
- TableView,
436
- TagInput,
437
- Menu,
438
- SingleSelect,
439
- MultiSelect,
440
- Spinner,
441
- Switch,
442
- FormGroup,
443
- InputGroup,
444
- } from '../index.js';
445
-
446
- import { prettyDate } from '../utils.js';
447
-
448
- import DirectoryViewDemo from './DirectoryViewDemo.vue';
449
- import CustomMenuItem from './CustomMenuItem.vue';
450
-
451
- export default {
452
- name: 'WidgetGallery',
453
- components: {
454
- CustomMenuItem,
455
- Spinner,
456
- Button,
457
- ButtonGroup,
458
- Dialog,
459
- DirectoryViewDemo,
460
- EmailInput,
461
- InputDialog,
462
- MainLayout,
463
- TextInput,
464
- PasswordInput,
465
- Icon,
466
- Breadcrumb,
467
- Checkbox,
468
- Radiobutton,
469
- OfflineBanner,
470
- Popover,
471
- ProgressBar,
472
- SideBar,
473
- Menu,
474
- MultiSelect,
475
- SingleSelect,
476
- Notification,
477
- NumberInput,
478
- TabView,
479
- TableView,
480
- TagInput,
481
- Switch,
482
- FormGroup,
483
- InputGroup,
484
- },
485
- data() {
486
- return {
487
- dateTime: '2024-05-01T01:13',
488
- buttonTypeToggle: false,
489
- largeDialog: false,
490
- modalIsDialog: false,
491
- showXInDialog: false,
492
- checkbox1Enabled: true,
493
- checkbox2Enabled: false,
494
- switch1: true,
495
- switch2: false,
496
- offlineBannerActive: false,
497
- textInputValue: '',
498
- textInputErrorValue: '',
499
- textInputDisabledValue: 'do not edit',
500
- passwordInputValue: '',
501
- passwordInputDisabledValue: 'something',
502
- emailInputValue: '',
503
- numberInputValue: 0,
504
- numberInputDisabledValue: 1337,
505
- tagInputValue: [ 'pecan', 'walnut' ],
506
- radiobutton: 'one',
507
- progressValue: 44,
508
- breadcrumbHomeItem: {
509
- label: 'Home',
510
- icon: 'fa-solid fa-house',
511
- route: '#'
512
- },
513
- breadcrumbItems: [{
514
- icon: '',
515
- label: 'Cloudron',
516
- action: () => { console.log('Breadcrumb Cloudron'); }
517
- }, {
518
- icon: 'fa-solid fa-user-astronaut',
519
- label: 'Founders',
520
- action: () => { console.log('Breadcrumb Founders'); }
521
- }, {
522
- icon: 'fa-regular fa-file-pdf',
523
- label: 'Documents',
524
- action: () => { console.log('Breadcrumb Documents'); }
525
- }, {
526
- icon: '',
527
- label: 'Tax',
528
- action: () => { console.log('Breadcrumb Tax'); }
529
- }],
530
- singleselectModel: [{
531
- display: 'Pizza',
532
- id: 'pizza'
533
- }, {
534
- display: 'Pasta',
535
- id: 'pasta'
536
- }, {
537
- display: 'Pizza',
538
- id: 'pizza2'
539
- }, {
540
- display: 'Falafel',
541
- id: 'falafel'
542
- }, {
543
- display: 'Menemen',
544
- id: 'menemen'
545
- }],
546
- singleselectValue: {
547
- display: 'Pasta',
548
- id: 'pasta'
549
- },
550
- singleselectValueWithKey: 'falafel',
551
- singleselectValueNoInit: '',
552
- multiselectModel: [{
553
- display: 'Pizza',
554
- id: 'pizza'
555
- }, {
556
- display: 'Pasta',
557
- id: 'pasta'
558
- }, {
559
- separator: true,
560
- }, {
561
- display: 'Pizza',
562
- id: 'pizza2'
563
- }, {
564
- display: 'Falafel',
565
- id: 'falafel'
566
- }, {
567
- display: 'Menemen',
568
- id: 'menemen'
569
- }],
570
- multiselectValue: [],
571
- multiselectValueWithCustomLabel: [],
572
- multiselectValueWithOptionKey: [ 'falafel' ],
573
- multiselectValueWithSelectAll: [],
574
- menuModelItemsSingle: [{
575
- label: 'Single Item',
576
- action: () => { console.log('Single Menu Item'); }
577
- }],
578
- menuModelItems: [{
579
- label: 'Item 1',
580
- action: () => { console.log('Menu Item 1 clicked'); }
581
- }, {
582
- label: 'Item 2',
583
- action: () => { console.log('Menu Item 2 clicked'); }
584
- }, {
585
- separator: true
586
- }, {
587
- label: 'Item 3',
588
- action: () => { console.log('Menu Item 3 clicked'); }
589
- }],
590
- menuModelItemsWithIcons: [{
591
- icon: 'fa-solid fa-circle-notch fa-spin',
592
- label: 'Item 1',
593
- action: () => { console.log('Menu Item 1 clicked'); }
594
- }, {
595
- icon: '',
596
- label: 'Item 2',
597
- checkbox: true,
598
- action: () => { console.log('Menu Item 2 clicked'); }
599
- }, {
600
- separator: true
601
- }, {
602
- href: 'https://cloudron.io',
603
- target: '_blank',
604
- label: 'Item as link',
605
- }, {
606
- separator: true
607
- }, {
608
- icon: 'fa-regular fa-file-pdf',
609
- label: 'Item 3',
610
- action: () => { console.log('Menu Item 3 clicked'); }
611
- }],
612
- menuModelWithDOMElements: [{
613
- label: 'Normal Item',
614
- action: () => { console.log('Normal Item clicked'); }
615
- }, {
616
- type: CustomMenuItem,
617
- action: () => { console.log('Large Item clicked'); }
618
- }, {
619
- label: 'This is a link',
620
- href: 'https://cloudron.io',
621
- target: '_blank',
622
- }],
623
- notificationPosition: 'top-center',
624
- tableColumns: {
625
- status: {
626
- label: 'Status',
627
- sort(a, b) {
628
- return a.index < b.index ? -1 : 1;
629
- }
630
- },
631
- filename: {
632
- label: 'File name',
633
- icon: 'fa-solid fa-lock',
634
- sort: true,
635
- },
636
- extra: {
637
- label: 'Extra',
638
- hideMobile: true,
639
- },
640
- action: {
641
- label: 'Actions',
642
- sort: false
643
- }
644
- },
645
- tableModel: [],
646
- tabs: {
647
- tab0: 'Fruit',
648
- tab1: 'Veggies',
649
- tab2: 'Meat',
650
- tab3: 'Drinks',
651
- }
652
- };
653
- },
654
- methods: {
655
- prettyDate,
656
- onButtonClick(modal) {
657
- this.modalIsDialog = modal;
658
- this.showXInDialog = !modal;
659
- this.$refs.myDialog.open();
660
- console.log('Button clicked');
661
- },
662
- onPopover(popover, event, elem = null) {
663
- popover.open(event, elem);
664
- },
665
- async onAskForConfirmation() {
666
- console.log('User answered with:', await this.$refs.myInputDialog.confirm({
667
- message: 'Really delete this or that?',
668
- confirmStyle: 'danger',
669
- confirmLabel: 'yeah sure',
670
- rejectLabel: 'hm no'
671
- }) ? 'ok' : 'nay');
672
- },
673
- async onAskForString() {
674
- console.log('User answered with:', await this.$refs.myInputDialog.prompt({
675
- message: 'New Filename',
676
- modal: false,
677
- value: 'newfile.txt',
678
- confirmStyle: 'success',
679
- confirmLabel: 'Save',
680
- rejectLabel: 'Close'
681
- }));
682
- },
683
- onOpenMenu(menu, event, elem = null) {
684
- menu.open(event, elem);
685
- },
686
- onSideBarEntry() {
687
- this.$refs.sideBar.close();
688
- },
689
- onRaiseNotification(text, type) {
690
- if (type) window.pankow.notify({ text, type });
691
- else window.pankow.notify(text);
692
- },
693
- onRaisePersistentNotification(text) {
694
- window.pankow.notify({
695
- text,
696
- persistent: true
697
- });
698
- },
699
- onChangeTab(tab) {
700
- this.$refs.tabview.open(tab);
701
- },
702
- onSwitchChange(value) {
703
- console.log('switch changed to', value);
704
- },
705
- },
706
- async mounted() {
707
- console.log('Pankow widget gallery');
708
-
709
- for (let i = 0; i < 100; ++i) {
710
- this.tableModel.push({
711
- status: {
712
- label: i + ' some state',
713
- index: i
714
- },
715
- filename: `Test file ${i}.txt`,
716
- extra: 'Only visible on desktop',
717
- action: 'foo'
718
- });
719
- }
720
-
721
- document.getElementById('borderRadiusInput').addEventListener('change', (event) => {
722
- let root = document.documentElement;
723
- root.style.setProperty('--pankow-border-radius', event.target.value + 'px');
724
- });
725
-
726
- setTimeout(() => {
727
- this.radiobutton = 'two';
728
- this.singleselectValueWithKey = 'pizza';
729
- }, 2000 );
730
- }
731
- };
732
-
733
- </script>
734
-
735
735
  <style>
736
736
 
737
737
  #app {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "3.2.0",
4
+ "version": "3.2.2",
5
5
  "description": "",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -14,13 +14,12 @@
14
14
  "dependencies": {
15
15
  "@fontsource/inter": "^5.2.6",
16
16
  "@fortawesome/fontawesome-free": "^6.7.2",
17
- "filesize": "^10.1.6",
18
- "moment": "^2.30.1",
17
+ "filesize": "^11.0.1",
19
18
  "monaco-editor": "^0.52.2"
20
19
  },
21
20
  "devDependencies": {
22
21
  "@vitejs/plugin-vue": "^6.0.0",
23
- "vite": "^7.0.3",
22
+ "vite": "^7.0.4",
24
23
  "vue": "^3.5.17"
25
24
  }
26
25
  }
package/utils.js CHANGED
@@ -1,7 +1,6 @@
1
1
 
2
2
  import { filesize } from 'filesize';
3
3
  import { customRef } from 'vue';
4
- import moment from 'moment';
5
4
 
6
5
  // https://vuejs.org/api/reactivity-advanced.html#customref
7
6
  function useDebouncedRef(value, delay = 300) {
@@ -69,17 +68,55 @@ function prettyDecimalSize(size, fallback) {
69
68
  return (size / Math.pow(1000, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
70
69
  }
71
70
 
71
+ function fromNow(date) {
72
+ const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
73
+
74
+ const now = new Date();
75
+ const diff = date - now;
76
+
77
+ const seconds = Math.round(diff / 1000);
78
+ const minutes = Math.round(diff / (1000 * 60));
79
+ const hours = Math.round(diff / (1000 * 60 * 60));
80
+ const days = Math.round(diff / (1000 * 60 * 60 * 24));
81
+ const weeks = Math.round(diff / (1000 * 60 * 60 * 24 * 7));
82
+ const months = Math.round(diff / (1000 * 60 * 60 * 24 * 30));
83
+ const years = Math.round(diff / (1000 * 60 * 60 * 24 * 365));
84
+
85
+ // Pick the best unit based on the magnitude
86
+ if (Math.abs(seconds) < 60) return rtf.format(seconds, 'second');
87
+ if (Math.abs(minutes) < 60) return rtf.format(minutes, 'minute');
88
+ if (Math.abs(hours) < 24) return rtf.format(hours, 'hour');
89
+ if (Math.abs(days) < 7) return rtf.format(days, 'day');
90
+ if (Math.abs(weeks) < 4) return rtf.format(weeks, 'week');
91
+ if (Math.abs(months) < 12) return rtf.format(months, 'month');
92
+
93
+ return rtf.format(years, 'year');
94
+ }
95
+
72
96
  // this will print a human friendly datetime offset from now
73
97
  function prettyDate(value) {
74
98
  if (!value) return 'never';
75
- return moment(value).fromNow();
99
+
100
+ const date = new Date(value);
101
+ if (isNaN(date.getTime())) return 'unkown';
102
+
103
+ return fromNow(date);
76
104
  }
77
105
 
78
106
  function prettyLongDate(value) {
79
107
  if (!value) return 'unknown';
80
108
 
81
109
  const date = new Date(value);
82
- return date.toLocaleString();
110
+ if (isNaN(date.getTime())) return 'unknown';
111
+
112
+ const formatter = new Intl.DateTimeFormat(undefined, {
113
+ dateStyle: 'long',
114
+ timeStyle: 'medium'
115
+ });
116
+
117
+ const formattedDate = formatter.format(date);
118
+
119
+ return formattedDate;
83
120
  }
84
121
 
85
122
  function prettyFileSize(value) {