1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-22 03:12:58 +03:00

Added responsive button container component for toolbars (#412)

This commit is contained in:
avele 2019-11-06 14:20:47 +04:00 committed by GitHub
parent 907f2a09d7
commit bd454e56c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 304 additions and 215 deletions

View File

@ -6,7 +6,9 @@ module.exports = {
},
modules: 'commonjs',
useBuiltIns: 'usage'
}]
}
],
'@vue/babel-preset-jsx'
],
plugins: [
'@babel/plugin-transform-runtime',

View File

@ -68,12 +68,15 @@ const config = {
transpileOnly: true,
appendTsSuffixTo: [/\.vue$/]
}
}
},
]
},
{
test: /\.tsx$/,
use: [
{
loader: 'babel-loader'
},
{
loader: 'ts-loader',
options: {

View File

@ -27,7 +27,24 @@
>{{ category.group }}</span>
</div>
<div class="category-actions">
<ResponsiveBtnsContainer>
<template #menuBtn="{ on }">
<v-btn
text
icon
aria-label="Actions"
v-tooltip="'Actions'"
data-testid="CategoryHeader-MobileMenuBtn"
class="category-actions-mobile-menu-btn"
v-on="on"
>
<v-icon
color="grey darken-2"
size="18"
>$vuetify.icons.bars</v-icon>
</v-btn>
</template>
<CategoryHeaderBtn
class="mr-1"
text="New item"
@ -48,58 +65,7 @@
data-testid="CategoryHeader-CategoryDeleteBtn"
@click="deleteCategory"
/>
</div>
<!-- TODO create responsive toolbar component to get rid of duplication of buttons -->
<v-menu bottom left offset-y>
<template v-slot:activator="{ on }">
<v-btn
text
icon
aria-label="Actions"
v-tooltip="'Actions'"
data-testid="CategoryHeader-MobileMenuBtn"
class="category-actions-menu-btn"
v-on="on"
>
<v-icon
color="grey darken-2"
size="18"
>$vuetify.icons.bars</v-icon>
</v-btn>
</template>
<v-list class="category-actions-menu-list">
<v-list-item>
<CategoryHeaderBtn
block
text="New item"
icon="plus"
data-testid="CategoryHeader-NewItemBtn"
@click="openAddItemDialog"
/>
</v-list-item>
<v-list-item>
<CategoryHeaderBtn
block
text="Category settings"
icon="cog"
data-testid="CategoryHeader-CategorySettingsBtn"
@click="openCategorySettingsEditDialog"
/>
</v-list-item>
<v-list-item>
<CategoryHeaderBtn
block
text="Delete category"
icon="trash-alt"
data-testid="CategoryHeader-CategoryDeleteBtn"
@click="deleteCategory"
/>
</v-list-item>
</v-list>
</v-menu>
</ResponsiveBtnsContainer>
</div>
@ -118,11 +84,13 @@ import CategorySettingsDialog from 'client/components/CategorySettingsDialog.vue
import CategoryHeaderBtn from 'client/components/CategoryHeaderBtn.vue'
import Confirm from 'client/helpers/ConfirmDecorator'
import { ICategoryFull } from 'client/service/Category'
import ResponsiveBtnsContainer from 'client/components/ResponsiveBtnsContainer.vue'
@Component({
components: {
CategorySettingsDialog,
CategoryHeaderBtn
CategoryHeaderBtn,
ResponsiveBtnsContainer
}
})
export default class CategoryHeader extends Vue {
@ -209,41 +177,13 @@ export default class CategoryHeader extends Vue {
font-weight: 600;
}
.category-actions-menu-btn {
display: none;
.category-actions-mobile-menu-btn {
margin: 0;
width: 36px;
height: 36px;
}
.category-actions {
white-space: nowrap;
flex: 1;
}
.category-actions-menu-list {
>>> .v-list-item {
height: 36px;
padding: 0;
}
>>> button {
height: 100% !important;
padding: 0 6px;
.v-btn__content {
justify-content: flex-start;
}
}
}
@media (max-width: 768px) {
.category-actions-menu-btn {
display: block;
}
.category-actions {
display: none;
}
.category-header__second-row {
/* Cause menu btn appears after page loading and causes second row to jump */
min-height: 36px;

View File

@ -47,61 +47,12 @@
<v-spacer></v-spacer>
<v-toolbar-items ref="categoryItemActions">
<div class="category-item-toolbar-btns">
<category-item-btn
titleTooltip
size="40px"
iconSize="18"
title="Move item up"
data-testid="CategoryItemToolbar-MoveUpBtn"
icon="arrow-up"
@click="moveItem('up')"
/>
<category-item-btn
titleTooltip
size="40px"
iconSize="18"
title="Move item down"
data-testid="CategoryItemToolbar-MoveDownBtn"
icon="arrow-down"
@click="moveItem('down')"
/>
<category-item-btn
titleTooltip
size="40px"
iconSize="18"
title="Edit item info"
data-testid="CategoryItemToolbar-EditInfoBtn"
icon="cog"
@click="toggleEditItemInfoMenu"
>
<v-icon
v-if="isItemInfoEdited"
class="unsaved-changes-icon"
color="#6495ed"
size="8"
>$vuetify.icons.circle</v-icon>
</category-item-btn>
<category-item-btn
titleTooltip
size="40px"
iconSize="18"
title="Delete item"
icon="trash-alt"
data-testid="CategoryItemToolbar-DeleteBtn"
@click="deleteItem"
/>
</div>
<v-menu
bottom
left
offset-y
:attach="$refs.categoryItemActions && $refs.categoryItemActions.$el"
<v-toolbar-items ref="toolbarItems">
<ResponsiveBtnsContainer
:menuAttach="toolbarItemsEl"
class="category-item-toolbar-btns"
>
<template v-slot:activator="{ on }">
<template #menuBtn="{ on }">
<v-btn
icon
small
@ -119,64 +70,35 @@
</v-btn>
</template>
<v-list class="category-item-toolbar__mobile-menu-list">
<v-list-item>
<category-item-btn
block
text
showTitle
iconSize="18"
title="Move item up"
icon="arrow-up"
data-testid="CategoryItemToolbar-MoveUpBtn"
@click="moveItem('up')"
/>
</v-list-item>
<v-list-item>
<category-item-btn
block
text
showTitle
iconSize="18"
title="Move item down"
icon="arrow-down"
data-testid="CategoryItemToolbar-MoveDownBtn"
@click="moveItem('down')"
/>
</v-list-item>
<v-list-item>
<category-item-btn
block
text
showTitle
iconSize="18"
title="Edit item info"
icon="cog"
data-testid="CategoryItemToolbar-EditInfoBtn"
@click="toggleEditItemInfoMenu"
>
<v-icon
v-if="isItemInfoEdited"
class="unsaved-changes-icon"
color="#6495ed"
size="8"
>$vuetify.icons.circle</v-icon>
</category-item-btn>
</v-list-item>
<v-list-item>
<category-item-btn
block
text
showTitle
iconSize="18"
title="Delete item"
icon="trash-alt"
data-testid="CategoryItemToolbar-DeleteBtn"
@click="deleteItem"
/>
</v-list-item>
</v-list>
</v-menu>
<CategoryItemBtn
v-for="btn in actionBtns"
:key="btn.dataTestId"
titleTooltip
size="40px"
iconSize="18"
:title="btn.title"
:data-testid="btn.dataTestId"
:icon="btn.icon"
:showUnsavedIcon="btn.showUnsavedIcon"
@click="btn.clickFunc"
/>
<template slot="menuItems">
<CategoryItemBtn
v-for="btn in actionBtns"
:key="btn.dataTestId"
block
text
showTitle
iconSize="18"
:title="btn.title"
:data-testid="btn.dataTestId"
:icon="btn.icon"
:showUnsavedIcon="btn.showUnsavedIcon"
@click="btn.clickFunc"
/>
</template>
</ResponsiveBtnsContainer>
</v-toolbar-items>
</v-toolbar>
</div>
@ -239,10 +161,12 @@ import { Prop, Watch } from 'vue-property-decorator'
import normalizeUrl from 'normalize-url'
import Confirm from 'client/helpers/ConfirmDecorator'
import CategoryItemBtn from 'client/components/CategoryItemBtn.vue'
import ResponsiveBtnsContainer from 'client/components/ResponsiveBtnsContainer.vue'
@Component({
components: {
CategoryItemBtn
CategoryItemBtn,
ResponsiveBtnsContainer
}
})
export default class CategoryItemToolbar extends Vue {
@ -255,6 +179,36 @@ export default class CategoryItemToolbar extends Vue {
itemNameEdit: string = this.itemName
itemLinkEdit: string = this.itemLink
itemHackageEdit: string = this.itemHackage
toolbarItemsEl = null
get actionBtns () {
return [
{
title: 'Move item up',
dataTestId: 'CategoryItemToolbar-MoveUpBtn',
icon: 'arrow-up',
clickFunc: () => this.moveItem('up')
},
{
title: 'Move item down',
dataTestId: 'CategoryItemToolbar-MoveDownBtn',
icon: 'arrow-down',
clickFunc: () => this.moveItem('down')
},
{
title: 'Edit item info',
dataTestId: 'CategoryItemToolbar-EditInfoBtn',
icon: 'cog',
showUnsavedIcon: this.isItemInfoEdited,
clickFunc: () => this.toggleEditItemInfoMenu()
},
{
title: 'Delete item',
icon: 'trash-alt',
dataTestId: 'CategoryItemToolbar-DeleteBtn',
clickFunc: () => this.deleteItem()
}
]
}
get isItemInfoEdited () {
return this.itemName !== this.itemNameEdit || this.itemLink !== this.itemLinkEdit || this.itemHackage !== this.itemHackageEdit
@ -274,6 +228,11 @@ export default class CategoryItemToolbar extends Vue {
this.itemLinkEdit = newVal
}
mounted () {
// Cause $refs is not reactive we need to set it manually after its available
this.toolbarItemsEl = this.$refs.toolbarItems
}
toggleEditItemInfoMenu () {
this.isEditItemInfoMenuOpen = !this.isEditItemInfoMenuOpen
}
@ -419,24 +378,26 @@ a.category-item-anchor {
}
.category-item-toolbar-btns {
display: flex;
align-items: center;
flex: 1;
>>> .responsive-bar__desktop-wrap {
display: flex;
align-items: center;
flex: 1;
> * {
width: 1.6rem !important;
height: 1.6rem !important;
}
> * {
width: 1.6rem !important;
height: 1.6rem !important;
}
> *:not(:last-child) {
margin-right: 6px;
> *:not(:last-child) {
margin-right: 6px;
}
}
}
.category-toolbar-mobile-menu-btn {
display: none;
/* Somewhy vuetify sets important "height: 100%"" for direct child buttons of toolbar */
/* For some reason vuetify sets important "height: 100%"" for direct child buttons of toolbar */
height: 1.6rem !important;
width: 1.6rem !important;
border-radius: 0;
margin: 0;
}
@ -457,12 +418,6 @@ a.category-item-anchor {
}
@media (max-width: 768px) {
.category-toolbar-mobile-menu-btn {
display: flex;
}
.category-item-toolbar-btns {
display: none;
}
.category-item-badges {
display: flex;
flex-wrap: wrap;

View File

@ -0,0 +1,82 @@
<script lang="tsx">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop } from 'vue-property-decorator'
import { VMenu, VList, VListItem, VBtn, VIcon } from 'vuetify/lib'
import CategoryHeaderBtn from 'client/components/CategoryHeaderBtn.vue'
@Component
export default class ResponsiveMenu extends Vue {
@Prop() menuAttach
render (h) {
const menuItemsSlot = this.$slots.menuItems || this.$slots.default
const menuItems = menuItemsSlot
.map(slotItem => (
<VListItem>
{slotItem}
</VListItem>
))
const VMenuScopedSlots = {
activator: ({ on }) => (
<span class='responsive-bar__mobile-menu-btn-wrap'>
{this.$scopedSlots.menuBtn({ on })}
</span>
)
}
return (
<div>
<div class='responsive-bar__desktop-wrap'>
{this.$slots.default}
</div>
<VMenu
bottom
left
offset-y
attach={this.menuAttach}
scopedSlots={VMenuScopedSlots}
>
<VList class='responsive-bar__mobile-menu-list'>
{menuItems}
</VList>
</VMenu>
</div>
)
}
}
</script>
<style lang="postcss" scoped>
.responsive-bar__mobile-menu-btn-wrap {
display: none;
}
.responsive-bar__mobile-menu-list {
>>> .v-list-item {
height: 36px;
padding: 0;
}
>>> button {
display: flex;
flex: 1 0 auto;
height: 100% !important;
min-width: 100% !important;
margin: 0;
padding: 0 6px;
.v-btn__content {
justify-content: flex-start;
}
}
}
@media (max-width: 768px) {
.responsive-bar__mobile-menu-btn-wrap {
display: block;
}
.responsive-bar__desktop-wrap {
display: none !important;
}
}
</style>

104
front/package-lock.json generated
View File

@ -405,6 +405,15 @@
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-syntax-jsx": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz",
"integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-syntax-object-rest-spread": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz",
@ -1058,6 +1067,83 @@
"integrity": "sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw==",
"dev": true
},
"@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz",
"integrity": "sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==",
"dev": true
},
"@vue/babel-plugin-transform-vue-jsx": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.0.0.tgz",
"integrity": "sha512-U+JNwVQSmaLKjO3lzCUC3cNXxprgezV1N+jOdqbP4xWNaqtWUCJnkjTVcgECM18A/AinDKPcUUeoyhU7yxUxXQ==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/plugin-syntax-jsx": "^7.2.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"html-tags": "^2.0.0",
"lodash.kebabcase": "^4.1.1",
"svg-tags": "^1.0.0"
}
},
"@vue/babel-preset-jsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.1.tgz",
"integrity": "sha512-SeyndwQZc8MAOkhbJaC34ocTwcKekKkwrwnTMC3YF8VmGp5IQWW5gPIU66bqO9WFBXFA3J3ANsUbP2pj8q8KdQ==",
"dev": true,
"requires": {
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.0.0",
"@vue/babel-sugar-functional-vue": "^1.0.0",
"@vue/babel-sugar-inject-h": "^1.0.0",
"@vue/babel-sugar-v-model": "^1.1.1",
"@vue/babel-sugar-v-on": "^1.1.0"
}
},
"@vue/babel-sugar-functional-vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.0.0.tgz",
"integrity": "sha512-XE/jNaaorTuhWayCz+QClk5AB9OV5HzrwbzEC6sIUY0J60A28ONQKeTwxfidW42egOkqNH/UU6eE3KLfmiDj0Q==",
"dev": true,
"requires": {
"@babel/plugin-syntax-jsx": "^7.2.0"
}
},
"@vue/babel-sugar-inject-h": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.0.0.tgz",
"integrity": "sha512-NxWU+DqtbZgfGvd25GPoFMj+rvyQ8ZA1pHj8vIeqRij+vx3sXoKkObjA9ulZunvWw5F6uG9xYy4ytpxab/X+Hg==",
"dev": true,
"requires": {
"@babel/plugin-syntax-jsx": "^7.2.0"
}
},
"@vue/babel-sugar-v-model": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.1.1.tgz",
"integrity": "sha512-qiPbdUTiqNQdhXzvWQMVfrYGHCiMmscY7j/cudLxdxWZ8AFhgPRVlniVgaWIT7A1iOjs92e8U6qVyqkf0d4ZrA==",
"dev": true,
"requires": {
"@babel/plugin-syntax-jsx": "^7.2.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.0.0",
"camelcase": "^5.0.0",
"html-tags": "^2.0.0",
"svg-tags": "^1.0.0"
}
},
"@vue/babel-sugar-v-on": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.1.0.tgz",
"integrity": "sha512-8DwAj/RLpmrDP4eZ3erJcKcyuLArLUYagNODTsSQrMdG5zmLJoFFtEjODfYRh/XxM2wXv9Wxe+HAB41FQxxwQA==",
"dev": true,
"requires": {
"@babel/plugin-syntax-jsx": "^7.2.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.0.0",
"camelcase": "^5.0.0"
}
},
"@vue/component-compiler-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.0.0.tgz",
@ -6285,6 +6371,12 @@
"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
"dev": true
},
"html-tags": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz",
"integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=",
"dev": true
},
"http-assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz",
@ -7174,6 +7266,12 @@
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0="
},
"lodash.kebabcase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
"integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=",
"dev": true
},
"lodash.tail": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
@ -10441,6 +10539,12 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
},
"svg-tags": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
"integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
"dev": true
},
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",

View File

@ -58,6 +58,8 @@
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-preset-jsx": "^1.1.1",
"@babel/core": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.5",

View File

@ -6,6 +6,7 @@
"client/*"
]
},
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"experimentalDecorators": true,