@afeefa/vue-app 0.0.65 → 0.0.68

Sign up to get free protection for your applications and to get access to all the features.
@@ -1 +1 @@
1
- 0.0.65
1
+ 0.0.68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.65",
3
+ "version": "0.0.68",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -1,3 +1,6 @@
1
+ import { AlertEvent } from '@a-vue/events'
2
+ import { eventBus } from '@a-vue/plugins/event-bus/EventBus'
3
+
1
4
  import { ApiAction } from './ApiAction'
2
5
 
3
6
  export class SaveAction extends ApiAction {
@@ -12,4 +15,12 @@ export class SaveAction extends ApiAction {
12
15
 
13
16
  this.alert('Die Daten wurden gespeichert.')
14
17
  }
18
+
19
+ processError (result) {
20
+ eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
21
+ headline: 'Die Daten konntent nicht gespeichert werden.',
22
+ message: result.message,
23
+ detail: result.detail
24
+ }))
25
+ }
15
26
  }
@@ -1,19 +1,38 @@
1
1
  <template>
2
- <div class="d-flex flex-wrap align-center">
3
- <div
4
- v-for="(breadcrumb, index) in breadcrumbs"
5
- :key="index"
6
- class="item mr-2 d-flex align-center"
7
- >
8
- <v-icon>$chevronRightIcon</v-icon>
9
-
10
- <router-link
11
- :to="breadcrumb.to"
12
- :exact="true"
2
+ <div class="a-breadcrumbs d-flex align-start gap-2 mr-4">
3
+ <div :class="['breadcrumbs d-flex align-center', {'flex-wrap': wrapBreadcrumbs_}]">
4
+ <div
5
+ v-for="(breadcrumb, index) in breadcrumbs"
6
+ :key="index"
7
+ class="item mr-2 d-flex align-center"
13
8
  >
14
- {{ getItemTitle(breadcrumb.title) }}
15
- </router-link>
9
+ <v-icon v-if="index > 0">
10
+ $chevronRightIcon
11
+ </v-icon>
12
+
13
+ <router-link
14
+ :to="breadcrumb.to"
15
+ :exact="true"
16
+ >
17
+ {{ breadcrumb.title }}
18
+ </router-link>
19
+ </div>
16
20
  </div>
21
+
22
+ <v-avatar
23
+ v-if="expandVisible"
24
+ class="expand"
25
+ color="#EEE"
26
+ size="1.3rem"
27
+ @click="wrapBreadcrumbs"
28
+ >
29
+ <a-icon>$caret{{ wrapBreadcrumbs_ ? 'Up' : 'Down' }}Icon</a-icon>
30
+ </v-avatar>
31
+
32
+ <div
33
+ v-else
34
+ class="expandDummy"
35
+ />
17
36
  </div>
18
37
  </template>
19
38
 
@@ -28,6 +47,8 @@ export default class ABreadcrumbs extends Vue {
28
47
  breadcrumbs = []
29
48
  titleCache = {}
30
49
  lastRoute = null
50
+ expandVisible = false
51
+ wrapBreadcrumbs_ = false
31
52
 
32
53
  created () {
33
54
  this.$events.on(SaveEvent.STOP_SAVING, this.afterSave)
@@ -101,21 +122,46 @@ export default class ABreadcrumbs extends Vue {
101
122
  }
102
123
 
103
124
  this.breadcrumbs = breadcrumbs
125
+ this.wrapBreadcrumbs_ = false
126
+
127
+ this.scrollBreadcrumbs()
128
+ }
129
+
130
+ scrollBreadcrumbs () {
131
+ this.$nextTick(() => {
132
+ const objDiv = this.$el.querySelector('.breadcrumbs')
133
+ if (objDiv.scrollWidth > objDiv.offsetWidth) {
134
+ objDiv.scrollLeft = objDiv.scrollWidth
135
+ this.expandVisible = true
136
+ } else {
137
+ objDiv.scrollLeft = 0
138
+ this.expandVisible = false
139
+ }
140
+ })
104
141
  }
105
142
 
106
- getItemTitle (title) {
107
- // title = title.concat(title).concat(title)
108
- if (title.length > 20) {
109
- // title = title.slice(0, 10).trim() + '...' + title.slice(-10).trim()
143
+ wrapBreadcrumbs () {
144
+ this.wrapBreadcrumbs_ = !this.wrapBreadcrumbs_
145
+ if (this.wrapBreadcrumbs_) {
146
+ const objDiv = this.$el.querySelector('.breadcrumbs')
147
+ objDiv.scrollLeft = 0
148
+ } else {
149
+ this.scrollBreadcrumbs()
110
150
  }
111
- return title
112
- // return title.toUpperCase()
113
151
  }
114
152
  }
115
153
  </script>
116
154
 
117
155
 
118
156
  <style lang="scss" scoped>
157
+ .a-breadcrumbs {
158
+ overflow: hidden;
159
+ }
160
+
161
+ .breadcrumbs {
162
+ overflow: hidden;
163
+ }
164
+
119
165
  .item {
120
166
  white-space: nowrap;
121
167
 
@@ -131,4 +177,14 @@ export default class ABreadcrumbs extends Vue {
131
177
  }
132
178
  }
133
179
  }
180
+
181
+ .expand {
182
+ cursor: pointer;
183
+ margin-top: 1px;
184
+ }
185
+
186
+ .expandDummy {
187
+ width: 1.5rem;
188
+ height: 1.5rem;
189
+ }
134
190
  </style>
@@ -13,7 +13,7 @@
13
13
  :label="label"
14
14
  :style="cwm_widthStyle"
15
15
  readonly
16
- v-bind="attrs"
16
+ v-bind="{...$attrs, ...attrs}"
17
17
  :rules="validationRules"
18
18
  v-on="on"
19
19
  @click.native="on.click"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <v-icon
3
- :class="{isButton}"
3
+ :class="{button}"
4
4
  v-bind="$attrs"
5
5
  v-on="$listeners"
6
6
  >
@@ -13,17 +13,14 @@
13
13
  import { Component, Vue } from '@a-vue'
14
14
 
15
15
  @Component({
16
- props: ['button']
16
+ props: [{button: false}]
17
17
  })
18
18
  export default class AIcon extends Vue {
19
- get isButton () {
20
- return this.button !== undefined
21
- }
22
19
  }
23
20
  </script>
24
21
 
25
22
  <style lang="scss" scoped>
26
- .v-icon:not(.isButton)::after {
23
+ .v-icon:not(.button)::after {
27
24
  background: none;
28
25
  }
29
26
  </style>
@@ -1,12 +1,11 @@
1
1
  <template>
2
2
  <v-btn
3
- small
4
3
  v-bind="$attrs"
5
4
  v-on="$listeners"
6
5
  >
7
6
  <v-icon
8
7
  left
9
- class="mr-0"
8
+ class="mr-1"
10
9
  >
11
10
  {{ icon }}
12
11
  </v-icon>
@@ -58,13 +58,6 @@ export default class ARow extends Vue {
58
58
  <style scoped lang="scss">
59
59
  .a-row {
60
60
  display: flex;
61
- overflow: hidden;
62
- @media (max-width: 900px), (orientation : portrait) {
63
- flex-wrap: wrap;
64
- & > * {
65
- flex: 0 0 auto;
66
- }
67
- }
68
61
 
69
62
  &.full {
70
63
  width: 100%;
@@ -20,7 +20,7 @@ import { debounce } from '@a-vue/utils/debounce'
20
20
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
21
21
 
22
22
  @Component({
23
- props: ['focus', 'debounce', 'validator', 'password']
23
+ props: ['focus', 'debounce', 'validator', {password: false, number: false}]
24
24
  })
25
25
  export default class ATextField extends Mixins(ComponentWidthMixin) {
26
26
  showPassword = false
@@ -58,21 +58,21 @@ export default class ATextField extends Mixins(ComponentWidthMixin) {
58
58
  }
59
59
 
60
60
  get type () {
61
- if (this.password !== undefined && !this.showPassword) {
61
+ if (this.password && !this.showPassword) {
62
62
  return 'password'
63
63
  }
64
64
  return 'text'
65
65
  }
66
66
 
67
67
  get appendIcon () {
68
- if (this.password !== undefined) {
68
+ if (this.password) {
69
69
  return this.showPassword ? '$eyeIcon' : '$eyeOffIcon'
70
70
  }
71
71
  return null
72
72
  }
73
73
 
74
74
  get autocomplete () {
75
- if (this.password !== undefined) {
75
+ if (this.password) {
76
76
  return 'new-password'
77
77
  }
78
78
  return null
@@ -87,7 +87,8 @@ export default class ATextField extends Mixins(ComponentWidthMixin) {
87
87
  if (!this.validator) {
88
88
  return false
89
89
  }
90
- return this.validator.getParams().max || false
90
+
91
+ return (!this.number && this.validator.getParams().max) || false
91
92
  }
92
93
  }
93
94
  </script>
@@ -70,10 +70,19 @@ export class FormFieldMixin extends Vue {
70
70
 
71
71
  if (field.hasOptions()) {
72
72
  const options = field.getOptions()
73
- return Object.keys(options).map(value => ({
74
- itemText: options[value],
75
- itemValue: value
76
- }))
73
+ return options.map((value, index) => {
74
+ if (typeof value === 'object') { // object option
75
+ return {
76
+ itemText: value.title,
77
+ itemValue: value.value
78
+ }
79
+ } else { // scalar option
80
+ return {
81
+ itemText: value,
82
+ itemValue: index
83
+ }
84
+ }
85
+ })
77
86
  }
78
87
  }
79
88
 
@@ -1,9 +1,11 @@
1
1
  <template>
2
2
  <a-text-field
3
- v-model="model[name]"
3
+ :value="internalValue"
4
4
  :label="label || name"
5
5
  :validator="validator"
6
6
  v-bind="$attrs"
7
+ @input="textFieldValueChanged"
8
+ @blur="onBlur"
7
9
  />
8
10
  </template>
9
11
 
@@ -11,7 +13,70 @@
11
13
  import { Component, Mixins } from '@a-vue'
12
14
  import { FormFieldMixin } from '../FormFieldMixin'
13
15
 
14
- @Component
16
+ @Component({
17
+ props: [{emptyNull: false}]
18
+ })
15
19
  export default class FormFieldText extends Mixins(FormFieldMixin) {
20
+ internalValue = ''
21
+
22
+ created () {
23
+ this.setInternalValue(this.model[this.name])
24
+ this.$watch(() => this.model[this.name], value => {
25
+ this.setInternalValue(value)
26
+ })
27
+ }
28
+
29
+ onBlur () {
30
+ this.setInternalValue(this.model[this.name], true)
31
+ }
32
+
33
+ textFieldValueChanged (value) {
34
+ this.internalValue = value
35
+
36
+ // cast to number
37
+ if (this.isNumber) {
38
+ value = Number(value)
39
+ if (isNaN(value)) {
40
+ return // do not set anything to the model
41
+ }
42
+ }
43
+
44
+ // set model value to null if empty
45
+ if (this.emptyNull) {
46
+ if (this.isNumber) {
47
+ if (value === 0) {
48
+ value = null
49
+ }
50
+ } else {
51
+ if (!value) {
52
+ value = null
53
+ }
54
+ }
55
+ }
56
+
57
+ this.model[this.name] = value
58
+ }
59
+
60
+ setInternalValue (value, reset = false) {
61
+ if (this.isNumber) {
62
+ // reset text field if value is null but keep leading 0 (allows for copy and paste)
63
+ if (value === null) {
64
+ if (!reset && this.internalValue === '0') {
65
+ value = '0'
66
+ } else {
67
+ value = ''
68
+ }
69
+ }
70
+ } else { // null string should be ''
71
+ if (!value) {
72
+ value = ''
73
+ }
74
+ }
75
+ this.internalValue = value
76
+ }
77
+
78
+ get isNumber () {
79
+ return this.$attrs.number === ''
80
+ }
16
81
  }
17
82
  </script>
@@ -11,6 +11,7 @@ import { FilterSourceType } from './FilterSourceType'
11
11
  'listAction',
12
12
  'filterHistoryKey',
13
13
  'loadOnlyIfKeyword',
14
+ 'checkBeforeLoad',
14
15
  {
15
16
  filterSource: FilterSourceType.QUERY_STRING,
16
17
  events: true,
@@ -120,6 +121,16 @@ export class ListViewMixin extends Vue {
120
121
  }
121
122
 
122
123
  async load () {
124
+ if (this.checkBeforeLoad) {
125
+ const canLoad = await this.checkBeforeLoad()
126
+ if (!canLoad) {
127
+ if (this.meta_.used_filters) {
128
+ this.listViewModel.initFromUsedFilters(this.meta_.used_filters, this.meta_.count_search)
129
+ }
130
+ return
131
+ }
132
+ }
133
+
123
134
  if (this._loadOnlyIfKeyword && !this.filters.q.value) {
124
135
  this.models_ = []
125
136
  this.meta_ = {}
@@ -25,10 +25,14 @@ export class ClickOutsideMixin extends Vue {
25
25
  // popup clicked
26
26
  const thisIndex = getZIndex(this.$el)
27
27
  const targetIndex = getZIndex(e.target)
28
- if (targetIndex > thisIndex) {
28
+ if (targetIndex > 10 && targetIndex > thisIndex) { // sidebar === 6
29
29
  return
30
30
  }
31
31
 
32
+ this.com_onClickOutside()
32
33
  this.$emit('click:outside')
33
34
  }
35
+
36
+ com_onClickOutside () {
37
+ }
34
38
  }
@@ -73,78 +73,46 @@
73
73
  </v-container>
74
74
  </v-navigation-drawer>
75
75
 
76
- <v-navigation-drawer
77
- v-if="false"
78
- app
79
- clipped
80
- right
81
- >
82
- <v-list>
83
- <v-list-item
84
- v-for="n in 5"
85
- :key="n"
86
- link
87
- >
88
- <v-list-item-content>
89
- <v-list-item-title>Item {{ n }}</v-list-item-title>
90
- </v-list-item-content>
91
- </v-list-item>
92
- </v-list>
93
- </v-navigation-drawer>
94
-
95
- <v-app-bar
96
- v-if="false"
97
- app
98
- flat
99
- dense
100
- color="#FAFAFA"
101
- >
102
- <div class="d-flex align-start mt-n2">
103
- <v-app-bar-nav-icon
104
- class="sidebarToggleButton mr-2 ml-n1"
105
- @click="toggleDrawer"
106
- />
107
- <a-breadcrumbs class="mt-2" />
108
- </div>
109
-
110
- <a-loading-indicator
111
- fixed
112
- top
113
- left
114
- class="loadingIndicator"
115
- :isLoading="isLoading"
116
- :color="loaderColor"
117
- />
118
- </v-app-bar>
76
+ <a-loading-indicator
77
+ fixed
78
+ top
79
+ left
80
+ class="loadingIndicator"
81
+ :isLoading="isLoading"
82
+ :color="loaderColor"
83
+ />
119
84
 
120
85
  <v-main id="v-main">
121
- <a-row class="topbar">
86
+ <a-row
87
+ start
88
+ class="topbar"
89
+ >
122
90
  <v-app-bar-nav-icon
123
- class="sidebarToggleButton mr-2 ml-n1"
91
+ class="sidebarToggleButton mr-2 ml-4"
124
92
  @click="toggleDrawer"
125
93
  />
94
+
126
95
  <a-breadcrumbs />
127
96
  </a-row>
128
97
 
129
98
  <v-container
130
99
  fluid
131
- class="pa-4"
100
+ class="pa-8 pt-4"
132
101
  >
133
- <div class="sticky-app-bar d-flex align-center">
134
- <app-bar-title-container class="flex-grow-1" />
135
- <app-bar-buttons class="mr-2" />
136
- </div>
102
+ <sticky-header />
137
103
 
138
104
  <router-view :class="{isLoading}" />
139
105
  </v-container>
140
106
 
141
- <sticky-footer-container v-if="true" />
107
+ <sticky-footer-container />
142
108
  </v-main>
143
109
 
144
110
  <a-dialog id="app" />
145
111
 
146
112
  <a-save-indicator />
147
113
 
114
+ <sidebar />
115
+
148
116
  <flying-context-container />
149
117
  </div>
150
118
  </template>
@@ -158,6 +126,8 @@ import AppBarButtons from './app/AppBarButtons'
158
126
  import AppBarTitleContainer from './app/AppBarTitleContainer'
159
127
  import FlyingContextContainer from './FlyingContextContainer'
160
128
  import StickyFooterContainer from './StickyFooterContainer'
129
+ import Sidebar from './Sidebar'
130
+ import StickyHeader from './StickyHeader'
161
131
  import '../styles.scss'
162
132
 
163
133
  @Component({
@@ -165,12 +135,13 @@ import '../styles.scss'
165
135
  AppBarButtons,
166
136
  AppBarTitleContainer,
167
137
  FlyingContextContainer,
168
- StickyFooterContainer
138
+ StickyFooterContainer,
139
+ Sidebar,
140
+ StickyHeader
169
141
  }
170
142
  })
171
143
  export default class App extends Vue {
172
144
  drawer = true
173
- mainDrawer = false
174
145
  isLoading = false
175
146
  account = null
176
147
 
@@ -185,17 +156,6 @@ export default class App extends Vue {
185
156
  this.$emit('appLoaded')
186
157
  }
187
158
 
188
- mounted () {
189
- const el = document.querySelector('.sticky-app-bar')
190
- const observer = new IntersectionObserver(
191
- ([e]) => {
192
- e.target.classList.toggle('is-pinned', e.intersectionRatio < 1)
193
- },
194
- { threshold: [1] }
195
- )
196
- observer.observe(el)
197
- }
198
-
199
159
  get SidebarMenu () {
200
160
  return appConfig.components.SidebarMenu
201
161
  }
@@ -228,16 +188,6 @@ export default class App extends Vue {
228
188
  this.drawer = !this.drawer
229
189
  }
230
190
 
231
- @Watch('drawer')
232
- async drawerChanged () {
233
- if (this.drawer) {
234
- this.mainDrawer = false
235
- } else {
236
- await sleep(0.1)
237
- this.mainDrawer = true
238
- }
239
- }
240
-
241
191
  get hasAuthService () {
242
192
  return !!appConfig.authService
243
193
  }
@@ -281,16 +231,16 @@ export default class App extends Vue {
281
231
  top: 0;
282
232
  padding: .2rem 1rem;
283
233
  }
284
- .sticky-app-bar {
285
- position: sticky;
286
- top: -1px;
287
- margin: -1rem -1rem 0;
288
- padding: 1rem;
289
-
290
- &.is-pinned {
291
- background: white;
292
- z-index: 2;
293
- box-shadow: 0 4px 7px -4px #00000033;
294
- }
234
+
235
+ .a-breadcrumbs {
236
+ margin-top: 7px;
237
+ }
238
+
239
+ .menubar {
240
+ // background: #666666 !important;
241
+ }
242
+
243
+ #sidebar {
244
+ // background: #F4F4F4 !important;
295
245
  }
296
246
  </style>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <v-navigation-drawer
3
+ id="sidebar"
4
+ v-model="visible"
5
+ app
6
+ right
7
+ disable-resize-watcher
8
+ width="220"
9
+ >
10
+ <div id="sidebar__children">
11
+ <div class="top" />
12
+ <div class="bottom" />
13
+ </div>
14
+ </v-navigation-drawer>
15
+ </template>
16
+
17
+ <script>
18
+ import { Component, Vue } from '@a-vue'
19
+
20
+ @Component({
21
+ props: []
22
+ })
23
+ export default class Sidebar extends Vue {
24
+ visible = false
25
+
26
+ mounted () {
27
+ this.mutationWatcher = new MutationObserver(this.domChanged)
28
+ this.mutationWatcher.observe(this.$el.querySelector('#sidebar__children > .top'), { childList: true })
29
+ this.mutationWatcher.observe(this.$el.querySelector('#sidebar__children > .bottom'), { childList: true })
30
+
31
+ this.domChanged()
32
+ }
33
+
34
+ domChanged () {
35
+ this.visible = this.hasSidebarItems()
36
+ }
37
+
38
+ getChildrenContainer () {
39
+ return this.$el.querySelector('#sidebar__children')
40
+ }
41
+
42
+ hasSidebarItems () {
43
+ return !!(this.$el.querySelector('#sidebar__children .top').children.length +
44
+ this.$el.querySelector('#sidebar__children .bottom').children.length)
45
+ }
46
+ }
47
+ </script>
48
+
49
+
50
+ <style lang="scss" scoped>
51
+ #sidebar {
52
+ &__children {
53
+ width: 100%;
54
+ }
55
+ }
56
+
57
+ #sidebar__children {
58
+ height: 100%;
59
+ padding: 2rem;
60
+ // padding-left: 4rem;
61
+
62
+ display: flex;
63
+ flex-direction: column;
64
+ justify-content: space-between;
65
+ }
66
+ </style>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <div class="sidebarItem">
3
+ <div :class="contextId">
4
+ <slot />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script>
10
+ import { Component, Vue } from '@a-vue'
11
+ import { randomCssClass } from '@a-vue/utils/random'
12
+
13
+ @Component({
14
+ props: [
15
+ {
16
+ top: true,
17
+ bottom: false
18
+ }
19
+ ]
20
+ })
21
+ export default class SidebarItem extends Vue {
22
+ contextId = randomCssClass(10)
23
+
24
+ mounted () {
25
+ const container = this.getSidebarContainer()
26
+ console.log(container)
27
+ container.appendChild(this.getContent())
28
+ }
29
+
30
+ destroyed () {
31
+ const container = this.getSidebarContainer()
32
+ const el = this.getContent()
33
+ if (container.contains(el)) {
34
+ container.removeChild(el)
35
+ }
36
+ }
37
+
38
+ getContent () {
39
+ return document.querySelector('.' + this.contextId)
40
+ }
41
+
42
+ getSidebarContainer () {
43
+ console.log('toporbottom', this.$props)
44
+ return document.querySelector('#sidebar__children > .' + this.position)
45
+ }
46
+
47
+ get position () {
48
+ if (this.bottom) {
49
+ return 'bottom'
50
+ } else {
51
+ return 'top'
52
+ }
53
+ }
54
+ }
55
+ </script>
56
+
57
+
58
+ <style lang="scss" scoped>
59
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div
3
+ id="stickyHeader"
4
+ :class="['d-flex align-center gap-8', {visible}]"
5
+ >
6
+ <app-bar-title-container class="appBarTitle flex-grow-1" />
7
+ <app-bar-buttons class="appBarButtons mr-2" />
8
+ </div>
9
+ </template>
10
+
11
+ <script>
12
+ import { Component, Vue } from '@a-vue'
13
+ import AppBarButtons from './app/AppBarButtons'
14
+ import AppBarTitleContainer from './app/AppBarTitleContainer'
15
+
16
+ @Component({
17
+ components: {
18
+ AppBarButtons,
19
+ AppBarTitleContainer
20
+ }
21
+ })
22
+ export default class StickyHeader extends Vue {
23
+ visible = false
24
+
25
+ mounted () {
26
+ // watch mutation
27
+ this.mutationWatcher = new MutationObserver(this.domChanged)
28
+ this.mutationWatcher.observe(this.$el.querySelector('.appBarTitle'), { childList: true })
29
+ this.mutationWatcher.observe(this.$el.querySelector('.appBarButtons'), { childList: true })
30
+
31
+ // watch intersection
32
+ const el = document.querySelector('#stickyHeader')
33
+ const observer = new IntersectionObserver(
34
+ ([e]) => {
35
+ e.target.classList.toggle('is-pinned', e.intersectionRatio < 1)
36
+ },
37
+ { threshold: [1] }
38
+ )
39
+ observer.observe(el)
40
+
41
+ this.domChanged()
42
+ }
43
+
44
+ domChanged () {
45
+ this.visible = this.hasItems()
46
+ }
47
+
48
+ hasItems () {
49
+ return !!(this.$el.querySelector('.appBarTitle').children.length +
50
+ this.$el.querySelector('.appBarButtons').children.length)
51
+ }
52
+ }
53
+ </script>
54
+
55
+
56
+ <style lang="scss" scoped>
57
+ #stickyHeader {
58
+ position: sticky;
59
+ top: -1px;
60
+ margin: -1rem -2rem 2rem;
61
+ padding: 1rem 2rem;
62
+
63
+ &:not(.visible) {
64
+ display: none !important;
65
+ }
66
+
67
+ &.is-pinned {
68
+ background: white;
69
+ z-index: 2;
70
+ box-shadow: 0 4px 7px -4px #00000033;
71
+ }
72
+ }
73
+ </style>
@@ -12,10 +12,3 @@ import { Component, Vue } from '@a-vue'
12
12
  export default class AppBarButtons extends Vue {
13
13
  }
14
14
  </script>
15
-
16
-
17
- <style lang="scss" scoped>
18
- #appBarButtons:empty {
19
- display: none !important;
20
- }
21
- </style>
@@ -1,13 +1,36 @@
1
1
  <template>
2
- <div class="d-flex align-center">
3
- <v-icon
4
- class="mr-2"
5
- :color="icon.color"
6
- size="1.8rem"
7
- v-text="icon.icon"
8
- />
9
-
10
- <h2>{{ title }}</h2>
2
+ <div class="d-flex align-center gap-4">
3
+ <v-btn
4
+ v-if="back"
5
+ fab
6
+ x-small
7
+ color="#F4F4F4"
8
+ title="Zurück"
9
+ class="mr-n2"
10
+ @click="$router.push(back)"
11
+ >
12
+ <v-icon>
13
+ $arrowLeftIcon
14
+ </v-icon>
15
+ </v-btn>
16
+
17
+ <v-avatar
18
+ color="#F4F4F4"
19
+ size="3rem"
20
+ >
21
+ <v-icon
22
+ :color="icon.color"
23
+ size="2.2rem"
24
+ v-text="icon.icon"
25
+ />
26
+ </v-avatar>
27
+
28
+ <div class="titleContainer">
29
+ <h3 v-if="subtitle">
30
+ {{ subtitle }}
31
+ </h3>
32
+ <h2>{{ title }}</h2>
33
+ </div>
11
34
  </div>
12
35
  </template>
13
36
 
@@ -15,7 +38,7 @@
15
38
  import { Component, Vue } from '@a-vue'
16
39
 
17
40
  @Component({
18
- props: ['icon', 'title']
41
+ props: ['back', 'icon', 'title', 'subtitle']
19
42
  })
20
43
  export default class appBarTitle extends Vue {
21
44
  mounted () {
@@ -41,8 +64,29 @@ export default class appBarTitle extends Vue {
41
64
 
42
65
 
43
66
  <style lang="scss" scoped>
67
+ .titleContainer {
68
+ overflow: hidden;
69
+ margin-top: -.2rem;
70
+ }
71
+
72
+ h3 {
73
+ font-size: .9rem;
74
+ font-weight: normal;
75
+ margin-bottom: .1rem;
76
+ line-height: 1rem;
77
+ color: #999999;
78
+
79
+ white-space: nowrap;
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
82
+ }
83
+
44
84
  h2 {
45
- // white-space: nowrap;
85
+ font-size: 1.5rem;
46
86
  line-height: 1.5rem;
87
+
88
+ white-space: nowrap;
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
47
91
  }
48
92
  </style>
@@ -11,9 +11,8 @@ export default class AppBarTitleContainer extends Vue {
11
11
  }
12
12
  </script>
13
13
 
14
-
15
14
  <style lang="scss" scoped>
16
- #appBarTitleContainer:empty {
17
- display: none;
15
+ #appBarTitleContainer {
16
+ min-width: 0; // https://stackoverflow.com/questions/38657688/text-overflow-ellipsis-not-working-in-a-nested-flex-container
18
17
  }
19
18
  </style>
@@ -68,6 +68,7 @@
68
68
  >
69
69
  <template #activator>
70
70
  <a-icon-button
71
+ small
71
72
  icon="$plusIcon"
72
73
  :text="selectableConfig.addButtonTitle || 'Hinzufügen'"
73
74
  class="mt-4"
@@ -1,17 +1,23 @@
1
1
  <template>
2
2
  <div class="detailProperty">
3
3
  <div class="header">
4
- <v-icon
4
+ <v-avatar
5
5
  v-if="_icon"
6
- :color="_icon.color"
7
- size="2rem"
6
+ color="#EEEEEE"
7
+ size="2.5rem"
8
8
  >
9
- {{ _icon.icon }}
10
- </v-icon>
11
- <label :class="['label', {'label--withIcon': _icon != null}]">{{ label }}</label>
9
+ <v-icon
10
+ :color="_icon.color"
11
+ size="1.5rem"
12
+ >
13
+ {{ _icon.icon }}
14
+ </v-icon>
15
+ </v-avatar>
16
+
17
+ <label :class="['label', {'label--withIcon': !!_icon}]">{{ label }}</label>
12
18
  </div>
13
19
 
14
- <div :class="['content', {'content--withIcon': _icon != null}]">
20
+ <div :class="['content', {'content--withIcon': !!_icon}]">
15
21
  <a-row
16
22
  vertical
17
23
  gap="6"
@@ -52,29 +58,27 @@ export default class DetailProperty extends Vue {
52
58
  flex-wrap: nowrap;
53
59
  align-items: center;
54
60
  height: 40px;
55
- .v-icon {
61
+
62
+ .v-avatar {
56
63
  flex: 0 0 40px;
57
64
  margin-right: 15px;
58
65
  }
66
+
59
67
  .label {
60
68
  display: block;
61
69
  text-transform: uppercase;
62
70
  letter-spacing: 2px;
63
- @media (max-width: 900px), (orientation : portrait) {
64
- padding-left: 55px;
65
- &--withIcon {
66
- padding-left: 0;
67
- }
71
+ padding-left: 55px;
72
+ &--withIcon {
73
+ padding-left: 0;
68
74
  }
69
75
  }
70
76
  }
71
77
  .content {
78
+ padding-left: 55px;
72
79
  &--withIcon {
73
80
  padding-left: 55px;
74
81
  }
75
- @media (max-width: 900px), (orientation : portrait) {
76
- padding-left: 55px;
77
- }
78
82
  }
79
83
  }
80
84
  </style>
@@ -1,20 +1,27 @@
1
1
  <template>
2
2
  <a-row
3
- gap="2"
3
+ gap="1"
4
4
  v-bind="$attrs"
5
5
  >
6
6
  <v-btn
7
- :small="small"
7
+ fab
8
+ small
8
9
  :disabled="($has.reset && !changed) || !valid"
9
10
  color="green white--text"
11
+ title="Speichern"
10
12
  @click="$emit('save')"
11
13
  >
12
- Speichern
14
+ <v-icon>
15
+ $checkIcon
16
+ </v-icon>
13
17
  </v-btn>
14
18
 
15
19
  <v-icon
16
20
  v-if="$has.reset && changed"
17
21
  :small="small"
22
+ :class="{disabled: !changed}"
23
+ :disabled="!changed"
24
+ color="#999999"
18
25
  text
19
26
  title="Formular zurücksetzen"
20
27
  @click="$emit('reset')"
@@ -32,7 +39,7 @@ import { mdiRotateLeft} from '@mdi/js'
32
39
  props: [
33
40
  'changed',
34
41
  'valid',
35
- 'small'
42
+ {small: false}
36
43
  ]
37
44
  })
38
45
  export default class EditFormButtons extends Vue {
@@ -41,3 +48,10 @@ export default class EditFormButtons extends Vue {
41
48
  undoIcon = mdiRotateLeft
42
49
  }
43
50
  </script>
51
+
52
+
53
+ <style lang="scss" scoped>
54
+ .v-icon--disabled {
55
+ opacity: .3;
56
+ }
57
+ </style>
@@ -1,13 +1,22 @@
1
1
  <template>
2
2
  <div>
3
- <a-icon-button
4
- small
5
- color="red white--text"
6
- :class="'removeButton-' + dialogId"
7
- icon="$trashCanIcon"
8
- text="Löschen"
9
- @click="remove"
10
- />
3
+ <v-hover v-slot="{ hover }">
4
+ <v-btn
5
+ :class="'removeButton-' + dialogId"
6
+ fab
7
+ small
8
+ :color="(hover ? 'red' : 'grey lighten-3')"
9
+ title="Löschen"
10
+ @click="remove"
11
+ >
12
+ <v-icon
13
+ :color="hover ? 'white' : '#999999'"
14
+ size="1.4rem"
15
+ >
16
+ $trashCanIcon
17
+ </v-icon>
18
+ </v-btn>
19
+ </v-hover>
11
20
 
12
21
  <a-dialog
13
22
  :id="dialogId"
@@ -19,6 +19,7 @@ import ListView from './list/ListView'
19
19
  import ModelCount from './model/ModelCount'
20
20
  import ModelIcon from './model/ModelIcon'
21
21
  import EditPage from './pages/EditPage'
22
+ import SidebarItem from './SidebarItem.vue'
22
23
  import Start from './Start.vue'
23
24
  import StickyFooter from './StickyFooter.vue'
24
25
 
@@ -48,3 +49,4 @@ Vue.component('AppBarTitle', AppBarTitle)
48
49
  Vue.component('Start', Start)
49
50
  Vue.component('FlyingContext', FlyingContext)
50
51
  Vue.component('StickyFooter', StickyFooter)
52
+ Vue.component('SidebarItem', SidebarItem)
@@ -12,7 +12,7 @@
12
12
  :valid="valid"
13
13
  />
14
14
 
15
- <sticky-footer>
15
+ <app-bar-button>
16
16
  <edit-form-buttons
17
17
  :changed="changed"
18
18
  :valid="valid"
@@ -20,7 +20,7 @@
20
20
  @save="$emit('save', modelToEdit, ignoreChangesOnRouteChange)"
21
21
  @reset="$refs.form.reset()"
22
22
  />
23
- </sticky-footer>
23
+ </app-bar-button>
24
24
  </template>
25
25
  </edit-form>
26
26
  </template>
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  mdiAlarmLightOutline,
3
+ mdiArrowLeft,
3
4
  mdiCalendar,
4
5
  mdiCheck,
5
6
  mdiCheckBold,
@@ -12,9 +13,15 @@ import {
12
13
  mdiLock,
13
14
  mdiLogoutVariant,
14
15
  mdiMagnify,
16
+ mdiMenuDown,
17
+ mdiMenuUp,
15
18
  mdiPencil,
16
19
  mdiPlus,
17
- mdiThumbUpOutline
20
+ mdiThumbUpOutline,
21
+ mdiAccountGroup,
22
+ mdiShopping,
23
+ mdiMessage,
24
+ mdiPencilBoxMultiple
18
25
  } from '@mdi/js'
19
26
  import Vue from 'vue'
20
27
  import Vuetify from 'vuetify/lib'
@@ -40,7 +47,14 @@ export default new Vuetify({
40
47
  searchIcon: mdiMagnify,
41
48
  lockIcon: mdiLock,
42
49
  checkIcon: mdiCheck,
43
- checkBoldIcon: mdiCheckBold
50
+ checkBoldIcon: mdiCheckBold,
51
+ arrowLeftIcon: mdiArrowLeft,
52
+ caretDownIcon: mdiMenuDown,
53
+ caretUpIcon: mdiMenuUp,
54
+ householdMembers: mdiAccountGroup,
55
+ shop: mdiShopping,
56
+ annotation: mdiMessage,
57
+ duplicates: mdiPencilBoxMultiple
44
58
  }
45
59
  },
46
60
  breakpoint: {
@@ -32,6 +32,26 @@
32
32
  background-color: #E9E9E9;
33
33
  }
34
34
 
35
+ .theme--light.v-btn.v-btn--disabled,
36
+ .theme--light.v-btn.v-btn--disabled .v-icon,
37
+ .theme--light.v-btn.v-btn--disabled .v-btn__loading {
38
+ color: white !important;
39
+ }
40
+
41
+ .theme--light.v-btn.v-btn--disabled.v-btn--has-bg {
42
+ background-color: #EEEEEE !important;
43
+ }
44
+
45
+ .v-btn {
46
+ box-shadow: none !important;
47
+ }
48
+
49
+ // @for $i from 1 through 10 {
50
+ // .gap-{$i} {
51
+ // gap: 0.25rem * $i;
52
+ // }
53
+ // }
54
+
35
55
  .gap-0 {
36
56
  gap: 0;
37
57
  }