@it-enterprise/forcebpm-ui-kit 1.0.2-beta.1 → 1.0.2-beta.11
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/README.md +81 -65
- package/index.js +13 -0
- package/package.json +35 -2
- package/plugin.js +191 -0
- package/src/FConfirmModal.vue +2 -4
- package/src/FContextMenu.vue +122 -0
- package/src/FNoData.vue +60 -0
- package/src/FNotify.vue +258 -0
- package/src/FPreLoader.vue +108 -0
- package/src/FShare.vue +127 -0
package/README.md
CHANGED
|
@@ -1,93 +1,109 @@
|
|
|
1
|
-
# ForceBPM
|
|
1
|
+
# ForceBPM UI Kit
|
|
2
2
|
|
|
3
|
+
Vue 3 компонентная библиотека для ForceBPM приложений.
|
|
3
4
|
|
|
5
|
+
## Установка
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
|
8
|
-
|
|
9
|
-
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
|
10
|
-
|
|
11
|
-
## Add your files
|
|
12
|
-
|
|
13
|
-
* [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
|
14
|
-
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
cd existing_repo
|
|
18
|
-
git remote add origin https://gitlab.com/it-enterprise/npm-packages/forcebpm-ui-kit.git
|
|
19
|
-
git branch -M main
|
|
20
|
-
git push -uf origin main
|
|
7
|
+
```bash
|
|
8
|
+
npm install @it-enterprise/forcebpm-ui-kit
|
|
21
9
|
```
|
|
22
10
|
|
|
23
|
-
##
|
|
11
|
+
## Требования
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
- Vue 3.x
|
|
14
|
+
- Vuetify 3.x
|
|
15
|
+
- vue-i18n (опционально, для переводов)
|
|
26
16
|
|
|
27
|
-
##
|
|
17
|
+
## Использование
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
* [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
31
|
-
* [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
32
|
-
* [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
33
|
-
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
|
19
|
+
### 1. Как плагин (рекомендуется)
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
```js
|
|
22
|
+
// main.js
|
|
23
|
+
import { createApp } from 'vue'
|
|
24
|
+
import { createI18n } from 'vue-i18n'
|
|
25
|
+
import { createVuetify } from 'vuetify'
|
|
26
|
+
import ForceBPMUiKit from '@it-enterprise/forcebpm-ui-kit'
|
|
27
|
+
import App from './App.vue'
|
|
36
28
|
|
|
37
|
-
|
|
29
|
+
const i18n = createI18n({
|
|
30
|
+
locale: 'uk',
|
|
31
|
+
fallbackLocale: 'en',
|
|
32
|
+
messages: {}
|
|
33
|
+
})
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
41
|
-
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
42
|
-
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
43
|
-
* [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
35
|
+
const vuetify = createVuetify({})
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
const app = createApp(App)
|
|
46
38
|
|
|
47
|
-
|
|
39
|
+
app.use(i18n)
|
|
40
|
+
app.use(vuetify)
|
|
41
|
+
app.use(ForceBPMUiKit, {
|
|
42
|
+
i18n, // Интеграция с vue-i18n (добавит переводы автоматически)
|
|
43
|
+
registerGlobal: true, // Регистрация компонентов глобально (по умолчанию true)
|
|
44
|
+
prefix: 'F' // Префикс компонентов (по умолчанию 'F')
|
|
45
|
+
})
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## Suggestions for a good README
|
|
52
|
-
|
|
53
|
-
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
47
|
+
app.mount('#app')
|
|
48
|
+
```
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
Choose a self-explaining name for your project.
|
|
50
|
+
### 2. Использование отдельных компонентов (tree-shaking)
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
```vue
|
|
53
|
+
<script setup>
|
|
54
|
+
import { FAvatar, FNotify, FNoData } from '@it-enterprise/forcebpm-ui-kit'
|
|
55
|
+
</script>
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
<template>
|
|
58
|
+
<FAvatar :full-user="user" />
|
|
59
|
+
<FNoData text="Немає даних" />
|
|
60
|
+
</template>
|
|
61
|
+
```
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
63
|
+
### 3. Только переводы
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
```js
|
|
66
|
+
import { createI18n } from 'vue-i18n'
|
|
67
|
+
import { defaultMessages } from '@it-enterprise/forcebpm-ui-kit/plugin'
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const i18n = createI18n({
|
|
70
|
+
locale: 'uk',
|
|
71
|
+
messages: {
|
|
72
|
+
en: { ...myMessages.en, ...defaultMessages.en },
|
|
73
|
+
uk: { ...myMessages.uk, ...defaultMessages.uk }
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
```
|
|
72
77
|
|
|
73
|
-
##
|
|
74
|
-
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
78
|
+
## Компоненты
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
| Компонент | Описание |
|
|
81
|
+
|-----------|----------|
|
|
82
|
+
| `FAvatar` | Аватар пользователя с меню информации |
|
|
83
|
+
| `FConfirmModal` | Модальное окно подтверждения |
|
|
84
|
+
| `FContextMenu` | Контекстное меню |
|
|
85
|
+
| `FHtmlContent` | Безопасный рендер HTML контента |
|
|
86
|
+
| `FNoData` | Плейсхолдер "Нет данных" |
|
|
87
|
+
| `FNotify` | Система уведомлений |
|
|
88
|
+
| `FPreLoader` | Прелоадер |
|
|
89
|
+
| `FShare` | Диалог "Поделиться" |
|
|
78
90
|
|
|
79
|
-
##
|
|
80
|
-
State if you are open to contributions and what your requirements are for accepting them.
|
|
91
|
+
## Опции плагина
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
| Опция | Тип | По умолчанию | Описание |
|
|
94
|
+
|-------|-----|--------------|----------|
|
|
95
|
+
| `registerGlobal` | `boolean` | `true` | Регистрировать компоненты глобально |
|
|
96
|
+
| `i18n` | `object` | `null` | Экземпляр vue-i18n для интеграции переводов |
|
|
97
|
+
| `prefix` | `string` | `'F'` | Префикс имён компонентов |
|
|
98
|
+
| `messages` | `object` | `null` | Дополнительные/кастомные переводы |
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
## Локализация
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
По умолчанию включены переводы для:
|
|
103
|
+
- `en` - English
|
|
104
|
+
- `uk` - Українська
|
|
105
|
+
- `ru` - Русский
|
|
88
106
|
|
|
89
|
-
##
|
|
90
|
-
For open source projects, say how it is licensed.
|
|
107
|
+
## Лицензия
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
109
|
+
MIT
|
package/index.js
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
+
// Components
|
|
1
2
|
export { default as FHtmlContent } from './src/FHtmlContent.vue'
|
|
2
3
|
export { default as FAvatar } from './src/FAvatar.vue'
|
|
4
|
+
export { default as FConfirmModal } from './src/FConfirmModal.vue'
|
|
5
|
+
export { default as FPreLoader } from './src/FPreLoader.vue'
|
|
6
|
+
export { default as FNoData } from './src/FNoData.vue'
|
|
7
|
+
export { default as FNotify } from './src/FNotify.vue'
|
|
8
|
+
export { default as FShare } from './src/FShare.vue'
|
|
9
|
+
export { default as FContextMenu } from './src/FContextMenu.vue'
|
|
10
|
+
|
|
11
|
+
// Plugin
|
|
12
|
+
export { ForceBPMUiKit, defaultMessages } from './plugin.js'
|
|
13
|
+
|
|
14
|
+
// Default export - plugin
|
|
15
|
+
export { default } from './plugin.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@it-enterprise/forcebpm-ui-kit",
|
|
3
|
-
"version": "1.0.2-beta.
|
|
3
|
+
"version": "1.0.2-beta.11",
|
|
4
4
|
"description": "FBPM UI Kit",
|
|
5
5
|
"author": "it-enterprise",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
"import": "./index.js",
|
|
12
12
|
"require": "./index.js"
|
|
13
13
|
},
|
|
14
|
+
"./plugin": {
|
|
15
|
+
"import": "./plugin.js",
|
|
16
|
+
"require": "./plugin.js"
|
|
17
|
+
},
|
|
18
|
+
"./locales": {
|
|
19
|
+
"import": "./plugin.js",
|
|
20
|
+
"require": "./plugin.js"
|
|
21
|
+
},
|
|
14
22
|
"./FHtmlContent": {
|
|
15
23
|
"import": "./src/FHtmlContent.vue",
|
|
16
24
|
"require": "./src/FHtmlContent.vue"
|
|
@@ -18,11 +26,36 @@
|
|
|
18
26
|
"./FAvatar": {
|
|
19
27
|
"import": "./src/FAvatar.vue",
|
|
20
28
|
"require": "./src/FAvatar.vue"
|
|
29
|
+
},
|
|
30
|
+
"./FConfirmModal": {
|
|
31
|
+
"import": "./src/FConfirmModal.vue",
|
|
32
|
+
"require": "./src/FConfirmModal.vue"
|
|
33
|
+
},
|
|
34
|
+
"./FPreLoader": {
|
|
35
|
+
"import": "./src/FPreLoader.vue",
|
|
36
|
+
"require": "./src/FPreLoader.vue"
|
|
37
|
+
},
|
|
38
|
+
"./FNoData": {
|
|
39
|
+
"import": "./src/FNoData.vue",
|
|
40
|
+
"require": "./src/FNoData.vue"
|
|
41
|
+
},
|
|
42
|
+
"./FNotify": {
|
|
43
|
+
"import": "./src/FNotify.vue",
|
|
44
|
+
"require": "./src/FNotify.vue"
|
|
45
|
+
},
|
|
46
|
+
"./FShare": {
|
|
47
|
+
"import": "./src/FShare.vue",
|
|
48
|
+
"require": "./src/FShare.vue"
|
|
49
|
+
},
|
|
50
|
+
"./FContextMenu": {
|
|
51
|
+
"import": "./src/FContextMenu.vue",
|
|
52
|
+
"require": "./src/FContextMenu.vue"
|
|
21
53
|
}
|
|
22
54
|
},
|
|
23
55
|
"files": [
|
|
24
56
|
"src",
|
|
25
57
|
"index.js",
|
|
58
|
+
"plugin.js",
|
|
26
59
|
"README.md",
|
|
27
60
|
"LICENSE"
|
|
28
61
|
],
|
|
@@ -59,4 +92,4 @@
|
|
|
59
92
|
"url": "https://gitlab.com/it-enterprise/npm-packages/forcebpm-ui-kit/-/issues"
|
|
60
93
|
},
|
|
61
94
|
"homepage": "https://gitlab.com/it-enterprise/npm-packages/forcebpm-ui-kit#readme"
|
|
62
|
-
}
|
|
95
|
+
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForceBPM UI Kit Plugin
|
|
3
|
+
* Vue 3 plugin for registering components globally and integrating with i18n
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as components from './index.js'
|
|
7
|
+
|
|
8
|
+
// Default translations for all components
|
|
9
|
+
export const defaultMessages = {
|
|
10
|
+
en: {
|
|
11
|
+
buttons: {
|
|
12
|
+
close: 'Close',
|
|
13
|
+
share: 'Share',
|
|
14
|
+
yes: 'Yes',
|
|
15
|
+
no: 'No'
|
|
16
|
+
},
|
|
17
|
+
noData: {
|
|
18
|
+
title: 'No data available'
|
|
19
|
+
},
|
|
20
|
+
tooltip: {
|
|
21
|
+
actions: 'Actions TEST'
|
|
22
|
+
},
|
|
23
|
+
updating: 'Updating...',
|
|
24
|
+
notification: {
|
|
25
|
+
openTask: 'Open task',
|
|
26
|
+
openDocument: 'Open document',
|
|
27
|
+
openDescussion: 'Open discussion'
|
|
28
|
+
},
|
|
29
|
+
user: {
|
|
30
|
+
telephone: 'Phone',
|
|
31
|
+
position: 'Position',
|
|
32
|
+
department: 'Department'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
uk: {
|
|
36
|
+
buttons: {
|
|
37
|
+
close: 'Закрити',
|
|
38
|
+
share: 'Поділитися',
|
|
39
|
+
yes: 'Так',
|
|
40
|
+
no: 'Ні'
|
|
41
|
+
},
|
|
42
|
+
noData: {
|
|
43
|
+
title: 'Дані відсутні'
|
|
44
|
+
},
|
|
45
|
+
tooltip: {
|
|
46
|
+
actions: 'Дії TEST'
|
|
47
|
+
},
|
|
48
|
+
updating: 'Оновлення...',
|
|
49
|
+
notification: {
|
|
50
|
+
openTask: 'Відкрити задачу',
|
|
51
|
+
openDocument: 'Відкрити документ',
|
|
52
|
+
openDescussion: 'Відкрити обговорення'
|
|
53
|
+
},
|
|
54
|
+
user: {
|
|
55
|
+
telephone: 'Телефон',
|
|
56
|
+
position: 'Посада',
|
|
57
|
+
department: 'Відділ'
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
ru: {
|
|
61
|
+
buttons: {
|
|
62
|
+
close: 'Закрыть',
|
|
63
|
+
share: 'Поделиться',
|
|
64
|
+
yes: 'Да',
|
|
65
|
+
no: 'Нет'
|
|
66
|
+
},
|
|
67
|
+
noData: {
|
|
68
|
+
title: 'Данные отсутствуют'
|
|
69
|
+
},
|
|
70
|
+
tooltip: {
|
|
71
|
+
actions: 'Действия TEST'
|
|
72
|
+
},
|
|
73
|
+
updating: 'Обновление...',
|
|
74
|
+
notification: {
|
|
75
|
+
openTask: 'Открыть задачу',
|
|
76
|
+
openDocument: 'Открыть документ',
|
|
77
|
+
openDescussion: 'Открыть обсуждение'
|
|
78
|
+
},
|
|
79
|
+
user: {
|
|
80
|
+
telephone: 'Телефон',
|
|
81
|
+
position: 'Должность',
|
|
82
|
+
department: 'Отдел'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* ForceBPM UI Kit Plugin
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Basic usage with global registration
|
|
92
|
+
* import { createApp } from 'vue'
|
|
93
|
+
* import { ForceBPMUiKit } from '@it-enterprise/forcebpm-ui-kit'
|
|
94
|
+
*
|
|
95
|
+
* const app = createApp(App)
|
|
96
|
+
* app.use(ForceBPMUiKit)
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // With i18n integration
|
|
100
|
+
* import { createApp } from 'vue'
|
|
101
|
+
* import { createI18n } from 'vue-i18n'
|
|
102
|
+
* import { ForceBPMUiKit } from '@it-enterprise/forcebpm-ui-kit'
|
|
103
|
+
*
|
|
104
|
+
* const i18n = createI18n({ locale: 'uk', messages: {} })
|
|
105
|
+
* const app = createApp(App)
|
|
106
|
+
*
|
|
107
|
+
* app.use(i18n)
|
|
108
|
+
* app.use(ForceBPMUiKit, { i18n })
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Without global registration (tree-shaking friendly)
|
|
112
|
+
* import { createApp } from 'vue'
|
|
113
|
+
* import { ForceBPMUiKit } from '@it-enterprise/forcebpm-ui-kit'
|
|
114
|
+
*
|
|
115
|
+
* const app = createApp(App)
|
|
116
|
+
* app.use(ForceBPMUiKit, { registerGlobal: false })
|
|
117
|
+
*/
|
|
118
|
+
export const ForceBPMUiKit = {
|
|
119
|
+
/**
|
|
120
|
+
* Install the plugin
|
|
121
|
+
* @param {import('vue').App} app - Vue application instance
|
|
122
|
+
* @param {Object} options - Plugin options
|
|
123
|
+
* @param {boolean} [options.registerGlobal=true] - Register components globally
|
|
124
|
+
* @param {Object} [options.i18n=null] - vue-i18n instance for translations
|
|
125
|
+
* @param {string} [options.prefix='F'] - Component name prefix
|
|
126
|
+
* @param {Object} [options.messages=null] - Custom translations to merge
|
|
127
|
+
*/
|
|
128
|
+
install(app, options = {}) {
|
|
129
|
+
const {
|
|
130
|
+
registerGlobal = true,
|
|
131
|
+
i18n = null,
|
|
132
|
+
prefix = 'F',
|
|
133
|
+
messages = null
|
|
134
|
+
} = options
|
|
135
|
+
|
|
136
|
+
// Merge translations into i18n if provided
|
|
137
|
+
if (i18n) {
|
|
138
|
+
const messagesToMerge = messages
|
|
139
|
+
? mergeDeep(defaultMessages, messages)
|
|
140
|
+
: defaultMessages
|
|
141
|
+
|
|
142
|
+
Object.entries(messagesToMerge).forEach(([locale, localeMessages]) => {
|
|
143
|
+
if (i18n.global) {
|
|
144
|
+
// Vue I18n v9+ (Composition API)
|
|
145
|
+
i18n.global.mergeLocaleMessage(locale, localeMessages)
|
|
146
|
+
} else if (typeof i18n.mergeLocaleMessage === 'function') {
|
|
147
|
+
// Vue I18n legacy mode
|
|
148
|
+
i18n.mergeLocaleMessage(locale, localeMessages)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Register components globally
|
|
154
|
+
if (registerGlobal) {
|
|
155
|
+
Object.entries(components).forEach(([name, component]) => {
|
|
156
|
+
// Allow custom prefix (e.g., 'Fbpm' instead of 'F')
|
|
157
|
+
const componentName = prefix === 'F'
|
|
158
|
+
? name
|
|
159
|
+
: name.replace(/^F/, prefix)
|
|
160
|
+
|
|
161
|
+
app.component(componentName, component)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Provide plugin config for components that may need it
|
|
166
|
+
app.provide('forcebpm-ui-kit', {
|
|
167
|
+
prefix,
|
|
168
|
+
i18n: !!i18n
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Deep merge utility for objects
|
|
175
|
+
*/
|
|
176
|
+
function mergeDeep(target, source) {
|
|
177
|
+
const output = { ...target }
|
|
178
|
+
|
|
179
|
+
for (const key in source) {
|
|
180
|
+
if (source[key] instanceof Object && key in target) {
|
|
181
|
+
output[key] = mergeDeep(target[key], source[key])
|
|
182
|
+
} else {
|
|
183
|
+
output[key] = source[key]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return output
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Default export for convenience
|
|
191
|
+
export default ForceBPMUiKit
|
package/src/FConfirmModal.vue
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
// FConfirmModal
|
|
3
3
|
import { onBeforeMount, onBeforeUnmount, ref, defineEmits } from 'vue'
|
|
4
|
-
import events from '@/plugins/eventBus/events'
|
|
5
|
-
import emitter from '@/plugins/eventBus'
|
|
6
4
|
|
|
7
5
|
const show = ref(false)
|
|
8
6
|
const loading = ref(false)
|
|
@@ -38,11 +36,11 @@ const declineHandler = () => {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
onBeforeMount(() => {
|
|
41
|
-
|
|
39
|
+
emit('confirmModalMount', showModal)
|
|
42
40
|
})
|
|
43
41
|
|
|
44
42
|
onBeforeUnmount(() => {
|
|
45
|
-
|
|
43
|
+
emit('confirmModalUnmount', showModal)
|
|
46
44
|
})
|
|
47
45
|
</script>
|
|
48
46
|
<template>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FContextMenu
|
|
3
|
+
import { computed, mergeProps } from 'vue'
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
list: {
|
|
6
|
+
type: Array,
|
|
7
|
+
required: true
|
|
8
|
+
// Example of list item:
|
|
9
|
+
// {
|
|
10
|
+
// name: 'Button Name',
|
|
11
|
+
// action: () => {},
|
|
12
|
+
// color: 'text',
|
|
13
|
+
// icon: 'fire',
|
|
14
|
+
// dividerTop: false, // optional setting to add top visual separation of list blocks
|
|
15
|
+
// dividerBottom: false // optional setting to add bottom visual separation of list blocks
|
|
16
|
+
// }
|
|
17
|
+
},
|
|
18
|
+
menuProps: {
|
|
19
|
+
type: Object,
|
|
20
|
+
default: () => ({
|
|
21
|
+
location: 'start'
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
tooltipProps: {
|
|
25
|
+
type: Object,
|
|
26
|
+
default: () => ({
|
|
27
|
+
location: 'bottom'
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
disabled: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: false
|
|
33
|
+
},
|
|
34
|
+
btnClass: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: ''
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const menu = defineModel({
|
|
41
|
+
type: Boolean,
|
|
42
|
+
default: false
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const isEmpty = computed(() => !props.list.length || props.list.every(el => el.hide))
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<v-menu v-if="!isEmpty" v-model="menu" v-bind="menuProps" content-class="f-context-menu-content">
|
|
50
|
+
<template #activator="{ props: propsMenu }">
|
|
51
|
+
<v-tooltip :disabled="menu || disabled" :text="$t('tooltip.actions')" v-bind="tooltipProps">
|
|
52
|
+
<template #activator="{ props: propsTooltip }">
|
|
53
|
+
<v-btn
|
|
54
|
+
size="25"
|
|
55
|
+
variant="text"
|
|
56
|
+
class="f-context-menu-activator"
|
|
57
|
+
:class="btnClass"
|
|
58
|
+
:disabled="disabled"
|
|
59
|
+
v-bind="mergeProps(propsMenu, propsTooltip)"
|
|
60
|
+
>
|
|
61
|
+
<FIcon icon="dots" size="18" :color="disabled ? 'disabled' : 'secondary'" />
|
|
62
|
+
</v-btn>
|
|
63
|
+
</template>
|
|
64
|
+
</v-tooltip>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<v-list>
|
|
68
|
+
<template v-for="(item, idx) in list">
|
|
69
|
+
<template v-if="!item.hide">
|
|
70
|
+
<!-- Divider top -->
|
|
71
|
+
<v-divider v-if="item.dividerTop" :key="`divider-top-${idx}`" class="bg-disabled my-3" />
|
|
72
|
+
|
|
73
|
+
<!-- List item -->
|
|
74
|
+
<v-list-item :key="idx" @click="item.action">
|
|
75
|
+
<v-list-item-title :class="`text-${item.color || 'subTitle'}`">
|
|
76
|
+
<FIcon v-if="item.icon" :icon="item.icon" :color="item.color || 'text'" size="18" />
|
|
77
|
+
<span :class="item.icon ? 'ml-1' : 'ml-5'">{{ item.name }}</span>
|
|
78
|
+
</v-list-item-title>
|
|
79
|
+
</v-list-item>
|
|
80
|
+
|
|
81
|
+
<!-- Divider bottom -->
|
|
82
|
+
<v-divider v-if="item.dividerBottom" :key="`divider-bottom-${idx}`" class="bg-disabled my-3" />
|
|
83
|
+
</template>
|
|
84
|
+
</template>
|
|
85
|
+
</v-list>
|
|
86
|
+
</v-menu>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style lang="scss" scoped>
|
|
90
|
+
.f-context-menu-content {
|
|
91
|
+
.v-list {
|
|
92
|
+
padding: 12px 6px;
|
|
93
|
+
overflow-x: hidden;
|
|
94
|
+
.v-list-item {
|
|
95
|
+
padding: 4px 6px;
|
|
96
|
+
min-height: auto;
|
|
97
|
+
:deep(.v-list-item__overlay) {
|
|
98
|
+
border-radius: 5.8px;
|
|
99
|
+
background: rgb(var(--v-theme-primary));
|
|
100
|
+
}
|
|
101
|
+
&:hover {
|
|
102
|
+
:deep(.v-list-item__overlay) {
|
|
103
|
+
opacity: 0.15;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
.v-list-item__content {
|
|
107
|
+
.v-list-item-title {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
font-size: 1em;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.v-divider {
|
|
116
|
+
position: relative;
|
|
117
|
+
min-width: calc(100% + 12px);
|
|
118
|
+
left: -6px;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
</style>
|
package/src/FNoData.vue
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FNoData
|
|
3
|
+
defineProps({
|
|
4
|
+
text: {
|
|
5
|
+
type: String,
|
|
6
|
+
default: null
|
|
7
|
+
},
|
|
8
|
+
height: {
|
|
9
|
+
type: String,
|
|
10
|
+
default: '100%',
|
|
11
|
+
validator: value => {
|
|
12
|
+
return /^(calc\([^()]+\)|\d+(\.\d+)?(px|em|rem|vh|vw|%)|auto)$/.test(value)
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
width: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: 'auto',
|
|
18
|
+
validator: value => {
|
|
19
|
+
return /^(calc\([^()]+\)|\d+(\.\d+)?(px|em|rem|vh|vw|%)|auto)$/.test(value)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
gradient: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: ''
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="f-no-data" :style="`height: ${height}; width: ${width}`">
|
|
32
|
+
<div class="f-no-data-overlay" :style="`background: ${gradient}`">
|
|
33
|
+
<span class="f-no-data-text">{{ text || $t('noData.title') }}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<style lang="scss" scoped>
|
|
39
|
+
.f-no-data {
|
|
40
|
+
min-height: 150px;
|
|
41
|
+
position: relative;
|
|
42
|
+
.f-no-data-overlay {
|
|
43
|
+
position: absolute;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
text-align: center;
|
|
49
|
+
height: 100%;
|
|
50
|
+
width: 100%;
|
|
51
|
+
color: rgb(var(--v-theme-primary));
|
|
52
|
+
top: calc(50% - 100% / 2);
|
|
53
|
+
left: calc(50% - 100% / 2);
|
|
54
|
+
font-size: 1em;
|
|
55
|
+
.f-no-data-text {
|
|
56
|
+
width: 250px;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</style>
|
package/src/FNotify.vue
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FNotify
|
|
3
|
+
import { computed, ref, onBeforeUnmount, onMounted, watch, defineAsyncComponent, defineEmits } from 'vue'
|
|
4
|
+
const FHtmlContent = defineAsyncComponent(() => import('./FHtmlContent'))
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
notify: {
|
|
8
|
+
type: Array,
|
|
9
|
+
default: () => []
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
const emit = defineEmits(['removeNotify', 'goToL'])
|
|
13
|
+
const timers = ref({}) // Store intervals for each notification by id
|
|
14
|
+
const hoveredNotifyId = ref(null)
|
|
15
|
+
const removeNotifyById = id => {
|
|
16
|
+
clearTimer(id)
|
|
17
|
+
emit('removeNotify', id)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const goToL = nf => {
|
|
21
|
+
emit('goToL', nf)
|
|
22
|
+
}
|
|
23
|
+
const removeNotifyAfterTimeOut = notify => {
|
|
24
|
+
// Initialize timer object for this notification
|
|
25
|
+
timers.value[notify.id] = {
|
|
26
|
+
intervalId: null,
|
|
27
|
+
remainingTime: notify.timeout,
|
|
28
|
+
pausedAt: Date.now()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timer = timers.value[notify.id]
|
|
32
|
+
|
|
33
|
+
// Update timer every 100ms
|
|
34
|
+
timer.intervalId = setInterval(() => {
|
|
35
|
+
if (timer.pausedAt && timer.remainingTime) {
|
|
36
|
+
const elapsed = Date.now() - timer.pausedAt
|
|
37
|
+
timer.remainingTime = timer.remainingTime - elapsed
|
|
38
|
+
timer.pausedAt = Date.now()
|
|
39
|
+
|
|
40
|
+
if (timer.remainingTime <= 0) {
|
|
41
|
+
emit('removeNotify', notify.id)
|
|
42
|
+
clearTimer(notify.id)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, 100)
|
|
46
|
+
}
|
|
47
|
+
const stopTimer = notifyId => {
|
|
48
|
+
const timer = timers.value[notifyId]
|
|
49
|
+
if (!timer || !timer.intervalId) return
|
|
50
|
+
|
|
51
|
+
// Calculate how much time has passed
|
|
52
|
+
const elapsed = Date.now() - timer.pausedAt
|
|
53
|
+
timer.remainingTime = timer.remainingTime - elapsed
|
|
54
|
+
|
|
55
|
+
// Stop interval
|
|
56
|
+
clearInterval(timer.intervalId)
|
|
57
|
+
timer.intervalId = null
|
|
58
|
+
timer.pausedAt = null
|
|
59
|
+
}
|
|
60
|
+
const continueTimer = notifyId => {
|
|
61
|
+
const timer = timers.value[notifyId]
|
|
62
|
+
if (!timer || !timer.remainingTime || timer.remainingTime <= 0) return
|
|
63
|
+
|
|
64
|
+
// Remember when we continued
|
|
65
|
+
timer.pausedAt = Date.now()
|
|
66
|
+
|
|
67
|
+
// Update every 100ms
|
|
68
|
+
timer.intervalId = setInterval(() => {
|
|
69
|
+
if (timer.pausedAt && timer.remainingTime) {
|
|
70
|
+
const elapsed = Date.now() - timer.pausedAt
|
|
71
|
+
timer.remainingTime = timer.remainingTime - elapsed
|
|
72
|
+
timer.pausedAt = Date.now()
|
|
73
|
+
|
|
74
|
+
if (timer.remainingTime <= 0) {
|
|
75
|
+
emit('removeNotify', notifyId)
|
|
76
|
+
clearTimer(notifyId)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, 100)
|
|
80
|
+
}
|
|
81
|
+
const clearTimer = notifyId => {
|
|
82
|
+
const timer = timers.value[notifyId]
|
|
83
|
+
if (!timer) return
|
|
84
|
+
|
|
85
|
+
if (timer.intervalId) clearInterval(timer.intervalId)
|
|
86
|
+
|
|
87
|
+
delete timers.value[notifyId]
|
|
88
|
+
}
|
|
89
|
+
const handleGlobalKeydown = event => {
|
|
90
|
+
// Ctrl+A или Cmd+A (Mac)
|
|
91
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'a' && hoveredNotifyId.value) {
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
|
|
94
|
+
const hoveredCard = document.querySelector(`[data-notify-id="${hoveredNotifyId.value}"]`)
|
|
95
|
+
if (hoveredCard) {
|
|
96
|
+
const textElement = hoveredCard.querySelector('.snack-text')
|
|
97
|
+
if (textElement) {
|
|
98
|
+
const range = document.createRange()
|
|
99
|
+
const selection = window.getSelection()
|
|
100
|
+
|
|
101
|
+
range.selectNodeContents(textElement)
|
|
102
|
+
selection.removeAllRanges()
|
|
103
|
+
selection.addRange(range)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
document.addEventListener('keydown', handleGlobalKeydown)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
onBeforeUnmount(() => {
|
|
114
|
+
document.removeEventListener('keydown', handleGlobalKeydown)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
watch(
|
|
118
|
+
props.notify,
|
|
119
|
+
(newval, oldVal) => {
|
|
120
|
+
if (newval.length > oldVal.length) {
|
|
121
|
+
// New notifications added
|
|
122
|
+
const newNotifications = newval.filter(n => !timers.value[n.id])
|
|
123
|
+
newNotifications.forEach(v => removeNotifyAfterTimeOut(v))
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{ deep: true }
|
|
127
|
+
)
|
|
128
|
+
</script>
|
|
129
|
+
<template>
|
|
130
|
+
<div v-if="props.notify.length" class="f-notify">
|
|
131
|
+
<div
|
|
132
|
+
v-for="nf in props.notify"
|
|
133
|
+
:key="nf.id"
|
|
134
|
+
:data-notify-id="nf.id"
|
|
135
|
+
:style="`border-color: rgb(var(--v-theme-${!nf.color ? 'secondary' : nf.color}))`"
|
|
136
|
+
class="snack"
|
|
137
|
+
@mouseover="hoveredNotifyId = nf.id"
|
|
138
|
+
@mouseleave="
|
|
139
|
+
() => {
|
|
140
|
+
hoveredNotifyId = null
|
|
141
|
+
continueTimer(nf.id)
|
|
142
|
+
}
|
|
143
|
+
"
|
|
144
|
+
@mouseenter="stopTimer(nf.id)"
|
|
145
|
+
>
|
|
146
|
+
<!-- Icon -->
|
|
147
|
+
<div class="icon" :class="`bg-${nf.color || 'secondary'}`">
|
|
148
|
+
<span v-if="nf.color === 'secondary' || nf.color === undefined" class="simple-icon">i</span>
|
|
149
|
+
<FIcon v-if="nf.color === 'success' || nf.color === 'primary'" icon="check" size="16" color="white" />
|
|
150
|
+
<span v-if="nf.color === 'info'" class="info-icon">!</span>
|
|
151
|
+
<FIcon v-if="nf.color === 'error' || nf.color === 'danger'" icon="times" size="16" color="white" />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Text -->
|
|
155
|
+
<div
|
|
156
|
+
v-if="nf.taskId || (nf.businessObjectKey && nf.businessObjectKeyType && nf.businessObjectDefinitionCode)"
|
|
157
|
+
class="text-subTitle"
|
|
158
|
+
style="overflow-wrap: anywhere"
|
|
159
|
+
>
|
|
160
|
+
<div class="d-flex flex-column">
|
|
161
|
+
<span class="snack-text selectable-text">
|
|
162
|
+
{{ nf.text }}
|
|
163
|
+
</span>
|
|
164
|
+
<span v-if="nf.commentId && nf.taskId" class="link-open-text cursor-pointer" @click.stop="goToL(nf)">
|
|
165
|
+
{{ $t('notification.openDescussion') }}
|
|
166
|
+
</span>
|
|
167
|
+
<span v-else-if="nf.taskId" class="link-open-text cursor-pointer" @click.stop="goToL(nf)">
|
|
168
|
+
{{ $t('notification.openTask') }}
|
|
169
|
+
</span>
|
|
170
|
+
<span
|
|
171
|
+
v-else-if="nf.businessObjectKey && nf.businessObjectKeyType && nf.businessObjectDefinitionCode"
|
|
172
|
+
class="link-open-text cursor-pointer"
|
|
173
|
+
@click.stop="goToL(nf)"
|
|
174
|
+
>
|
|
175
|
+
{{ $t('notification.openDocument') }}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div v-else class="text-subTitle" style="overflow-wrap: anywhere">
|
|
180
|
+
<FHtmlContent alowed-links class="snack-text selectable-text" :data="nf.text" />
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<!-- Close btn -->
|
|
184
|
+
<v-tooltip :text="$t('buttons.close')" attach offset="12">
|
|
185
|
+
<template #activator="{ props: tooltipProps }">
|
|
186
|
+
<v-btn class="mb-auto ml-auto" size="20" variant="text" v-bind="tooltipProps" @click="removeNotifyById(nf.id)">
|
|
187
|
+
<FIcon icon="times" color="text" size="0.9em" />
|
|
188
|
+
</v-btn>
|
|
189
|
+
</template>
|
|
190
|
+
</v-tooltip>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</template>
|
|
194
|
+
|
|
195
|
+
<style lang="scss" scoped>
|
|
196
|
+
.link-open-text {
|
|
197
|
+
margin: 2px 0;
|
|
198
|
+
color: rgb(var(--v-theme-links));
|
|
199
|
+
text-decoration: underline;
|
|
200
|
+
width: max-content;
|
|
201
|
+
}
|
|
202
|
+
.f-notify {
|
|
203
|
+
position: fixed;
|
|
204
|
+
height: auto;
|
|
205
|
+
max-width: 400px;
|
|
206
|
+
width: 100%;
|
|
207
|
+
bottom: 0;
|
|
208
|
+
right: 20px;
|
|
209
|
+
z-index: 4000;
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-direction: column;
|
|
212
|
+
justify-content: flex-end;
|
|
213
|
+
.snack {
|
|
214
|
+
border-width: 0.5px;
|
|
215
|
+
border-style: solid;
|
|
216
|
+
margin: 10px 0;
|
|
217
|
+
position: relative;
|
|
218
|
+
min-width: 100%;
|
|
219
|
+
width: 100%;
|
|
220
|
+
height: auto;
|
|
221
|
+
border-radius: 10.8px;
|
|
222
|
+
box-shadow: var(--f-box-shadow);
|
|
223
|
+
background: rgb(var(--v-theme-background));
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
padding: 12px 12px 12px 20px;
|
|
227
|
+
color: rgb(var(--v-theme-subTitle));
|
|
228
|
+
font-size: 0.95em;
|
|
229
|
+
line-height: 1;
|
|
230
|
+
transition: all 0.5s ease-out;
|
|
231
|
+
outline: none;
|
|
232
|
+
.icon {
|
|
233
|
+
margin-right: 12px;
|
|
234
|
+
margin-top: 0;
|
|
235
|
+
margin-bottom: auto;
|
|
236
|
+
height: 32px;
|
|
237
|
+
min-height: 32px;
|
|
238
|
+
width: 32px;
|
|
239
|
+
min-width: 32px;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
-webkit-touch-callout: none;
|
|
245
|
+
-webkit-user-select: none;
|
|
246
|
+
-khtml-user-select: none;
|
|
247
|
+
-moz-user-select: none;
|
|
248
|
+
-ms-user-select: none;
|
|
249
|
+
user-select: none;
|
|
250
|
+
.simple-icon,
|
|
251
|
+
.info-icon {
|
|
252
|
+
color: white;
|
|
253
|
+
font-size: 1.8em;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FPreLoader
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
preLoaders: {
|
|
5
|
+
type: Array,
|
|
6
|
+
default: () => []
|
|
7
|
+
}
|
|
8
|
+
})
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<v-overlay :model-value="!!props.preLoaders.length" z-index="208" opacity="0.5">
|
|
13
|
+
<div v-if="props.preLoaders.includes('tokenUpdate')" class="text-title">
|
|
14
|
+
{{ $t('updating') }}
|
|
15
|
+
</div>
|
|
16
|
+
<div class="lds-ellipsis">
|
|
17
|
+
<div></div>
|
|
18
|
+
<div></div>
|
|
19
|
+
<div></div>
|
|
20
|
+
<div></div>
|
|
21
|
+
</div>
|
|
22
|
+
</v-overlay>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<style lang="scss" scoped>
|
|
26
|
+
.v-overlay {
|
|
27
|
+
:deep(.v-overlay__scrim) {
|
|
28
|
+
background: rgb(var(--v-theme-background)) !important;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.v-overlay {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
:deep(.v-overlay__content) {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lds-ellipsis {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
position: relative;
|
|
48
|
+
width: 80px;
|
|
49
|
+
height: 80px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.lds-ellipsis div {
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 33px;
|
|
55
|
+
width: 13px;
|
|
56
|
+
height: 13px;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
background: rgb(var(--v-theme-primary));
|
|
59
|
+
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.lds-ellipsis div:nth-child(1) {
|
|
63
|
+
left: 8px;
|
|
64
|
+
animation: lds-ellipsis1 0.6s infinite;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.lds-ellipsis div:nth-child(2) {
|
|
68
|
+
left: 8px;
|
|
69
|
+
animation: lds-ellipsis2 0.6s infinite;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.lds-ellipsis div:nth-child(3) {
|
|
73
|
+
left: 32px;
|
|
74
|
+
animation: lds-ellipsis2 0.6s infinite;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.lds-ellipsis div:nth-child(4) {
|
|
78
|
+
left: 56px;
|
|
79
|
+
animation: lds-ellipsis3 0.6s infinite;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes lds-ellipsis1 {
|
|
83
|
+
0% {
|
|
84
|
+
transform: scale(0);
|
|
85
|
+
}
|
|
86
|
+
100% {
|
|
87
|
+
transform: scale(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@keyframes lds-ellipsis3 {
|
|
92
|
+
0% {
|
|
93
|
+
transform: scale(1);
|
|
94
|
+
}
|
|
95
|
+
100% {
|
|
96
|
+
transform: scale(0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes lds-ellipsis2 {
|
|
101
|
+
0% {
|
|
102
|
+
transform: translate(0, 0);
|
|
103
|
+
}
|
|
104
|
+
100% {
|
|
105
|
+
transform: translate(24px, 0);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
</style>
|
package/src/FShare.vue
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FShare
|
|
3
|
+
import { ref, onBeforeMount, onBeforeUnmount, defineEmits } from 'vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
buttons: {
|
|
7
|
+
type: Array,
|
|
8
|
+
default: () => []
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
const modelValue = ref(false)
|
|
12
|
+
const dataObj = ref(null)
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits(['shareOnMount', 'shareOnUnmount'])
|
|
15
|
+
|
|
16
|
+
const open = params => {
|
|
17
|
+
dataObj.value = params
|
|
18
|
+
modelValue.value = true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const close = () => {
|
|
22
|
+
dataObj.value = null
|
|
23
|
+
modelValue.value = false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const applyMethod = method => {
|
|
27
|
+
method(dataObj.value)
|
|
28
|
+
close()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onBeforeMount(() => {
|
|
32
|
+
emit('shareOnMount', open)
|
|
33
|
+
})
|
|
34
|
+
onBeforeUnmount(() => {
|
|
35
|
+
emit('shareOnUnmount', open)
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
<template>
|
|
39
|
+
<v-dialog v-model="modelValue" min-width="350" content-class="f-dialog">
|
|
40
|
+
<!-- Header -->
|
|
41
|
+
<div class="f-dialog-header">
|
|
42
|
+
<!-- Title -->
|
|
43
|
+
<div class="f-dialog-header-title">{{ $t('buttons.share') }}</div>
|
|
44
|
+
<v-tooltip offset="10" :text="$t('buttons.close')">
|
|
45
|
+
<template #activator="{ props: tooltipProps }">
|
|
46
|
+
<v-btn v-bind="tooltipProps" variant="text" size="32" class="f-dialog-header-action f-dialog-header-action-close" @click="close">
|
|
47
|
+
<FIcon icon="times" size="18" color="fields-light" />
|
|
48
|
+
</v-btn>
|
|
49
|
+
</template>
|
|
50
|
+
</v-tooltip>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Body -->
|
|
54
|
+
<div class="f-dialog-body">
|
|
55
|
+
<div class="share-wrap">
|
|
56
|
+
<template v-for="(btn, idx) in props.buttons">
|
|
57
|
+
<v-btn v-if="!btn.hide" :key="idx" variant="text" class="share-btn" color="subTitle" @click="applyMethod(btn.action)">
|
|
58
|
+
<FIcon :icon="btn.icon" size="24" :color="btn.color" />
|
|
59
|
+
<!-- <FHtmlContent :data="btn.name" /> -->
|
|
60
|
+
<span v-if="btn.name" class="share-btn-name">{{ btn.name }}</span>
|
|
61
|
+
</v-btn>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</v-dialog>
|
|
66
|
+
</template>
|
|
67
|
+
<style lang="scss" scoped>
|
|
68
|
+
:deep(.f-dialog) {
|
|
69
|
+
background: rgb(var(--v-theme-background));
|
|
70
|
+
box-shadow: 0px 4px 20px 0px rgb(var(--v-theme-title));
|
|
71
|
+
border-radius: 5.8px;
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
max-height: 90vh; // Limit for the entire dialog
|
|
75
|
+
width: auto;
|
|
76
|
+
.f-dialog-header {
|
|
77
|
+
flex-shrink: 0; // Header does not shrink
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
background: rgb(var(--v-theme-title-dark));
|
|
82
|
+
border-top-left-radius: 5px;
|
|
83
|
+
border-top-right-radius: 5px;
|
|
84
|
+
|
|
85
|
+
.f-dialog-header-title {
|
|
86
|
+
font-size: 1.15em;
|
|
87
|
+
line-height: 1.25em;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
color: rgb(var(--v-theme-fields-light));
|
|
90
|
+
padding: 12px 0px 10px 20px;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
.f-dialog-body {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
flex: 1;
|
|
97
|
+
min-height: 0; // Important for flexbox to work with overflow
|
|
98
|
+
.share-wrap {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
gap: 12px;
|
|
102
|
+
padding: 16px;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
height: 90px;
|
|
105
|
+
}
|
|
106
|
+
.share-btn .v-btn__content {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
width: 100px;
|
|
111
|
+
}
|
|
112
|
+
.share-btn-name {
|
|
113
|
+
width: 100px;
|
|
114
|
+
text-wrap: initial;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.v-theme--dark {
|
|
120
|
+
:deep(.f-dialog) {
|
|
121
|
+
box-shadow: var(--f-box-shadow);
|
|
122
|
+
.f-dialog-header {
|
|
123
|
+
border-bottom: 1px solid rgb(var(--v-theme-line));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|