@inriver/inri 1.0.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.
Files changed (114) hide show
  1. package/.eslintrc.js +5 -0
  2. package/componentProperties.d.ts +8 -0
  3. package/configs/base-eslint-config.js +36 -0
  4. package/configs/base-tailwind-config.js +149 -0
  5. package/i18n.ts +11 -0
  6. package/index.ts +25 -0
  7. package/package.json +36 -0
  8. package/src/components/CAutoBreadcrumbs.vue +72 -0
  9. package/src/components/CBtn.vue +98 -0
  10. package/src/components/CColorPicker.vue +55 -0
  11. package/src/components/CContextMenu.vue +29 -0
  12. package/src/components/CDatePicker.vue +51 -0
  13. package/src/components/CDeleteTileBtn.vue +52 -0
  14. package/src/components/CDialog.vue +253 -0
  15. package/src/components/CDialogCloseBtn.vue +9 -0
  16. package/src/components/CDirtyCheck.vue +68 -0
  17. package/src/components/CDrawer.vue +44 -0
  18. package/src/components/CDrawerButton.vue +91 -0
  19. package/src/components/CExpansionItem.vue +57 -0
  20. package/src/components/CExpansionItemWithCheckbox.vue +41 -0
  21. package/src/components/CFileDropzone.vue +114 -0
  22. package/src/components/CGrid.vue +101 -0
  23. package/src/components/CHeader.vue +10 -0
  24. package/src/components/CHeaderLinks.vue +61 -0
  25. package/src/components/CHeaderMenuLink.vue +51 -0
  26. package/src/components/CHeaderSearch.vue +142 -0
  27. package/src/components/CIconBtn.vue +70 -0
  28. package/src/components/CMenuItem.vue +66 -0
  29. package/src/components/CPageContainer.vue +11 -0
  30. package/src/components/CPageError.vue +24 -0
  31. package/src/components/CPageSpinner.vue +18 -0
  32. package/src/components/CSearch.vue +45 -0
  33. package/src/components/CSection.vue +29 -0
  34. package/src/components/CSectionTitle.vue +8 -0
  35. package/src/components/CSelect.vue +208 -0
  36. package/src/components/CSlideEffect.vue +62 -0
  37. package/src/components/CSpinner.vue +107 -0
  38. package/src/components/CTableActions.vue +16 -0
  39. package/src/components/CTileBtn.vue +178 -0
  40. package/src/components/CTimePicker.vue +42 -0
  41. package/src/components/CToggleGridView.vue +26 -0
  42. package/src/components/CToggleSidebarBtn.vue +19 -0
  43. package/src/components/CTooltip.vue +44 -0
  44. package/src/components/CTreeNode.vue +39 -0
  45. package/src/components/cards/CBannerCard.vue +103 -0
  46. package/src/components/cards/CSmallCard.vue +149 -0
  47. package/src/components/internal-components/ConfirmDialog.vue +78 -0
  48. package/src/composables/useDialog.ts +47 -0
  49. package/src/composables/useDirtyCheck.ts +25 -0
  50. package/src/composables/useDoubleClickListener.ts +28 -0
  51. package/src/composables/useSelectable.ts +62 -0
  52. package/src/composables/useTruncateChips.ts +46 -0
  53. package/src/i18n/de/index.ts +17 -0
  54. package/src/i18n/en/index.ts +84 -0
  55. package/src/i18n/es/index.ts +7 -0
  56. package/src/i18n/fr/index.ts +7 -0
  57. package/src/init/initInriFramework.ts +25 -0
  58. package/src/init/useInriApp.ts +55 -0
  59. package/src/layouts/CLayoutBlank.vue +7 -0
  60. package/src/layouts/CLayoutWithBackButton.vue +50 -0
  61. package/src/layouts/CLayoutWithSidebars.vue +46 -0
  62. package/src/layouts/layout-fragments/CPageAutoBreadcrumbs.vue +6 -0
  63. package/src/layouts/layout-fragments/CPageBackButton.vue +18 -0
  64. package/src/layouts/layout-fragments/CPageBlank.vue +20 -0
  65. package/src/layouts/layout-fragments/CPageFlexCol.vue +5 -0
  66. package/src/layouts/layout-fragments/CPageLeftSidebar.vue +5 -0
  67. package/src/layouts/layout-fragments/CPageMainContent.vue +5 -0
  68. package/src/layouts/layout-fragments/CPageOuterWrapper.vue +5 -0
  69. package/src/layouts/layout-fragments/CPageRightSidebar.vue +5 -0
  70. package/src/layouts/layout-fragments/CPageTitle.vue +34 -0
  71. package/src/plugins/dialog.ts +45 -0
  72. package/src/plugins/errors/ErrorFactories.ts +59 -0
  73. package/src/plugins/errors/Errors.ts +156 -0
  74. package/src/plugins/errors/Exception.ts +6 -0
  75. package/src/plugins/errors/globalErrorHandler.ts +21 -0
  76. package/src/plugins/errors/handleError.ts +34 -0
  77. package/src/plugins/errors/logger.ts +16 -0
  78. package/src/plugins/errors/vueErrorHandler.ts +26 -0
  79. package/src/plugins/globalAppConfig.ts +25 -0
  80. package/src/plugins/i18n.ts +46 -0
  81. package/src/plugins/inriIcons.ts +58 -0
  82. package/src/plugins/inriverQuasarComponentConfigurations.ts +67 -0
  83. package/src/plugins/library.ts +119 -0
  84. package/src/plugins/notify.ts +153 -0
  85. package/src/plugins/quasar-user-options.ts +19 -0
  86. package/src/plugins/validationRules.ts +292 -0
  87. package/src/shims-vue.d.ts +6 -0
  88. package/src/stores/inriStore.ts +54 -0
  89. package/src/stores/interfaceLanguageStore.ts +27 -0
  90. package/src/styles/_css-variables.scss +101 -0
  91. package/src/styles/_reset.scss +67 -0
  92. package/src/styles/_transitions.scss +9 -0
  93. package/src/styles/_utils.scss +15 -0
  94. package/src/styles/components/_breadcrumbs.scss +3 -0
  95. package/src/styles/components/_checkboxes.scss +17 -0
  96. package/src/styles/components/_chips.scss +17 -0
  97. package/src/styles/components/_forms.scss +203 -0
  98. package/src/styles/components/_menu.scss +12 -0
  99. package/src/styles/components/_notifications.scss +24 -0
  100. package/src/styles/components/_q-editor.scss +14 -0
  101. package/src/styles/components/_radio-buttons.scss +17 -0
  102. package/src/styles/components/_tables.scss +92 -0
  103. package/src/styles/components/_tooltips.scss +5 -0
  104. package/src/styles/components/_tree.scss +86 -0
  105. package/src/styles/components/_truncated-chips.scss +39 -0
  106. package/src/styles/main.scss +45 -0
  107. package/src/styles/quasar.variables.scss +50 -0
  108. package/src/typings.d.ts +70 -0
  109. package/src/utils/colorUtils.ts +73 -0
  110. package/src/utils/errorUtils.ts +37 -0
  111. package/src/utils/fileUtils.ts +57 -0
  112. package/src/utils/helpers.ts +6 -0
  113. package/src/utils/routeUtils.ts +23 -0
  114. package/tsconfig.json +37 -0
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <q-dialog
3
+ data-id="dialog"
4
+ class="c-dialog"
5
+ :model-value="modelValue"
6
+ :position="position"
7
+ transition-show="fade"
8
+ transition-hide="fade"
9
+ no-refocus
10
+ @update:model-value="cancel"
11
+ @keydown.ctrl.s.prevent="confirm"
12
+ >
13
+ <c-dirty-check
14
+ :is-dirty="isDirty"
15
+ :is-dialog-open="modelValue"
16
+ />
17
+
18
+ <q-card
19
+ class="dialog-card"
20
+ :style="{ maxWidth: maxWidth ? maxWidth : false }"
21
+ >
22
+ <q-form
23
+ ref="dialogForm"
24
+ greedy
25
+ class="flex flex-col flex-nowrap max-h-full"
26
+ @submit.prevent=""
27
+ >
28
+ <q-card-section class="px-5 pt-6 pb-4">
29
+ <slot name="header">
30
+ <h3 data-id="dialog-title">
31
+ {{ title }}
32
+ </h3>
33
+ </slot>
34
+ <div
35
+ v-if="text"
36
+ class="mt-2px"
37
+ >
38
+ {{ text }}
39
+ </div>
40
+
41
+ <div class="absolute right-0 top-0">
42
+ <slot name="corner-buttons" />
43
+ <c-dialog-close-btn @click="cancel" />
44
+ </div>
45
+ </q-card-section>
46
+
47
+ <q-card-section
48
+ v-if="$slots.default"
49
+ class="px-5 pb-4 overflow-y-overlay flex-1"
50
+ :class="contentClass"
51
+ >
52
+ <slot />
53
+ </q-card-section>
54
+
55
+ <div class="dialog-card-action-buttons px-5 pt-2 pb-10">
56
+ <c-btn
57
+ v-if="!hideConfirmButton"
58
+ data-id="dialog-confirm-button"
59
+ :label="confirmButtonText || $t('inri.common.confirm')"
60
+ :disabled="confirmIsDisabled"
61
+ @click="confirm"
62
+ >
63
+ <q-tooltip v-if="confirmIsDisabled">
64
+ {{ disabledConfirmTooltip || $t('inri.dialog.confirm_tooltip_invalid_fields') }}
65
+ </q-tooltip>
66
+ </c-btn>
67
+ <c-btn
68
+ v-if="!hideCancelButton"
69
+ data-id="dialog-cancel-button"
70
+ :label="cancelButtonText || $t('inri.common.cancel')"
71
+ color="surface"
72
+ @click="cancel"
73
+ />
74
+ </div>
75
+ </q-form>
76
+
77
+ <q-inner-loading :showing="isLoading">
78
+ <c-spinner />
79
+ </q-inner-loading>
80
+ </q-card>
81
+ </q-dialog>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ import { PropType, computed, ref } from 'vue'
86
+ import notify from '../plugins/notify'
87
+ import dialog from '../plugins/dialog'
88
+ import { useI18n } from 'vue-i18n'
89
+
90
+ const { t } = useI18n()
91
+
92
+ const props = defineProps({
93
+ modelValue: Boolean,
94
+ title: {
95
+ type: String,
96
+ default: '',
97
+ },
98
+ isDirty: {
99
+ type: Boolean,
100
+ default: undefined,
101
+ },
102
+ disableConfirm: Boolean,
103
+ disabledConfirmTooltip: {
104
+ type: String,
105
+ default: undefined,
106
+ },
107
+ confirmButtonText: {
108
+ type: String,
109
+ default: undefined,
110
+ },
111
+ cancelButtonText: {
112
+ type: String,
113
+ default: undefined,
114
+ },
115
+ hideConfirmButton: Boolean,
116
+ hideCancelButton: Boolean,
117
+ ignoreDirtyCheckOnConfirm: Boolean,
118
+ isLoading: Boolean,
119
+ confirmCancelTitle: {
120
+ type: String,
121
+ default: undefined,
122
+ },
123
+ confirmCancelMessage: {
124
+ type: String,
125
+ default: undefined,
126
+ },
127
+ maxWidth: {
128
+ type: [String, Number],
129
+ default: '100%',
130
+ },
131
+ contentClass: {
132
+ type: String,
133
+ default: '',
134
+ },
135
+ text: {
136
+ type: String,
137
+ default: undefined,
138
+ },
139
+ position: {
140
+ type: String as PropType<'top' | 'standard' | 'right' | 'bottom' | 'left' | undefined>,
141
+ default: 'top',
142
+ },
143
+ })
144
+
145
+ const emit = defineEmits([
146
+ 'confirm',
147
+ 'cancel',
148
+ ])
149
+
150
+ // State etc.
151
+ const confirmIsDisabled = computed(() => {
152
+ return (
153
+ props.disableConfirm ||
154
+ props.isLoading ||
155
+ (!props.ignoreDirtyCheckOnConfirm && props.isDirty === false)
156
+ )
157
+ })
158
+ const dialogForm = ref()
159
+
160
+ // Functions
161
+ function cancel () {
162
+ if (props.isDirty) {
163
+ dialog.confirm({
164
+ title: props.confirmCancelTitle || t('inri.dialog.unsaved_changes_title'),
165
+ text: props.confirmCancelMessage || t('inri.dialog.unsaved_changes_message'),
166
+ }).onOk(() => {
167
+ emit('cancel')
168
+ })
169
+ } else {
170
+ emit('cancel')
171
+ }
172
+ }
173
+
174
+ function confirm () {
175
+ if (!props.disableConfirm) {
176
+ dialogForm.value.validate().then(success => {
177
+ if (success) {
178
+ emit('confirm')
179
+ }
180
+ })
181
+ } else {
182
+ notify.error(props.disabledConfirmTooltip || t('inri.dialog.confirm_tooltip_invalid_fields'))
183
+ }
184
+ }
185
+
186
+ // So that we can trigger confirm/cancel from a component ref
187
+ defineExpose({
188
+ confirm,
189
+ cancel,
190
+ })
191
+ </script>
192
+
193
+ <style lang="scss">
194
+ // This css is also shared by the programmatic dialogs
195
+ .c-dialog {
196
+ .q-dialog__backdrop {
197
+ background: var(--dialog-backdrop-color);
198
+ }
199
+
200
+ .q-dialog__inner > div {
201
+ border-radius: 0;
202
+ }
203
+
204
+ .dialog-card {
205
+ display: flex;
206
+ max-height: 100vh;
207
+ flex-direction: column;
208
+ width: 100%;
209
+ box-shadow: var(--dialog-shadow);
210
+ }
211
+
212
+ .dialog-card-action-buttons {
213
+ .c-btn {
214
+ width: 100%;
215
+ }
216
+
217
+ .c-btn + .c-btn {
218
+ margin-top: 16px;
219
+ }
220
+ }
221
+ }
222
+
223
+ // 'sm' displays
224
+ @media (min-width: 640px) {
225
+ .c-dialog {
226
+ .dialog-card-action-buttons {
227
+ .c-btn {
228
+ width: auto;
229
+ }
230
+
231
+ .c-btn + .c-btn {
232
+ margin-top: 0;
233
+ margin-left: 10px;
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ // 'md' displays
240
+ @media (min-width: 1024px) {
241
+ .c-dialog {
242
+ // position="top" (default) should be offset with 60px from the top
243
+ .q-dialog__inner.fixed-top {
244
+ padding: 0 60px;
245
+
246
+ .dialog-card {
247
+ top: 61px;
248
+ max-height: calc(100vh - 120px);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <c-tile-btn
3
+ icon="mdi-close"
4
+ data-id="dialog-close-button"
5
+ :tooltip="$t('inri.common.close')"
6
+ small
7
+ class="absolute right-0 top-0"
8
+ />
9
+ </template>
@@ -0,0 +1,68 @@
1
+ <!-- eslint-disable vue/valid-template-root -->
2
+ <template />
3
+
4
+ <script setup lang="ts">
5
+ import { watch, onUnmounted, ref } from 'vue'
6
+ import { useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
7
+ import { useInriStore } from '../stores/inriStore'
8
+ import dialog from '../plugins/dialog'
9
+
10
+ const props = defineProps({
11
+ isDirty: {
12
+ type: Boolean,
13
+ default: undefined,
14
+ },
15
+ isDialogOpen: {
16
+ type: Boolean,
17
+ default: false,
18
+ },
19
+ })
20
+
21
+ const privateId = `dirty-check_${Math.random()}`
22
+ const dirtyDialogIsOpen = ref(false)
23
+ const willRedirect = ref(false)
24
+ const router = useRouter()
25
+ const inriStore = useInriStore()
26
+
27
+ watch(() => props.isDirty, value => {
28
+ inriStore.toggleDirtyComponent(privateId, value)
29
+ }, { immediate: true })
30
+
31
+ onUnmounted(() => {
32
+ inriStore.toggleDirtyComponent(privateId, false)
33
+ })
34
+
35
+ onBeforeRouteUpdate((to) => {
36
+ return handleRouteChange(to)
37
+ })
38
+
39
+ onBeforeRouteLeave((to) => {
40
+ return handleRouteChange(to)
41
+ })
42
+
43
+ function handleRouteChange (to) {
44
+ // If a dialog is open then just do nothing (prevent navigation)
45
+ if (props.isDialogOpen) {
46
+ return false
47
+ }
48
+
49
+ if (props.isDirty && !willRedirect.value) {
50
+ if (!dirtyDialogIsOpen.value) {
51
+ dirtyDialogIsOpen.value = true
52
+ dialog.confirm({
53
+ title: 'unsaved data',
54
+ text: 'are you sure you want to leave the page without saving your data?',
55
+ confirmButtonText: 'yes',
56
+ }).onOk(() => {
57
+ willRedirect.value = true
58
+ router.push(to)
59
+ }).onDismiss(() => {
60
+ dirtyDialogIsOpen.value = false
61
+ })
62
+ return false
63
+ } else {
64
+ return false
65
+ }
66
+ }
67
+ }
68
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <q-drawer
3
+ :data-id="left ? 'left-drawer' : 'right-drawer'"
4
+ :model-value="isOpen"
5
+ :side="left ? 'left' : 'right'"
6
+ bordered
7
+ :width="340"
8
+ class="overflow-y-overlay"
9
+ :overlay="!left"
10
+ :breakpoint="600"
11
+ @update:model-value="toggle"
12
+ >
13
+ <slot />
14
+ </q-drawer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { useInriStore } from '../stores/inriStore'
19
+ import { computed } from 'vue'
20
+
21
+ const props = defineProps({
22
+ left: {
23
+ type: Boolean,
24
+ default: false,
25
+ },
26
+ })
27
+
28
+ const inriStore = useInriStore()
29
+ const isOpen = computed(() => props.left ? inriStore.state.leftDrawerIsOpen : inriStore.state.rightDrawerIsOpen)
30
+
31
+ function toggle () {
32
+ if (props.left) {
33
+ inriStore.toggleLeftDrawer()
34
+ } else {
35
+ inriStore.toggleRightDrawer()
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <style>
41
+ .q-drawer--right {
42
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
43
+ }
44
+ </style>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <q-btn
3
+ ref="drawerButton"
4
+ data-id="drawer-btn"
5
+ class="drawer-button group"
6
+ :class="{
7
+ 'is-left': left,
8
+ 'w-60px sm:w-340px': isOpen,
9
+ 'w-60px': !isOpen
10
+ }"
11
+ :color="color"
12
+ :text-color="textColor"
13
+ unelevated
14
+ :aria-label="$t('inri.toggle_drawer')"
15
+ @click="toggle"
16
+ >
17
+ <c-slide-effect
18
+ :parent-color="color"
19
+ slide-right
20
+ class="-z-1"
21
+ />
22
+
23
+ <q-icon
24
+ :name="!left ? 'mdi-chevron-left' : 'mdi-chevron-right'"
25
+ :class="isOpen ? 'rotate-180' : 'rotate-360'"
26
+ />
27
+ </q-btn>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed, ref } from 'vue'
32
+ import { useInriStore } from '../stores/inriStore'
33
+
34
+ const props = defineProps({
35
+ left: {
36
+ type: Boolean,
37
+ default: false,
38
+ },
39
+ color: {
40
+ type: String,
41
+ default: 'primary',
42
+ },
43
+ textColor: {
44
+ type: String,
45
+ default: 'on-primary',
46
+ },
47
+ })
48
+
49
+ const drawerButton = ref()
50
+ const inriStore = useInriStore()
51
+ const isOpen = computed(() => props.left ? inriStore.state.leftDrawerIsOpen : inriStore.state.rightDrawerIsOpen)
52
+
53
+ function toggle () {
54
+ if (props.left) {
55
+ inriStore.toggleLeftDrawer()
56
+ } else {
57
+ inriStore.toggleRightDrawer()
58
+ }
59
+
60
+ // Remove focus from button on click
61
+ drawerButton.value.$el.blur()
62
+ }
63
+ </script>
64
+
65
+ <style lang="scss">
66
+ .drawer-button {
67
+ height: 60px;
68
+ border-radius: 0;
69
+ transition: width 0.1s;
70
+
71
+ .q-focus-helper {
72
+ display: none;
73
+ }
74
+
75
+ .q-btn__content {
76
+ justify-content: start;
77
+ }
78
+
79
+ .mdi {
80
+ transition: all 0.3s;
81
+ font-size: 28px;
82
+ }
83
+ }
84
+
85
+ // Left side drawer button
86
+ .drawer-button.is-left {
87
+ .q-btn__content {
88
+ justify-content: end;
89
+ }
90
+ }
91
+ </style>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <q-expansion-item
3
+ :label="label"
4
+ :header-class="headerClass"
5
+ class="c-expansion-item"
6
+ >
7
+ <slot />
8
+ </q-expansion-item>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ defineProps({
13
+ label: {
14
+ type: String,
15
+ default: undefined,
16
+ },
17
+ headerClass: {
18
+ type: String,
19
+ default: undefined,
20
+ },
21
+ })
22
+ </script>
23
+
24
+ <style lang="scss">
25
+ .c-expansion-item {
26
+ border-bottom: 1px solid var(--color-border);
27
+
28
+ .q-item__section {
29
+ padding: 0;
30
+ }
31
+
32
+ .q-expansion-item__container {
33
+ .q-item {
34
+ padding-right: 12px;
35
+ }
36
+ }
37
+
38
+ .q-item__label {
39
+ line-height: 1.5em !important;
40
+ font-weight: 600;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ }
45
+
46
+ .q-expansion-item__toggle-icon {
47
+ color: var(--color-grey-darkest);
48
+ font-size: 22px;
49
+ }
50
+
51
+ // Selected link
52
+ .q-router-link--exact-active {
53
+ background: var(--item-selected-bg-color);
54
+ font-weight: 600;
55
+ }
56
+ }
57
+ </style>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <q-expansion-item
3
+ :header-class="headerClass"
4
+ class="c-expansion-item"
5
+ >
6
+ <template #header>
7
+ <q-checkbox
8
+ :model-value="modelValue"
9
+ v-bind="$inri.checkbox"
10
+ @update:model-value="$emit('update:modelValue', $event)"
11
+ />
12
+ <div class="ml-3 flex-1 flex align-middle items-center overflow-hidden">
13
+ <span class="truncate">{{ label }}</span>
14
+ </div>
15
+ </template>
16
+ <slot />
17
+ </q-expansion-item>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ defineEmits(['update:modelValue'])
22
+
23
+ defineProps({
24
+ modelValue: {
25
+ type: Boolean,
26
+ default: undefined,
27
+ },
28
+ label: {
29
+ type: String,
30
+ default: undefined,
31
+ },
32
+ headerClass: {
33
+ type: String,
34
+ default: undefined,
35
+ },
36
+ })
37
+ </script>
38
+
39
+ <style>
40
+ /* shared */
41
+ </style>
@@ -0,0 +1,114 @@
1
+ <template>
2
+ <div
3
+ v-show="isDragging"
4
+ :style="`height: ${height}; width: ${width}`"
5
+ :class="[
6
+ 'absolute',
7
+ 'inset-x-0',
8
+ 'z-10',
9
+ isDragging && 'border-2 border-solid border-green bg-green-light-30',
10
+ ]"
11
+ @drop.prevent="handleDrop"
12
+ >
13
+ <div class="relative w-full h-full flex justify-center items-end p-8">
14
+ <div class="absolute flex items-center rounded bg-grey-darkest text-white px-5 py-4">
15
+ <q-icon
16
+ name="mdi-download-outline"
17
+ class="mr-5 rotate-180"
18
+ size="24px"
19
+ />
20
+ <i18n-t
21
+ v-if="hintLabel"
22
+ keypath="inri.dropzone.hint"
23
+ tag="span"
24
+ scope="global"
25
+ >
26
+ <b>{{ hintLabel }}</b>
27
+ </i18n-t>
28
+ <span v-else>{{ $t('inri.dropzone.hint_general') }}</span>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { ref, onMounted, onUnmounted } from 'vue'
36
+ import { useI18n } from 'vue-i18n'
37
+ import notify from '../plugins/notify'
38
+
39
+ const { t } = useI18n()
40
+
41
+ defineProps({
42
+ hintLabel: {
43
+ type: String,
44
+ default: '',
45
+ },
46
+ height: {
47
+ type: String,
48
+ default: '100%',
49
+ },
50
+ width: {
51
+ type: String,
52
+ default: '100%',
53
+ },
54
+ })
55
+
56
+ const emit = defineEmits(['files-dropped'])
57
+
58
+ const isDragging = ref(false)
59
+ let isDraggingTimeout: ReturnType<typeof setTimeout>
60
+
61
+ function setDraggingTimeout () {
62
+ isDraggingTimeout = setTimeout(() => {
63
+ isDragging.value = false
64
+ }, 50)
65
+ }
66
+
67
+ function isFileDrag (e: DragEvent) {
68
+ return e.dataTransfer?.items.length && e.dataTransfer?.items[0].kind === 'file'
69
+ }
70
+
71
+ function handleDragEnter (e) {
72
+ if (isFileDrag(e)) {
73
+ isDragging.value = true
74
+ clearTimeout(isDraggingTimeout)
75
+ e.preventDefault()
76
+ }
77
+ }
78
+
79
+ function handleDragOver (e) {
80
+ if (isFileDrag(e)) {
81
+ isDragging.value = true
82
+ clearTimeout(isDraggingTimeout)
83
+ e.preventDefault()
84
+ }
85
+ }
86
+
87
+ function handleDragLeave (e) {
88
+ setDraggingTimeout()
89
+ e.preventDefault()
90
+ }
91
+
92
+ onMounted(() => {
93
+ document.body.addEventListener('dragenter', handleDragEnter, false)
94
+ document.body.addEventListener('dragover', handleDragOver, false)
95
+ document.body.addEventListener('dragleave', handleDragLeave, false)
96
+ })
97
+
98
+ onUnmounted(() => {
99
+ document.body.removeEventListener('dragenter', handleDragEnter, false)
100
+ document.body.removeEventListener('dragover', handleDragOver, false)
101
+ document.body.removeEventListener('dragleave', handleDragLeave, false)
102
+ })
103
+
104
+ function handleDrop (e) {
105
+ setDraggingTimeout()
106
+
107
+ if (!e.dataTransfer.files) {
108
+ notify.error(t('dropzone.invalid_drop_items'))
109
+ return
110
+ }
111
+
112
+ emit('files-dropped', [...e.dataTransfer.files])
113
+ }
114
+ </script>