@cloudron/pankow 4.2.2 → 4.3.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/components/ListItem.vue +188 -0
- package/components/MenuItem.vue +2 -2
- package/components/MenuItemLink.vue +1 -1
- package/components/Notification.vue +9 -61
- package/components/{Radiobutton.vue → RadioButton.vue} +12 -28
- package/components/SectionItem.vue +113 -0
- package/components/TabView.vue +26 -31
- package/fallbackImage.js +21 -17
- package/fetcher.js +47 -5
- package/gestures.js +15 -11
- package/index.js +19 -6
- package/notifyState.js +48 -0
- package/package.json +15 -3
- package/plugin.js +39 -0
- package/tooltip.js +34 -15
- package/types/fallbackImage.d.ts +5 -4
- package/types/fetcher.d.ts +60 -10
- package/types/gestures.d.ts +16 -1
- package/types/index.d.ts +4 -1
- package/types/notifyState.d.ts +8 -0
- package/types/plugin.d.ts +4 -0
- package/types/tooltip.d.ts +5 -8
- package/types/utils.d.ts +185 -25
- package/utils.js +154 -14
- package/viewers/GenericViewer.vue +20 -32
- package/viewers/ImageViewer.vue +150 -140
- package/viewers/PdfViewer.vue +27 -35
- package/viewers/TextViewer.vue +134 -136
- package/vite-plugin.js +16 -0
- /package/components/{Breadcrumb.vue → BreadCrumb.vue} +0 -0
- /package/components/{Checkbox.vue → CheckBox.vue} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
import { computed, useTemplateRef, ref } from 'vue';
|
|
4
|
+
import Menu from './Menu.vue';
|
|
5
|
+
import Button from './Button.vue';
|
|
6
|
+
import ButtonGroup from './ButtonGroup.vue';
|
|
7
|
+
import Icon from './Icon.vue';
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
label: String,
|
|
11
|
+
subtext: String,
|
|
12
|
+
href: String,
|
|
13
|
+
actions: {
|
|
14
|
+
type: Array,
|
|
15
|
+
default: () => [],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const quickActions = computed(() => {
|
|
20
|
+
if (window.innerWidth <= 576) return [];
|
|
21
|
+
|
|
22
|
+
const visibleActions = props.actions.filter(a => !(typeof a.visible !== 'undefined' && !a.visible) && !a.separator);
|
|
23
|
+
if (visibleActions.length <= 2) return visibleActions;
|
|
24
|
+
|
|
25
|
+
return visibleActions.filter(a => a.quickAction);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const visibleActionCount = computed(() => {
|
|
29
|
+
return props.actions.filter(a => !(typeof a.visible !== 'undefined' && !a.visible) && !a.separator).length;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const isMenuOpen = ref(false);
|
|
33
|
+
|
|
34
|
+
const menuElement = useTemplateRef('menuElement');
|
|
35
|
+
function onMenu(event) {
|
|
36
|
+
isMenuOpen.value = true;
|
|
37
|
+
menuElement.value.open(event, event.currentTarget);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<component
|
|
44
|
+
:is="href ? 'a' : 'div'"
|
|
45
|
+
:href="href"
|
|
46
|
+
class="pankow-list-item"
|
|
47
|
+
:class="{ 'pankow-list-item-link': href }"
|
|
48
|
+
>
|
|
49
|
+
<div class="pankow-list-item-left">
|
|
50
|
+
<slot name="left" />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="pankow-list-item-center">
|
|
53
|
+
<div class="pankow-list-item-label">{{ label }}</div>
|
|
54
|
+
<div v-if="subtext" class="pankow-list-item-subtext">{{ subtext }}</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="pankow-list-item-right">
|
|
57
|
+
<Icon v-if="href" icon="fa-solid fa-chevron-right" class="pankow-list-item-chevron" />
|
|
58
|
+
<div v-else-if="visibleActionCount > 0" class="pankow-list-item-action-bar" :class="{ 'is-menu-open': isMenuOpen }">
|
|
59
|
+
<Menu ref="menuElement" :model="actions" @close="isMenuOpen = false" />
|
|
60
|
+
<ButtonGroup class="pankow-list-item-action-bar-quick-actions">
|
|
61
|
+
<Button tool v-for="quickAction in quickActions" :key="quickAction" :icon="quickAction.icon" @click="quickAction.action && quickAction.action()" :href="quickAction.href || null" :target="quickAction.target || null" v-tooltip.top="quickAction.label"/>
|
|
62
|
+
<Button tool @click.capture="onMenu($event)" icon="fa-solid fa-ellipsis" v-if="visibleActionCount > 0 && visibleActionCount !== quickActions.length"/>
|
|
63
|
+
</ButtonGroup>
|
|
64
|
+
<Button tool :plain="isMenuOpen ? null : true" secondary @click.capture="onMenu($event)" icon="fa-solid fa-ellipsis" v-if="visibleActionCount > 0" class="pankow-list-item-action-bar-menu-trigger"/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</component>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
|
|
72
|
+
.pankow-list-item {
|
|
73
|
+
display: flex;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 10px;
|
|
77
|
+
padding: 12px 16px;
|
|
78
|
+
background: var(--pankow-body-background-color);
|
|
79
|
+
border-radius: var(--pankow-border-radius);
|
|
80
|
+
color: inherit;
|
|
81
|
+
transition: background-color 0.15s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pankow-list-item-link {
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.pankow-list-item:hover {
|
|
90
|
+
background: var(--pankow-color-background-hover);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.pankow-list-item-left {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.pankow-list-item-center {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
flex: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.pankow-list-item-label {
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
font-weight: var(--pankow-font-weight-bold);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.pankow-list-item-subtext {
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
color: var(--pankow-color-text-secondary);
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pankow-list-item-right {
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.pankow-list-item-chevron {
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
color: #999;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.pankow-list-item-action-bar {
|
|
130
|
+
display: flex;
|
|
131
|
+
gap: 5px;
|
|
132
|
+
justify-content: end;
|
|
133
|
+
min-height: 31px;
|
|
134
|
+
align-items: center;
|
|
135
|
+
min-width: 55px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.pankow-list-item-action-bar-menu-trigger {
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.pankow-list-item-action-bar-quick-actions {
|
|
143
|
+
display: flex;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.pankow-list-item-action-bar .pankow-list-item-action-bar-quick-actions .pankow-button {
|
|
147
|
+
background-color: var(--pankow-input-background-color);
|
|
148
|
+
color: var(--pankow-color-text);
|
|
149
|
+
border: 1px solid transparent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.pankow-list-item-action-bar .pankow-list-item-action-bar-quick-actions .pankow-button:hover {
|
|
153
|
+
color: var(--pankow-color-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (hover: hover) {
|
|
157
|
+
.pankow-list-item-action-bar-quick-actions {
|
|
158
|
+
display: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.pankow-list-item-action-bar-menu-trigger {
|
|
162
|
+
display: flex;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.pankow-list-item:hover .pankow-list-item-action-bar-quick-actions {
|
|
166
|
+
display: flex;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.pankow-list-item:hover .pankow-list-item-action-bar-menu-trigger {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (hover: none) {
|
|
175
|
+
.pankow-list-item-action-bar-quick-actions {
|
|
176
|
+
display: flex;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.pankow-list-item-action-bar-menu-trigger {
|
|
180
|
+
display: none;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.pankow-list-item-action-bar.is-menu-open .pankow-list-item-action-bar-menu-trigger {
|
|
185
|
+
display: block !important;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
</style>
|
package/components/MenuItem.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
|
|
3
3
|
import { computed } from 'vue';
|
|
4
|
-
import
|
|
4
|
+
import CheckBox from './CheckBox.vue';
|
|
5
5
|
import Icon from './Icon.vue';
|
|
6
6
|
|
|
7
7
|
const emit = defineEmits(['activated']);
|
|
@@ -43,7 +43,7 @@ function onActivated() {
|
|
|
43
43
|
|
|
44
44
|
<template>
|
|
45
45
|
<div class="pankow-menu-item" :class="{'pankow-menu-item-disabled': isDisabled }" @click.stop="onActivated" :separator="item.separator" v-if="isVisible" tabindex="0" @keydown.enter.stop="onActivated" @keydown.space.stop="onActivated">
|
|
46
|
-
<
|
|
46
|
+
<CheckBox v-model="item.checked" v-if="item.checkbox" style="margin-right: 4px; display: inline-flex;"/>
|
|
47
47
|
<span v-if="!item.separator" :style="{ width: (hasIcons && !item.checkbox) ? '29px' : 0 }"><Icon v-show="hasIcons" :icon="item.icon"/></span>{{ item.label }}
|
|
48
48
|
<div class="pankow-menu-item-separator-line" v-if="item.separator"></div>
|
|
49
49
|
</div>
|
|
@@ -1,71 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<div
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<Icon @click="onClose(message.id)" icon="fa-solid fa-xmark" class="pankow-notification-close"/>
|
|
8
|
-
</div>
|
|
2
|
+
<TransitionGroup name="pankow-notification-fade" :class="`pankow-notification-container ${position}`" tag="div">
|
|
3
|
+
<div v-for="message in messages" :key="message.id">
|
|
4
|
+
<div class="pankow-notification" :[message.type]="''">
|
|
5
|
+
{{ message.text }}
|
|
6
|
+
<Icon @click="removeNotify(message.id)" icon="fa-solid fa-xmark" class="pankow-notification-close"/>
|
|
9
7
|
</div>
|
|
10
|
-
</
|
|
11
|
-
</
|
|
8
|
+
</div>
|
|
9
|
+
</TransitionGroup>
|
|
12
10
|
</template>
|
|
13
11
|
|
|
14
|
-
<script>
|
|
15
|
-
|
|
16
|
-
import { uuidv4 } from '../utils.js';
|
|
12
|
+
<script setup>
|
|
17
13
|
|
|
14
|
+
import { messages, position, removeNotify } from '../notifyState.js';
|
|
18
15
|
import Icon from './Icon.vue';
|
|
19
16
|
|
|
20
|
-
export default {
|
|
21
|
-
name: 'Notification',
|
|
22
|
-
components: {
|
|
23
|
-
Icon
|
|
24
|
-
},
|
|
25
|
-
props: {
|
|
26
|
-
position: {
|
|
27
|
-
type: String,
|
|
28
|
-
default: 'top-center',
|
|
29
|
-
validator(value, props) {
|
|
30
|
-
return [ 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right' ].includes(value);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
data() {
|
|
35
|
-
return {
|
|
36
|
-
messages: []
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
methods: {
|
|
40
|
-
raise(options) {
|
|
41
|
-
const id = uuidv4();
|
|
42
|
-
const text = typeof options === 'object' && 'text' in options ? options.text : options;
|
|
43
|
-
const persistent = typeof options === 'object' && 'persistent' in options ? options.persistent : false;
|
|
44
|
-
const timeout = typeof options === 'object' && 'timeout' in options ? options.timeout : 2000;
|
|
45
|
-
const type = typeof options === 'object' && 'type' in options ? options.type : null;
|
|
46
|
-
|
|
47
|
-
this.messages.push({ id, text, persistent, type });
|
|
48
|
-
|
|
49
|
-
if (!persistent) {
|
|
50
|
-
setTimeout(() => {
|
|
51
|
-
const index = this.messages.findIndex((value) => value.id === id);
|
|
52
|
-
if (index > -1) this.messages.splice(index, 1);
|
|
53
|
-
}, timeout);
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
onClose(id) {
|
|
57
|
-
const index = this.messages.findIndex((value) => value.id === id);
|
|
58
|
-
if (index > -1) this.messages.splice(index, 1);
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
mounted() {
|
|
62
|
-
if (!window.pankow) window.pankow = {};
|
|
63
|
-
if (window.pankow.notify) return console.error('Pankow: Notification DOM node already exists! Only ever use a single one.');
|
|
64
|
-
|
|
65
|
-
window.pankow.notify = (options) => { this.raise(options); }
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
17
|
</script>
|
|
70
18
|
|
|
71
19
|
<style>
|
|
@@ -7,37 +7,21 @@
|
|
|
7
7
|
</span>
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
|
-
<script>
|
|
10
|
+
<script setup>
|
|
11
11
|
|
|
12
12
|
import { uuidv4 } from '../utils.js';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
computed: {
|
|
26
|
-
internalValue: {
|
|
27
|
-
get() {
|
|
28
|
-
return this.modelValue
|
|
29
|
-
},
|
|
30
|
-
set(internalValue) {
|
|
31
|
-
this.$emit('update:modelValue', internalValue)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
data () {
|
|
36
|
-
return {
|
|
37
|
-
internalId: uuidv4()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
};
|
|
14
|
+
defineProps({
|
|
15
|
+
value: String,
|
|
16
|
+
label: String,
|
|
17
|
+
id: String,
|
|
18
|
+
name: String,
|
|
19
|
+
disabled: null
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const internalValue = defineModel({ type: String });
|
|
23
|
+
|
|
24
|
+
const internalId = uuidv4();
|
|
41
25
|
|
|
42
26
|
</script>
|
|
43
27
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
import { useTemplateRef, ref, onMounted, onUnmounted } from 'vue';
|
|
4
|
+
|
|
5
|
+
const mobileFilterBar = useTemplateRef('mobileFilterBar');
|
|
6
|
+
|
|
7
|
+
const isMobile = ref(false);
|
|
8
|
+
|
|
9
|
+
defineProps({
|
|
10
|
+
title: String,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function checkForMobile() {
|
|
14
|
+
isMobile.value = window.innerWidth <= 576;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
checkForMobile();
|
|
19
|
+
window.addEventListener('resize', checkForMobile);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
onUnmounted(() => {
|
|
23
|
+
window.removeEventListener('resize', checkForMobile);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="pankow-section-item">
|
|
30
|
+
<h2 class="pankow-section-item-header">
|
|
31
|
+
<div>
|
|
32
|
+
<div class="pankow-section-item-header-title-text">
|
|
33
|
+
<slot name="header-title">{{ title }}</slot>
|
|
34
|
+
<slot name="header-title-extra"></slot>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<Teleport :disabled="!isMobile" :to="mobileFilterBar">
|
|
39
|
+
<slot name="filter-bar"></slot>
|
|
40
|
+
</Teleport>
|
|
41
|
+
<slot name="header-buttons"></slot>
|
|
42
|
+
</div>
|
|
43
|
+
</h2>
|
|
44
|
+
<div class="pankow-section-item-mobile-filter-bar" v-show="isMobile && $slots['filter-bar']" ref="mobileFilterBar"></div>
|
|
45
|
+
<hr class="pankow-section-item-divider"/>
|
|
46
|
+
<div class="pankow-section-item-body">
|
|
47
|
+
<slot></slot>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
|
|
54
|
+
.pankow-section-item {
|
|
55
|
+
margin-bottom: 20px;
|
|
56
|
+
position: relative;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pankow-section-item-header {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-wrap: wrap;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 5px;
|
|
65
|
+
padding-left: 15px;
|
|
66
|
+
padding-right: 15px;
|
|
67
|
+
padding-top: 5px;
|
|
68
|
+
margin-top: 0;
|
|
69
|
+
margin-bottom: 0;
|
|
70
|
+
font-size: 24px;
|
|
71
|
+
font-weight: var(--pankow-font-weight-bold);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.pankow-section-item-header > div {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 6px;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.pankow-section-item-header-title-text {
|
|
82
|
+
display: flex;
|
|
83
|
+
gap: 6px;
|
|
84
|
+
align-items: baseline;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.pankow-section-item-divider {
|
|
88
|
+
border: 0;
|
|
89
|
+
border-top: 1px solid #d8dee4;
|
|
90
|
+
margin-top: 10px;
|
|
91
|
+
margin-bottom: 5px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (prefers-color-scheme: dark) {
|
|
95
|
+
.pankow-section-item-divider {
|
|
96
|
+
border-top-color: #495057;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.pankow-section-item-body {
|
|
101
|
+
position: relative;
|
|
102
|
+
margin-bottom: 15px;
|
|
103
|
+
padding: 10px 15px 25px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.pankow-section-item-mobile-filter-bar {
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: 6px;
|
|
109
|
+
flex-wrap: wrap;
|
|
110
|
+
margin: 10px 15px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
</style>
|
package/components/TabView.vue
CHANGED
|
@@ -14,39 +14,34 @@
|
|
|
14
14
|
</div>
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
|
-
<script>
|
|
18
|
-
|
|
19
|
-
export default {
|
|
20
|
-
name: 'TabView',
|
|
21
|
-
emits: [ 'changed' ],
|
|
22
|
-
props: {
|
|
23
|
-
tabs: {
|
|
24
|
-
type: Object,
|
|
25
|
-
default: {}
|
|
26
|
-
},
|
|
27
|
-
defaultActive: String,
|
|
28
|
-
tabPosition: {
|
|
29
|
-
type: String,
|
|
30
|
-
default: 'top',
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
data() {
|
|
34
|
-
return {
|
|
35
|
-
active: ''
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
mounted() {
|
|
39
|
-
this.active = this.defaultActive;
|
|
40
|
-
},
|
|
41
|
-
methods: {
|
|
42
|
-
open(key) {
|
|
43
|
-
if (this.active === key) return;
|
|
17
|
+
<script setup>
|
|
44
18
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
19
|
+
import { ref, onMounted } from 'vue';
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits([ 'changed' ]);
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
tabs: {
|
|
24
|
+
type: Object,
|
|
25
|
+
default: {}
|
|
26
|
+
},
|
|
27
|
+
defaultActive: String,
|
|
28
|
+
tabPosition: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: 'top',
|
|
48
31
|
}
|
|
49
|
-
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const active = ref('');
|
|
35
|
+
|
|
36
|
+
onMounted(() => {
|
|
37
|
+
active.value = props.defaultActive;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function open(key) {
|
|
41
|
+
if (active.value === key) return;
|
|
42
|
+
active.value = key;
|
|
43
|
+
emit('changed', key);
|
|
44
|
+
}
|
|
50
45
|
|
|
51
46
|
</script>
|
|
52
47
|
|
package/fallbackImage.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
import fallbackImage from 'pankow/fallbackImage';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
/**
|
|
2
|
+
* FallbackImage Directive
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```
|
|
6
|
+
* import fallbackImage from 'pankow/fallbackImage';
|
|
7
|
+
* app.directive('fallback-image', fallbackImage);
|
|
8
|
+
* // <img v-fallback-image="'/fallback.png'"/>
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Vue directive `mounted` hook. Sets fallback src on image load error.
|
|
14
|
+
* @param {HTMLImageElement} el
|
|
15
|
+
* @param {import('vue').DirectiveBinding} binding - binding.value is the fallback URL
|
|
16
|
+
* @param {import('vue').VNode} vnode
|
|
17
|
+
*/
|
|
18
18
|
function mounted(el, binding, vnode) {
|
|
19
19
|
el.addEventListener('error', () => {
|
|
20
20
|
console.log('image loading failed, try to load', binding.value);
|
|
@@ -22,6 +22,10 @@ function mounted(el, binding, vnode) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Vue directive for image fallback. Register via `app.directive('fallback-image', fallbackImage)`.
|
|
27
|
+
* @type {import('vue').Directive}
|
|
28
|
+
*/
|
|
25
29
|
const fallbackImage = {
|
|
26
30
|
mounted
|
|
27
31
|
};
|
package/fetcher.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
|
|
2
|
+
/**
|
|
3
|
+
* Shared configuration for all HTTP requests.
|
|
4
|
+
* @typedef {Object} FetcherGlobalOptions
|
|
5
|
+
* @property {string} credentials - Fetch credentials mode
|
|
6
|
+
* @property {string} redirect - Fetch redirect mode
|
|
7
|
+
* @property {?function(Response|Error): void} errorHook - Called on non-2xx responses or network errors
|
|
8
|
+
*/
|
|
9
|
+
|
|
2
10
|
const globalOptions = {
|
|
3
11
|
credentials: 'same-origin',
|
|
4
12
|
redirect: 'error',
|
|
@@ -16,6 +24,7 @@ const globalOptions = {
|
|
|
16
24
|
* @param {Object} query Plain key/value map serialized as the URL query string
|
|
17
25
|
* @param {Object|FormData|null} body
|
|
18
26
|
* @param {RequestInit} [options] Extra fetch options; `signal` is supported for cancellation
|
|
27
|
+
* @returns {Promise<{ status: number, body: any }>}
|
|
19
28
|
*/
|
|
20
29
|
async function request(uri, method, headers, query, body, options) {
|
|
21
30
|
let response, responseBody;
|
|
@@ -64,27 +73,60 @@ async function request(uri, method, headers, query, body, options) {
|
|
|
64
73
|
return { status: response.status, body: responseBody };
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Sends a HEAD request.
|
|
78
|
+
* @param {string} uri
|
|
79
|
+
* @param {Object} [query={}] - Query parameters serialized as URL query string
|
|
80
|
+
* @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
|
|
81
|
+
* @returns {Promise<{ status: number, body: * }>}
|
|
82
|
+
*/
|
|
68
83
|
async function head(uri, query = {}, options = {}) {
|
|
69
84
|
return await request(uri, 'HEAD', {}, query, null, options);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Sends a GET request.
|
|
89
|
+
* @param {string} uri
|
|
90
|
+
* @param {Object} [query={}] - Query parameters serialized as URL query string
|
|
91
|
+
* @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
|
|
92
|
+
* @returns {Promise<{ status: number, body: * }>}
|
|
93
|
+
*/
|
|
73
94
|
async function get(uri, query = {}, options = {}) {
|
|
74
95
|
return await request(uri, 'GET', {}, query, null, options);
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Sends a POST request with a JSON or FormData body.
|
|
100
|
+
* @param {string} uri
|
|
101
|
+
* @param {Object|FormData} body - Request body; objects are JSON-stringified
|
|
102
|
+
* @param {Object} [query={}] - Query parameters serialized as URL query string
|
|
103
|
+
* @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
|
|
104
|
+
* @returns {Promise<{ status: number, body: * }>}
|
|
105
|
+
*/
|
|
78
106
|
async function post(uri, body, query = {}, options = {}) {
|
|
79
107
|
return await request(uri, 'POST', {}, query, body, options);
|
|
80
108
|
}
|
|
81
109
|
|
|
82
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Sends a PUT request with a JSON or FormData body.
|
|
112
|
+
* @param {string} uri
|
|
113
|
+
* @param {Object|FormData} body - Request body; objects are JSON-stringified
|
|
114
|
+
* @param {Object} [query={}] - Query parameters serialized as URL query string
|
|
115
|
+
* @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
|
|
116
|
+
* @returns {Promise<{ status: number, body: * }>}
|
|
117
|
+
*/
|
|
83
118
|
async function put(uri, body, query = {}, options = {}) {
|
|
84
119
|
return await request(uri, 'PUT', {}, query, body, options);
|
|
85
120
|
}
|
|
86
121
|
|
|
87
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Sends a DELETE request with an optional body.
|
|
124
|
+
* @param {string} uri
|
|
125
|
+
* @param {Object|FormData} body - Request body; objects are JSON-stringified
|
|
126
|
+
* @param {Object} [query={}] - Query parameters serialized as URL query string
|
|
127
|
+
* @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
|
|
128
|
+
* @returns {Promise<{ status: number, body: * }>}
|
|
129
|
+
*/
|
|
88
130
|
async function del(uri, body, query = {}, options = {}) {
|
|
89
131
|
return await request(uri, 'DELETE', {}, query, body, options);
|
|
90
132
|
}
|
package/gestures.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Swipe handler
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```
|
|
6
|
+
* import { onSwipe } from 'pankow/gestures';
|
|
7
|
+
* onSwipe(element, (direction) => {});
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
2
10
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
*/
|
|
11
|
+
/**
|
|
12
|
+
* Registers touch event listeners on an element and calls the callback on swipe.
|
|
13
|
+
* @param {HTMLElement} elem
|
|
14
|
+
* @param {function('left'|'right'|'up'|'down'): void} callback
|
|
15
|
+
* @param {number} [threshold=50] - Minimum pixel distance to trigger a swipe
|
|
16
|
+
*/
|
|
13
17
|
function onSwipe(elem, callback, threshold = 50) {
|
|
14
18
|
callback = typeof callback === 'function' ? callback : () => {};
|
|
15
19
|
|